关于request.getRequestDispatcher().forward()的妙用及DispatcherType对Filter配置的影响
作者:shuxiaohua
背景
我们应用如上图所示,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; } } } ... ... }
闭坑指导二
如果按照上述配置代码,仅仅只是给dispatcherServlet配置新的url及name,会导致上传附件功能异常。
@RequestMapping(value = "/xx/upload", method = RequestMethod.POST) public handleFormUpload(@RequestParam("file") MultipartFile file)
上述接口在处理附件时会抛出以下异常
o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.IllegalStateException: Unable to process parts as no multi-part configuration has been provided
at org.apache.catalina.connector.Request.parseParts(Request.java:2866)
at org.apache.catalina.connector.Request.getParts(Request.java:2834)
at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098)
at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
at javax.servlet.http.HttpServletRequestWrapper.getParts(HttpServletRequestWrapper.java:361)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95)
at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88)
at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122)
at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1205)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
... ...
跟踪堆栈,结合相关分析可得到如下信息:
1.spring mvc为了简化附件上传,在进入业务处理前,对先对附件进解析,即堆栈中的
org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1205)
解析后将HttpServletRequest转换成MultipartHttpServletRequest,方便上层使用,获取附件。
2.serlvet3.0开始,HttpServletRequest接口新增了getPart接口,用于方便的处理附件上传(multipart/form-data类型的请求)。
public Collection<Part> getParts() throws IOException, ServletException; public Part getPart(String name) throws IOException, ServletException;
新版本的spring mvc解析附件时,不在通过commons-fileupload进行处理,而是直接使用的servlet原生的api。
追踪堆栈可以看到如下代码
org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95)
private void parseRequest(HttpServletRequest request) { try { # 使用servlet原生的API对附件进行处理 Collection<Part> parts = request.getParts(); this.multipartParameterNames = new LinkedHashSet<>(parts.size()); MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { ... } setMultipartFiles(files); } }
3.跟踪堆栈,可以定位到追踪抛出异常代码的位置
private void parseParts(boolean explicit) { ... Context context = getContext(); MultipartConfigElement mce = getWrapper().getMultipartConfigElement(); if (mce == null) { if(context.getAllowCasualMultipartParsing()) { mce = new MultipartConfigElement(null, connector.getMaxPostSize(), connector.getMaxPostSize(), connector.getMaxPostSize()); } else { if (explicit) { # 异常在这里抛出 partsParseException = new IllegalStateException( sm.getString("coyoteRequest.noMultipartConfig")); return; } else { parts = Collections.emptyList(); return; } } }
对比没改造之前,发现就是因为mce为空导致。经过调试及查阅相关信息可知
MultipartConfigElement mce = getWrapper().getMultipartConfigElement(),这一行是获取servlet上传附件的配置。
默认情况下(allowCasualMultipartParsing=false),如果不配置multipartConfig的情况下使用getPart接口会抛异常。
如何配置multipartConfig,可见spring boot对dispatch的自动配置。
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); # 对servlet的MultipartConfig进行配置 multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; }
当然我们也可以配置全局开关allowCasualMultipartParsing,详见百度。
工作二-访问新url时,内部重定向到老url上
@WebFilter("/ceshi/*") public class TestFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; 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(); // Add the relevant path-mapped filters to this filter chain 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); } // Add filters that match on servlet name second 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); }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。