背景
我们应用如上图所示,Nignx做负债均衡,微服务间使用feign进行调用。 为了方便鉴权Filter配置拦截的url以及nginx配置对外暴露的url,我们为所有服务设计了统一的url规范
类型 | 用途 |
---|
v1/xx | 给前端用的url | v5/xx | 内部接口,服务间调用 |
因此所有服务都未配置server.servlet.context-path 那么问题来了,现在我们要把服务从虚拟机迁移到docker中。使用公司的docker需要有用于分发的文根,因为docker服务提供了公共域名(减小各个产品各自去申请域名的工作量),这样url必须有前缀文根让公共域名知道请求往哪个应用分发。同时docker的ip是变化的。配置nignx的upstream时只能配置域名,不能在像之前配置虚拟机的ip。 为了降低改动量,因此在保留原有的url外,重新提供一套带文根的url。–这样既能保证nginx能够方便的使用域名来配置upstream,也能保证之前feign调用的url保持不变。 同时为了保证之前配置的Filter对新url生效,请求进来后需要重定向到老url上。 备注上述所说的文根并非指servlet中的context-path,context-path只是servlet中的概念,对于HTTP或者nginx来讲,他们是没有context-path的概念的,他们只是需要利用url中的一小节进行路由分发。因此我们在v1/xx的基础上增加/a/v1/xx的url即可,至于是通过配置context-path实现,还是通过配置servlet-path实现都是可以的。
工作一-新增一套带文根的url
新增一套带文根的url,同时又保留老的url,只能给spring的DispatcherServlet新增一个servlet-path,类似于用原生servlet开发应用时,给一个servlet配置多个url。
<servlet-mapping>
<servlet-name>RedServlet</servlet-name>
<url-pattern>/red/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RedServlet</servlet-name>
<url-pattern>/red/red/*</url-pattern>
</servlet-mapping>
配置方法如下,因为新url和老url都要走到同一个业务类中,所以得复用spring自己自动配置的DispatcherServlet。spring自动配置的DispatcherServlet见org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration
@Bean
public ServletRegistrationBean dispatcherServletWithNewPrefix(DispatcherServlet dispatcherServlet) {
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet,
"/ceshi/*");
registration.setName("ceshi");
return registration;
}
闭坑指导
如上代码配置ServletRegistrationBean时,一定重新配置Name。因为tomcat在配置servlet的时候会根据servletName进行去重,如果有同名的servlet,后面的会注册失败。 详细的可以跟踪org.springframework.boot.web.servlet.ServletRegistrationBean#addRegistration断点往下看,这里重点关注对应的逻辑org.apache.catalina.core.ApplicationContext#addServlet(java.lang.String, java.lang.String, javax.servlet.Servlet, java.util.Map<java.lang.String,java.lang.String>)。
private ServletRegistration.Dynamic addServlet(String servletName, String servletClass,
Servlet servlet, Map<String,String> initParams) throws IllegalStateException {
# 此处参数校验等不重要的逻辑
# 通过本次要注册的servletName获取之前注册过的
Wrapper wrapper = (Wrapper) context.findChild(servletName);
if (wrapper == null) {
wrapper = context.createWrapper();
wrapper.setName(servletName);
context.addChild(wrapper);
} else {
# wrapper不为空说明之前有同名servlet,此次的servlet不在进行注册
if (wrapper.getName() != null &&
wrapper.getServletClass() != null) {
if (wrapper.isOverridable()) {
wrapper.setOverridable(false);
} else {
return null;
}
}
}
... ...
}
工作二-访问新url时,内部重定向到老url上
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
String path = httpServletRequest.getServletPath();
if(path .equals("/ceshi") ) {
String dispatcher = httpServletRequest.getPathInfo();
request.getRequestDispatcher(dispatcher).forward(request,response);
}
}
}
闭坑指导一
Filter里面不能在调用chain.doFilter(request,response) chain代表本次请求的执行流程,里面包含了需要执行的Filter以及servlet,如果执行完forward后再调用chain.doFilter,会将本该丢弃的流程重新执行一遍。
闭坑指导二-Filter的类型
forward后会调用那些Filter,是之前流程中还没调用的Filter吗? 要回答这个问题需要跟踪forward后的源码。具体逻辑见org.apache.catalina.core.ApplicationFilterFactory#createFilterChain 调用栈如下
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
# 去掉多余的参数校验,保留主逻辑方便代码阅读
ApplicationFilterChain filterChain = new ApplicationFilterChain();
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
# 重定向后DispatcherType为FORWARD
DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
for (FilterMap filterMap : filterMaps) {
# 判断Filter的DispatcherType是否匹配
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
# 判断Filter的url是否匹配
if (!matchFiltersURL(filterMap, requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
filterChain.addFilter(filterConfig);
}
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
# 判断Filter是否匹配该servletName
if (!matchFiltersServlet(filterMap, servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
filterChain.addFilter(filterConfig);
}
return filterChain;
}
private static boolean matchDispatcher(FilterMap filterMap, DispatcherType type) {
switch (type) {
case FORWARD :
if ((filterMap.getDispatcherMapping() & FilterMap.FORWARD) != 0) {
return true;
}
break;
case INCLUDE :
if ((filterMap.getDispatcherMapping() & FilterMap.INCLUDE) != 0) {
return true;
}
break;
case REQUEST :
if ((filterMap.getDispatcherMapping() & FilterMap.REQUEST) != 0) {
return true;
}
break;
case ERROR :
if ((filterMap.getDispatcherMapping() & FilterMap.ERROR) != 0) {
return true;
}
break;
case ASYNC :
if ((filterMap.getDispatcherMapping() & FilterMap.ASYNC) != 0) {
return true;
}
break;
}
return false;
}
由上可见,不管是正常的处理,还是从定向后的处理,筛选Filter时都遵从统一的逻辑,即DispatcherType是否满足、path是否满足、servlet是否满足。
ps:看来国外小哥写代码也不咋滴啊,上面两个循环命名可以合并成一个的。不仅如此,上述代码还会导致同时满足path和servletName的filter会重复添加
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath) && !matchFiltersServlet(filterMap, servletName))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
filterChain.addFilter(filterConfig);
}
|