最近有朋友问我:Spring MVC 中如何将请求映射到指定的Controller中的;
结合博主之前的 Spring MVC的请求执行流程 一文,这里做一个更细粒度的分析。
从Spring MVC的请求执行流程来看,DispatcherServlet#doDispatch()方法中会做请求的映射;具体体现在获取请求对应的HandlerExecutionChain
逻辑中。
DispatcherServlet#getHandler()
:
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {// 在Spring初始化的时候会加载所有的handlerMappingsif (this.handlerMappings != null) {for (HandlerMapping mapping : this.handlerMappings) {// 获取请求对应的HandlerExecutionChainHandlerExecutionChain handler = mapping.getHandler(request);if (handler != null) {return handler;}}}return null;
}
Spring启动时会加载所有HandlerMapping类型的Bean到IOC容器中,默认有5个,分别为:
针对HTTP GET、POST、PUT等普通请求,RequestMappingHandlerMapping负责处理,并且其中包含了 所有可以处理的请求路径、以及 请求和相应Controller(具体的类、方法)的映射Mapping。
Spring启动时会加载RequestMappingHandlerMapping到IOC容器中,RequestMappingHandlerMapping中使用MappingRegistry
保存了所有的请求映射关系,使用HandlerMethod
保存了一个请求映射。
RequestMappingInfoHandlerMapping 继承了 抽象类 AbstractHandlerMethodMapping, AbstractHandlerMethodMapping又实现了InitializingBean接口、重写了InitializingBean#afterPropertiesSet()方法;
因此实例化RequestMappingInfoHandlerMapping时会进入到AbstractHandlerMethodMapping#afterPropertiesSet()方法;
进入到initHandlerMethods()方法之后,会遍历IOC容器中所有的Bean,如果Bean被@Controller
或 @RequestMapping
注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping
、并注册到MappingRegistry
中。
所谓的判断Class是否为一个Handler,即判断Class是否被@Controller 或 @RequestMapping 注解标注;
AbstractHandlerMethodMapping#isHandler()方法用于判断某个类是否为一个拥有 MethodHandler的处理器。
具体的实现在其子类RequestMappingHandlerMapping
中:
仅仅判断类有没有被@Controller 或 @RequestMapping 注解标注。
确定一个类被@Controller 或 @RequestMapping 注解标注 后,需要进一步确定Class类中的哪几个方法是HandlerMethod(被@RequestMapping注解标注)、可以处理什么URL路径;
遍历类的方法,Spring封装了几层,具体的执行链路如下:
解析方法的HandlerMethod信息 的逻辑 被封装到函数式接口(@FunctionalInterface
)一路往下传递(从放在MetadataLookup中 到 MethodCallback 中)。
(1) 解析方法的HandlerMethod信息:
如果方法没有被@RequestMapping注解标注,则返回null,否则返回具体的HandlerMapping信息,比如:
(2) 将HandlerMethod注册到MappingRegistry中:
注册完一个HandlerMethod之后,MappingRegistry的内容如下:
请求进入到DispatcherServlet之后的时序图:
对应的代码执行链路如下:
UrlPathHelper#getLookupPathForRequest()方法中会对请求进行路径解析;其中会从两个维度进行路径解析:
这里主要做三件事:
(1)获取请求的ContextPath:
/*** Return the context path for the given request, detecting an include request* URL if called within a RequestDispatcher include.* As the value returned by {@code request.getContextPath()} is not* decoded by the servlet container, this method will decode it.* @param request current HTTP request* @return the context path*/
public String getContextPath(HttpServletRequest request) {String contextPath = (String) request.getAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE);if (contextPath == null) {contextPath = request.getContextPath();}if (StringUtils.matchesCharacter(contextPath, '/')) {// Invalid case, but happens for includes on Jetty: silently adapt it.contextPath = "";}return decodeRequestString(request, contextPath);
}
(2)获取请求HttpServletRequest的URI:
/*** Return the request URI for the given request, detecting an include request* URL if called within a RequestDispatcher include.* As the value returned by {@code request.getRequestURI()} is not* decoded by the servlet container, this method will decode it.*
The URI that the web container resolves should be correct, but some* containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"* in the URI. This method cuts off such incorrect appendices.* @param request current HTTP request* @return the request URI*/
public String getRequestUri(HttpServletRequest request) {String uri = (String) request.getAttribute(WebUtils.INCLUDE_REQUEST_URI_ATTRIBUTE);if (uri == null) {uri = request.getRequestURI();}return decodeAndCleanUriString(request, uri);
}
(3)获取requestUri和contextPath的差值:
/*** Match the given "mapping" to the start of the "requestUri" and if there* is a match return the extra part. This method is needed because the* context path and the servlet path returned by the HttpServletRequest are* stripped of semicolon content unlike the requestUri.*/
@Nullable
private String getRemainingPath(String requestUri, String mapping, boolean ignoreCase) {int index1 = 0;int index2 = 0;for (; (index1 < requestUri.length()) && (index2 < mapping.length()); index1++, index2++) {char c1 = requestUri.charAt(index1);char c2 = mapping.charAt(index2);if (c1 == ';') {index1 = requestUri.indexOf('/', index1);if (index1 == -1) {return null;}c1 = requestUri.charAt(index1);}if (c1 == c2 || (ignoreCase && (Character.toLowerCase(c1) == Character.toLowerCase(c2)))) {continue;}return null;}if (index2 != mapping.length()) {return null;}else if (index1 == requestUri.length()) {return "";}else if (requestUri.charAt(index1) == ';') {index1 = requestUri.indexOf('/', index1);}return (index1 != -1 ? requestUri.substring(index1) : "");
}
如果配置了alwaysUseFullPath
,则不会做Servlet层面的请求路径解析;
此处不对Servlet层面请求路径的解析进行过多讲解,一般不会走进去。
在解析完请求的路径之后,对MappingRegistry加一个读锁,然后再做路径匹配;
真正的请求路径匹配逻辑在AbstractHandlerMethodMapping#lookupHandlerMethod()
方法中;
RequestMethod
的RequestMappingInfo;因为Rest ful风格的缘故,可能会找到多个RequestMappingInfo。
MappingRegistry的urlLookup缓存是在SpringBoot启动时初始化的,见文章上半部分。
如果请求是动态地址,例如:@GetMapping("/get/{orderId}")
,则无法从MappingRegistry的urlLookup缓存中获取到请求路径对应的RequestMappingInfo,此时需要遍历所有的RequestMappingInfo,做正则匹配,进而找到具体的HandlerMethod。
因为请求的RequestMethod为GET,所以GET类型的RequestMappingInfo符合条件;
MappingRegistry的mappingLookup缓存也是在SpringBoot启动时初始化的,见文章上半部分。
最后将获取到的HandlerMethod一路向上返回;
Spring启动时会加载HandlerMapping的所有实现类;包括:负责处理HTTP请求的RequestMappingHandlerMapping;
RequestMappingHandlerMapping中使用MappingRegistry
保存了所有的请求映射关系,使用HandlerMethod
保存了一个请求映射;
RequestMappingInfoHandlerMapping间接实现了InitializingBean
接口,因此RequestMappingInfoHandlerMapping实例化时会进去到重写后的InitializingBean#afterPropertiesSet()逻辑;
遍历IOC容器中所有的Bean,如果Bean被@Controller
或 @RequestMapping
注解标注,则将Class中被@RequesMapping 注解标注的方法解析为HandlerMapping
、并注册到MappingRegistry
中。
将请求路径 和 RequestMappingInfo 作为KV保存在urlLookup缓存中;
private final MultiValueMap urlLookup = new LinkedMultiValueMap<>();
将RequestMappingInfo 和 HandlerMethod 作为kv保存在mappingLookup缓存中。
private final Map mappingLookup = new LinkedHashMap<>();
请求打过来之后,首先会对请求路径从两个维度进行处理;
然后根据解析后的请求路径去MappingRegistry中的urlLookup缓存找RequestMappingInfo;
由于Rest ful风格的存在,可能根据一个请求路径找到多个RequestMappingInfo;
所以需要进一步通过RequestMethod找到执行类型(GET/POST/PUT/DELTE)的RequestMappingInfo
动态地址无法根据请求路径找到具体的RequestMappingInfo,需要遍历所有的RequestMappingInfo做正则匹配,找到具体的RequestMappingInfo。
比如:@GetMapping("/get/{orderId}")
然后再根据RequestMappingInfo
去MappingRegistry
中的mappingLookup缓存找HandlerMethod。