關(guān)于request.getRequestDispatcher().forward()的妙用及DispatcherType對Filter配置的影響
背景
我們應(yīng)用如上圖所示,Nignx做負(fù)債均衡,微服務(wù)間使用feign進行調(diào)用。
為了方便鑒權(quán)Filter配置攔截的url以及nginx配置對外暴露的url,我們?yōu)樗蟹?wù)設(shè)計了統(tǒng)一的url規(guī)范
類型 | 用途 |
---|---|
v1/xx | 給前端用的url |
v5/xx | 內(nèi)部接口,服務(wù)間調(diào)用 |
因此所有服務(wù)都未配置server.servlet.context-path
那么問題來了,現(xiàn)在我們要把服務(wù)從虛擬機遷移到docker中。
使用公司的docker需要有用于分發(fā)的文根,因為docker服務(wù)提供了公共域名(減小各個產(chǎn)品各自去申請域名的工作量),這樣url必須有前綴文根讓公共域名知道請求往哪個應(yīng)用分發(fā)。
同時docker的ip是變化的。
配置nignx的upstream時只能配置域名,不能在像之前配置虛擬機的ip。
為了降低改動量,因此在保留原有的url外,重新提供一套帶文根的url。–這樣既能保證nginx能夠方便的使用域名來配置upstream,也能保證之前feign調(diào)用的url保持不變。
另外為了保證之前配置的Filter對新url生效,請求進來后需要重定向到老url上。
備注:
上述所說的文根并非指servlet中的context-path,context-path只是servlet中的概念,對于HTTP或者nginx來講,他們是沒有context-path的概念的,他們只是需要利用url中的一小節(jié)進行路由分發(fā)。
因此我們在v1/xx的基礎(chǔ)上增加/a/v1/xx的url即可,至于是通過配置context-path實現(xiàn),還是通過配置servlet-path實現(xiàn)都是可以的。
工作一-新增一套帶文根的url
新增一套帶文根的url,同時又保留老的url,只能給spring的DispatcherServlet新增一個servlet-path,類似于用原生servlet開發(fā)應(yīng)用時,給一個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都要走到同一個業(yè)務(wù)類中,所以得復(fù)用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; }
閉坑指導(dǎo)一
如上代碼配置ServletRegistrationBean時,一定重新配置Name。
因為tomcat在配置servlet的時候會根據(jù)servletName進行去重,如果有同名的servlet,后面的會注冊失敗。
詳細(xì)的可以跟蹤org.springframework.boot.web.servlet.ServletRegistrationBean#addRegistration斷點往下看,這里重點關(guān)注對應(yīng)的邏輯
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 { # 此處參數(shù)校驗等不重要的邏輯 # 通過本次要注冊的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; } } } ... ... }
閉坑指導(dǎo)二
如果按照上述配置代碼,僅僅只是給dispatcherServlet配置新的url及name,會導(dǎo)致上傳附件功能異常。
@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)
... ...
跟蹤堆棧,結(jié)合相關(guān)分析可得到如下信息:
1.spring mvc為了簡化附件上傳,在進入業(yè)務(wù)處理前,對先對附件進解析,即堆棧中的
org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1205)
解析后將HttpServletRequest轉(zhuǎn)換成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。
追蹤堆??梢钥吹饺缦麓a
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; } } }
對比沒改造之前,發(fā)現(xiàn)就是因為mce為空導(dǎo)致。經(jīng)過調(diào)試及查閱相關(guān)信息可知
MultipartConfigElement mce = getWrapper().getMultipartConfigElement(),這一行是獲取servlet上傳附件的配置。
默認(rèn)情況下(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; }
當(dāng)然我們也可以配置全局開關(guān)allowCasualMultipartParsing,詳見百度。
工作二-訪問新url時,內(nèi)部重定向到老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); } }
閉坑指導(dǎo)一
Filter里面不能在調(diào)用chain.doFilter(request,response)
chain代表本次請求的執(zhí)行流程,里面包含了需要執(zhí)行的Filter以及servlet,如果執(zhí)行完forward后再調(diào)用chain.doFilter,會將本該丟棄的流程重新執(zhí)行一遍。
閉坑指導(dǎo)二-Filter的類型
forward后會調(diào)用那些Filter,是之前流程中還沒調(diào)用的Filter嗎?
要回答這個問題需要跟蹤forward后的源碼。
具體邏輯見org.apache.catalina.core.ApplicationFilterFactory#createFilterChain
調(diào)用棧如下
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) { # 去掉多余的參數(shù)校驗,保留主邏輯方便代碼閱讀 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時都遵從統(tǒng)一的邏輯,即DispatcherType是否滿足、path是否滿足、servlet是否滿足。
ps:
看來國外小哥寫代碼也不咋滴啊,上面兩個循環(huán)命名可以合并成一個的。
不僅如此,上述代碼還會導(dǎo)致同時滿足path和servletName的filter會重復(fù)添加
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); }
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis foreach標(biāo)簽使用不當(dāng)導(dǎo)致異常的原因淺析
這篇文章主要介紹了Mybatis foreach標(biāo)簽使用不當(dāng)導(dǎo)致異常的原因探究,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-12-12SpringBoot項目中連接Gauss數(shù)據(jù)庫
本文主要介紹了SpringBoot項目中連接Gauss數(shù)據(jù)庫,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-06-06Java的SpringMVC中控制器返回XML數(shù)據(jù)問題
這篇文章主要介紹了Java的SpringMVC中控制器返回XML數(shù)據(jù)問題,控制器是處理HTTP請求的組件,它們接收來自客戶端的請求,并將其轉(zhuǎn)換為適當(dāng)?shù)捻憫?yīng),這些響應(yīng)可以是動態(tài)生成的?HTML?頁面,也可以是JSON或XML格式的數(shù)據(jù),需要的朋友可以參考下2023-07-07springboot?實現(xiàn)動態(tài)刷新配置的詳細(xì)過程
這篇文章主要介紹了springboot實現(xiàn)動態(tài)刷新配置,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05