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是怎样的?和普通的请求又有什么不同,为什么这样做等等。

注解映射器的实现

实现大致分为五步:

  1. 注册:扫描注解,分装RequestMappingInfo以及Handler,为什么要这么封装RequestMappingInfo,主要是方便我们加入匹配的规则。
  2. 遍历:遍历所有的映射配置,找出和请求对应的映射
  3. 匹配:根据找出的映射再次进行匹配条件
  4. 排序:当匹配到多个映射的时候,就先排个序,根据权重等条件
  5. 异常:如果匹配多个是抛异常的没有匹配到则返回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源码之映射体系的主要内容,如果未能解决你的问题,请参考以下文章

Spring Web源码之映射体系

Spring Web源码之映射体系

Spring Web源码之执行体系一

Spring Web源码之执行体系一

Spring Web源码之执行体系一

Spring Web源码之执行体系一