基于SpringMVC自定义HandlerMapping扩展Handler&注册自己的Servlet
今天在研究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() 方法