Spring Web源码之映射体系
Posted 木兮君
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Web源码之映射体系相关的知识,希望对你有一定的参考价值。
前言
今天小编继续分享关于spring mvc的内容,前两篇博文主要讲了主体结构,主要流程以及核心的组件。接下来小编进入流程中的细节,首先就是映射体系,一般咱们调用方法的时候基本根据url找到对应的handler,那spring mvc是怎么通过url找到对应的handler,其里面主要有哪些组件来做这些工作的,就是今天所要讲的内容。话不多说进入正题。
Spring MVC映射体系
映射器核心作用:就是基于httpRequest匹配Handler。匹配不一定都是url,其中还包括了请求头请求参数请求方法等等,所以小编说基于httpRequest来匹配。那继续进入映射体系结构。
映射体系结构
这里小编通过idea的控件导出类图并且加了一些说明:
如果有不明白的还是希望大家去翻一下源码,其中重要的解释小编已经给出,希望对阅读源码有所帮助。
接着往下看如何根据request来找到我们的handler。
Url映射以及注解映射具体实现
Url映射
url映射是如何get到Handler,先看一下时序图:
整个过程还是相对比较简单的。可以看下源码。小编先用测试用例代码演示
代码演示:
记得因为registerHandler是protect的索引记得测试类的包名相同即可。
@Test
public void urlMappingTest() throws Exception
SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
Object handler = new Object();
//注册handler
simpleUrlHandlerMapping.registerHandler("/hello", handler);
HandlerInterceptor handlerInterceptor = new HandlerInterceptor()
;
//添加拦截器
simpleUrlHandlerMapping.setInterceptors(handlerInterceptor);
//刷新拦截器
simpleUrlHandlerMapping.initInterceptors();
//获取执行器链
HandlerExecutionChain executionChain = simpleUrlHandlerMapping.
getHandler(new MockHttpServletRequest("GET", "/hello"));
Assert.assertSame(handler, executionChain.getHandler());
Assert.assertTrue(Arrays.stream(executionChain.getInterceptors()).anyMatch(item->item==handlerInterceptor));
相对来说url映射还是蛮简单的接下来是注解映射。
注解映射
其实大家大部分情况都是使用注解映射,在Controller层总是添加@RequestMapping注解,当然现在有@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping。这些只是简化版的@RequestMapping。
先看一下注解映射到底匹配了多少内容:
小编先代码演示一下注解映射;
代码演示:
public class RequestMappingTest
private RequestMappingHandlerMapping requestMappingHandlerMapping;
private TestController testController;
private RequestMappingInfo requestMappingInfo;
@Before
public void init()
requestMappingHandlerMapping = new RequestMappingHandlerMapping();
testController = new TestController();
@Test
public void matchTest() throws Exception
registerHandler("hello");
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest("GET", "/hello");
//请求头类型
mockHttpServletRequest.addHeader("auth","123");
//请求类型
mockHttpServletRequest.setContentType("text/json");
// mockHttpServletRequest.addHeader("ton","123");
//参数
mockHttpServletRequest.addParameter("userName","world");
//mockHttpServletRequest.addParameter("haha","123");
HandlerExecutionChain handlerExecutionChain = requestMappingHandlerMapping.
getHandler(mockHttpServletRequest);
Object handler = handlerExecutionChain.getHandler();
Assert.notNull(handler,"匹配成功");
public void registerHandler(String name,Class<?>... paramTypes) throws NoSuchMethodException
Method method = testController.getClass().getMethod(name, paramTypes);
//创建HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(testController,method);
//这边类和方法都可能有@requestMapping注解,则需要将两个合并变成一个
RequestMappingInfo mappingForMethod = requestMappingHandlerMapping.getMappingForMethod(method, TestController.class);
//注册进去
requestMappingHandlerMapping.registerMapping(mappingForMethod,handlerMethod,method);
@Controller
public static class TestController
@RequestMapping(value = "/hello",method = RequestMethod.GET,headers = "auth","!ton",params = "userName","!haha",consumes = "text/json")
public void hello()
相信大家以前也没在@RequestMapping中设置过这么多匹配的规则,这边小编也算是见识到了。当然这里还差ant表达式的匹配,小编这儿也就不演示了,希望大家自己也测试一下。(就是value里面hello的替换)
url的设计在实现过程中也非常重要。大家有空可以自己想一下,比方说分享出去的链接,在微信或钉钉页面能够展示图片内容,他的url是怎样的?和普通的请求又有什么不同,为什么这样做等等。
注解映射器的实现
实现大致分为五步:
- 注册:扫描注解,分装RequestMappingInfo以及Handler,为什么要这么封装RequestMappingInfo,主要是方便我们加入匹配的规则。
- 遍历:遍历所有的映射配置,找出和请求对应的映射
- 匹配:根据找出的映射再次进行匹配条件
- 排序:当匹配到多个映射的时候,就先排个序,根据权重等条件
- 异常:如果匹配多个是抛异常的没有匹配到则返回null。
这里看过了实现的步骤,那首先看下注册步骤中注册器的主要结构
注册器结构:
这里小编只是把核心的一些结构展示了一下,其实看源码还要其他组件,其他组件用到的基本很少(可能当初设计者心思缜密)。
上面小编已经演示过注册的过程代码了,即创建RequestMappingInfo(基于注解) 和HandlerMethod(基于method以及bean) 然后注册到RequestMappingHandlerMapping 中区。那我们接下来看下注册的源码。
一、注册
注册源码:
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerMapping
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethod
注册有两个一样的方法(只是调换了一下参数顺序),不知道为什么但最终调用到的是:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#register
public void register(T mapping, Object handler, Method method)
//读写锁是为了防止并发,不过一般我们都是初始化的时候发生,不是很需要
this.readWriteLock.writeLock().lock();
try
//根据bean和methos来封装HandlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
//封装进去
this.mappingLookup.put(mapping, handlerMethod);
//这里为下面的遍历做准备,再次封装map,url对应多个mapping
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls)
this.urlLookup.add(url, mapping);
String name = null;
if (getNamingStrategy() != null)
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null)
this.corsLookup.put(handlerMethod, corsConfig);
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
finally
this.readWriteLock.writeLock().unlock();
接下来是第二步骤遍历
二、遍历
大家会不会觉得遍历特简单,不就是循环查找,其实里面细节还是很多的,因为有时候涉及到正则表达,那是相当耗时间的,所以这里其实做了一次优化。那怎么优化的呢请看下图:
这里小编稍作解释:
这边其实会有两个map,最终会调用到MappingRegistry中的map,第一个map是url对应MappingRegistry的key的集合。这样在查找匹配的时候就会大大节约时间。
源码阅读
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#getHandlerInternal
然后调用到org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception
List<Match> matches = new ArrayList<>();
//首先用urlLookup查找url是否有多个mapping,有的话再去查找
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null)
//匹配条件封装匹配到的mappingInfo
addMatchingMappings(directPathMatches, matches, request);
//为空则查询所有的
if (matches.isEmpty())
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
//匹配多个的情况下
if (!matches.isEmpty())
//排序
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1)
if (logger.isTraceEnabled())
logger.trace(matches.size() + " matching mappings: " + matches);
if (CorsUtils.isPreFlightRequest(request))
return PREFLIGHT_AMBIGUOUS_MATCH;
Match secondBestMatch = matches.get(1);
//如果两个排序权重一样则会报错。
if (comparator.compare(bestMatch, secondBestMatch) == 0)
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': " + m1 + ", " + m2 + "");
handleMatch(bestMatch.mapping, lookupPath, request);
//返回HandlerMethod
return bestMatch.handlerMethod;
else
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
三、匹配条件
找到mapping后得去匹配条件找到对应的HandlerMethod
上面的代码
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#addMatchingMappings
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request)
for (T mapping : mappings)
T match = getMatchingMapping(mapping, request);
if (match != null)
//不为空则this.mappingRegistry.getMappings().get(mapping))拿到HandlerMethod
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
最终调用到
org.springframework.web.servlet.mvc.method.RequestMappingInfo#getMatchingCondition来匹配
public RequestMappingInfo getMatchingCondition(HttpServletRequest request)
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (methods == null || params == null || headers == null || consumes == null || produces == null)
return null;
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null)
return null;
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null)
return null;
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
这里先匹配的是方法,最后匹配的是url。因为这样匹配最简单,范围从小到大。然后更耗性能放到后面
四、排序
条件匹配后,先进行排序,也就是两个RequestMappingInfo 互相排序比较:
排序规则:org.springframework.web.servlet.mvc.method.RequestMappingInfo#compareTo
public int compareTo(RequestMappingInfo other, HttpServletRequest request)
int result;
// Automatic vs explicit HTTP HEAD mapping
if (HttpMethod.HEAD.matches(request.getMethod()))
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0)
return result;
result = this.patternsCondition.compareTo(other.getPatternsCondition(), request);
if (result != 0)
return result;
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0)
return result;
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0)
return result;
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0)
return result;
result = this.producesCondition.compareTo(other.getProducesCondition(), request);
if (result != 0)
return result;
// Implicit (no method) vs explicit HTTP method mappings
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0)
return result;
result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
if (result != 0)
return result;
return 0;
排序从源码中看就是url在前面
五、匹配不到或匹配多个
匹配不到的情况下返回null,如果有多个的话则抛出异常IllegalStateException,当然如果返回null最终还是会抛出异常。
那接下来小编整理了一份比较全的时序图。
注解映射的全流程
上面匹配所有的时候还需要走一遍getMatchingMapping以及getMatchingCondition。
总结
今天主要分享的是spring mvc的映射体系,他是如何根据url查找到handler,当然里面少了一些注册的流程,但那不重要。虽说映射比较简单但是里面的细节还是很多的,希望大家有所收获,更加透彻理解映射流程。映射结束后小编继续为大家带来之后的执行体系。谢谢!
以上是关于Spring Web源码之映射体系的主要内容,如果未能解决你的问题,请参考以下文章