今天在研究Zuul 源碼的時候發現其是自定義HandlerMapping 與 自己的Handler,相當於在SpringMVC的org.springframework.web.servlet.DispatcherServlet#doDispatch 方法中通過HandlerMapping 選擇handler時,擴展自己的HandlerMapping 和 Handler。讓MVC優先走自己的handlerMapping 選擇自己的handler。
下面貼出自己的測試代碼
1. 測試代碼
基於Servlet 實現自己的Handler。
1. MyServlet
package cn.xm.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class MyServlet extends HttpServlet { public MyServlet() { System.out.println("======myServlet"); } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("cn.xm.servlet.MyServlet"); } }
相當於我們原來的Servlet,然后注入到下面的Controller,在容器啟動會自動創建。調用過程中,會通過Controller 然后調用到Servlet。
2. MyController
package cn.xm.servlet; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.ServletWrappingController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyController extends ServletWrappingController { public MyController() { /** * 父類org.springframework.web.servlet.mvc.ServletWrappingController實現了InitializingBean 接口,在 afterPropertiesSet 方法中會反射創建 Servlet 對象 */ setServletClass(MyServlet.class); } @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { return super.handleRequestInternal(request, response); } }
構造方法指明Servlet的class類型,容器啟動會自動創建。
handleRequestInternal 方法是org.springframework.web.servlet.mvc.Controller#handleRequest 接口定義的方法,在SpringMVC 獲取到Controller 之后會自動調用該方法。上面會調用到父類org.springframework.web.servlet.mvc.ServletWrappingController#handleRequestInternal:(也就是會調用到MyServlet.service() 方法)
@Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { this.servletInstance.service(request, response); return null; }
3. MyHandlerMapping
自己的HandlerMapping,在獲取的時候第一次注冊相關的路由以及Controller。
package cn.xm.servlet; import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; import javax.servlet.http.HttpServletRequest; public class MyHandlerMapping extends AbstractUrlHandlerMapping { private MyController myController; private volatile boolean dirty = true; public MyHandlerMapping(MyController myController) { this.myController = myController; /** * 設置順序,值越小越先匹配 (在SpringMVC 處理流程會優先調用) */ setOrder(-200); } @Override protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { if (this.dirty) { synchronized (this) { if (this.dirty) { registerHandlers(); this.dirty = false; } } } return super.lookupHandler(urlPath, request); } private void registerHandlers() { // 我們假設自己的請求路徑都從 /api/v1/my/ 開始 registerHandler("/api/v1/my/**", this.myController); } }
4. MyConfiguration
注冊相關的對象到Spring 容器
package cn.xm.servlet; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyConfiguration { @Bean public MyController myController() { return new MyController(); } @Bean public MyHandlerMapping myHandlerMapping(MyController myController) { return new MyHandlerMapping(myController); }
5. 還有另一種注冊方式
package cn.xm.servlet; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyServletAutoconfiguration { /** * 注入servlet,這種注入方式不會走SpringMVC的中央處理器, 相當於tomcat 在選擇servlet的時候直接走該Servlet * * @return */ @Bean @ConditionalOnMissingBean(name = "myServlet") public ServletRegistrationBean myServlet() { ServletRegistrationBean myServlet = new ServletRegistrationBean(new MyServlet(), "/my/*"); return myServlet; } }
這種方式注入進去的不走SpringMVC的DispatcherServlet 邏輯,相當於和SpringMVC的DispatcherServlet 是平級,供tomcat 處理時選擇。
上面的配置就實現了兩種方式。
第一種基於AbstractUrlHandlerMapping 與 ServletWrappingController 走SpringMVC 的機制,在Spring的中央處理器進行分發。
第二種基於MyServletAutoconfiguration 這種方式不走SpringMVC,相當於Tomcat 根據路由選擇servlet的時候直接選中滿足條件的servlet。
2. 相關原理
1. 基於SpringMVC AbstractUrlHandlerMapping 原理
1. cn.xm.servlet.MyServlet 初始化時機
斷點跟蹤如下:
可以看出是在org.springframework.web.servlet.mvc.ServletWrappingController#afterPropertiesSet 方法中反射創建的對象:
@Override public void afterPropertiesSet() throws Exception { if (this.servletClass == null) { throw new IllegalArgumentException("'servletClass' is required"); } if (this.servletName == null) { this.servletName = this.beanName; } this.servletInstance = this.servletClass.newInstance(); this.servletInstance.init(new DelegatingServletConfig()); }
2. 調用原理
訪問: /api/v1/my/t3
(1) org.apache.catalina.mapper.Mapper#internalMapWrapper 找servlet (tomcat流程)

private final void internalMapWrapper(ContextVersion contextVersion, CharChunk path, MappingData mappingData) throws IOException { int pathOffset = path.getOffset(); int pathEnd = path.getEnd(); boolean noServletPath = false; int length = contextVersion.path.length(); if (length == (pathEnd - pathOffset)) { noServletPath = true; } int servletPath = pathOffset + length; path.setOffset(servletPath); // Rule 1 -- Exact Match MappedWrapper[] exactWrappers = contextVersion.exactWrappers; internalMapExactWrapper(exactWrappers, path, mappingData); // Rule 2 -- Prefix Match boolean checkJspWelcomeFiles = false; MappedWrapper[] wildcardWrappers = contextVersion.wildcardWrappers; if (mappingData.wrapper == null) { internalMapWildcardWrapper(wildcardWrappers, contextVersion.nesting, path, mappingData); if (mappingData.wrapper != null && mappingData.jspWildCard) { char[] buf = path.getBuffer(); if (buf[pathEnd - 1] == '/') { /* * Path ending in '/' was mapped to JSP servlet based on * wildcard match (e.g., as specified in url-pattern of a * jsp-property-group. * Force the context's welcome files, which are interpreted * as JSP files (since they match the url-pattern), to be * considered. See Bugzilla 27664. */ mappingData.wrapper = null; checkJspWelcomeFiles = true; } else { // See Bugzilla 27704 mappingData.wrapperPath.setChars(buf, path.getStart(), path.getLength()); mappingData.pathInfo.recycle(); } } } if(mappingData.wrapper == null && noServletPath && contextVersion.object.getMapperContextRootRedirectEnabled()) { // The path is empty, redirect to "/" path.append('/'); pathEnd = path.getEnd(); mappingData.redirectPath.setChars (path.getBuffer(), pathOffset, pathEnd - pathOffset); path.setEnd(pathEnd - 1); return; } // Rule 3 -- Extension Match MappedWrapper[] extensionWrappers = contextVersion.extensionWrappers; if (mappingData.wrapper == null && !checkJspWelcomeFiles) { internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); } // Rule 4 -- Welcome resources processing for servlets if (mappingData.wrapper == null) { boolean checkWelcomeFiles = checkJspWelcomeFiles; if (!checkWelcomeFiles) { char[] buf = path.getBuffer(); checkWelcomeFiles = (buf[pathEnd - 1] == '/'); } if (checkWelcomeFiles) { for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { path.setOffset(pathOffset); path.setEnd(pathEnd); path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); path.setOffset(servletPath); // Rule 4a -- Welcome resources processing for exact macth internalMapExactWrapper(exactWrappers, path, mappingData); // Rule 4b -- Welcome resources processing for prefix match if (mappingData.wrapper == null) { internalMapWildcardWrapper (wildcardWrappers, contextVersion.nesting, path, mappingData); } // Rule 4c -- Welcome resources processing // for physical folder if (mappingData.wrapper == null && contextVersion.resources != null) { String pathStr = path.toString(); WebResource file = contextVersion.resources.getResource(pathStr); if (file != null && file.isFile()) { internalMapExtensionWrapper(extensionWrappers, path, mappingData, true); if (mappingData.wrapper == null && contextVersion.defaultWrapper != null) { mappingData.wrapper = contextVersion.defaultWrapper.object; mappingData.requestPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.wrapperPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.requestPath.setString(pathStr); mappingData.wrapperPath.setString(pathStr); } } } } path.setOffset(servletPath); path.setEnd(pathEnd); } } /* welcome file processing - take 2 * Now that we have looked for welcome files with a physical * backing, now look for an extension mapping listed * but may not have a physical backing to it. This is for * the case of index.jsf, index.do, etc. * A watered down version of rule 4 */ if (mappingData.wrapper == null) { boolean checkWelcomeFiles = checkJspWelcomeFiles; if (!checkWelcomeFiles) { char[] buf = path.getBuffer(); checkWelcomeFiles = (buf[pathEnd - 1] == '/'); } if (checkWelcomeFiles) { for (int i = 0; (i < contextVersion.welcomeResources.length) && (mappingData.wrapper == null); i++) { path.setOffset(pathOffset); path.setEnd(pathEnd); path.append(contextVersion.welcomeResources[i], 0, contextVersion.welcomeResources[i].length()); path.setOffset(servletPath); internalMapExtensionWrapper(extensionWrappers, path, mappingData, false); } path.setOffset(servletPath); path.setEnd(pathEnd); } } // Rule 7 -- Default servlet if (mappingData.wrapper == null && !checkJspWelcomeFiles) { if (contextVersion.defaultWrapper != null) { mappingData.wrapper = contextVersion.defaultWrapper.object; mappingData.requestPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.wrapperPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); mappingData.matchType = MappingMatch.DEFAULT; } // Redirection to a folder char[] buf = path.getBuffer(); if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') { String pathStr = path.toString(); WebResource file; // Handle context root if (pathStr.length() == 0) { file = contextVersion.resources.getResource("/"); } else { file = contextVersion.resources.getResource(pathStr); } if (file != null && file.isDirectory() && contextVersion.object.getMapperDirectoryRedirectEnabled()) { // Note: this mutates the path: do not do any processing // after this (since we set the redirectPath, there // shouldn't be any) path.setOffset(pathOffset); path.append('/'); mappingData.redirectPath.setChars (path.getBuffer(), path.getStart(), path.getLength()); } else { mappingData.requestPath.setString(pathStr); mappingData.wrapperPath.setString(pathStr); } } } path.setOffset(pathOffset); path.setEnd(pathEnd); }
mappingData.wrapper 在Rule 7 -- Default servlet 找到SpringMVC 默認的DispatcherServlet
(2) org.apache.catalina.core.ApplicationFilterChain#internalDoFilter 經過一系列的filter之后進入DispatcherServlet 父類的相關方法

private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // Call the next filter if there is one if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res, this}; SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.filter"), e); } return; } // We fell off the end of the chain -- call the servlet instance try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } if (request.isAsyncSupported() && !servletSupportsAsync) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } // Use potentially wrapped request from this point if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal); } else { servlet.service(request, response); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.servlet"), e); } finally { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(null); lastServicedResponse.set(null); } } }
調用鏈如下:
(2)org.springframework.web.servlet.DispatcherServlet#doDispatch 進入SpringMVC 流程

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
1》 org.springframework.web.servlet.DispatcherServlet#getHandler 找相關的Handler:

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; }
這里的handlerMappings 如下:
然后調用hm.getHandler 找handler, 會調用到: cn.xm.servlet.MyHandlerMapping#lookupHandler 第一次初始化相關的handler。然后根據路徑進行匹配找到的handler 如下:
2》然后調用org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter#handle 進行執行:
@Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response); }
這里會調用到父類方法org.springframework.web.servlet.mvc.AbstractController#handleRequest:

public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { if (HttpMethod.OPTIONS.matches(request.getMethod())) { response.setHeader("Allow", getAllowHeader()); return null; } // Delegate to WebContentGenerator for checking and preparing. checkRequest(request); prepareResponse(response); // Execute handleRequestInternal in synchronized block if required. if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { return handleRequestInternal(request, response); } } } return handleRequestInternal(request, response); }
繼續調用到cn.xm.servlet.MyController-》org.springframework.web.servlet.mvc.ServletWrappingController#handleRequestInternal:
@Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { this.servletInstance.service(request, response); return null; }
到這里就從SpringMVC的流程進入到我們的Servlet開始調用方法 cn.xm.servlet.MyServlet#service。
2. 基於ServletRegistrationBean 方式原理
1. 創建時機
org.springframework.boot.web.servlet.ServletRegistrationBean#onStartup 方法如下:
@Override public void onStartup(ServletContext servletContext) throws ServletException { Assert.notNull(this.servlet, "Servlet must not be null"); String name = getServletName(); if (!isEnabled()) { logger.info("Servlet " + name + " was not registered (disabled)"); return; } logger.info("Mapping servlet: '" + name + "' to " + this.urlMappings); Dynamic added = servletContext.addServlet(name, this.servlet); if (added == null) { logger.info("Servlet " + name + " was not registered " + "(possibly already registered?)"); return; } configure(added); }
相當於調用javax.servlet.ServletContext#addServlet(java.lang.String, javax.servlet.Servlet) 方法動態的注冊Servlet, 這是ServletContext 提供的動態注冊Servlet 的API。
調用鏈如下:
核心是在 org.springframework.boot.context.embedded.EmbeddedWebApplicationContext#selfInitialize 調用onStartUp 方法
private void selfInitialize(ServletContext servletContext) throws ServletException { prepareEmbeddedWebApplicationContext(servletContext); ConfigurableListableBeanFactory beanFactory = getBeanFactory(); ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes( beanFactory); WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, getServletContext()); existingScopes.restore(); WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, getServletContext()); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } }
2. 調用時機
訪問:/my/t3
(1) 上面org.apache.catalina.mapper.Mapper#internalMapWrapper 方法在執行過程中從規則 Rule 2 -- Prefix Match 找到對應的myServlet。
(2) org.apache.catalina.core.ApplicationFilterChain#internalDoFilter 經過一系列的filter之后進入myServlet.service() 方法