Spring MVC注解Controller源码流程解析--定位HandlerMethod

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC注解Controller源码流程解析--定位HandlerMethod相关的知识,希望对你有一定的参考价值。

Spring MVC注解Controller源码流程解析--定位HandlerMethod


引言

Spring MVC注解Controller源码流程解析–映射建立

上一篇中,我们对映射建立的过程做了详细的分析,既然映射关系已经建立完毕了,那么下面就是当请求来临时,如何通过请求去映射集合中寻找出对应的HandlerMethod,然后再交给RequestMappingHandlerAdapter完成请求最终处理。

如果是通过请求路径去映射集合中通过精确匹配进行查询的话,其实实现起来就很简单了,但是因为要加入@RequestMapping中相关请求限制,包括通配符匹配和占位符匹配等等内容,会让寻找HandlerMethod的过程变的不那么简单,但是也没有那么复杂,下面我们就来看看。


定位HandlerMethod

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception 
		        ....
		        //检查是否是文件上传请求,如果是的话,就返回封装后的MultipartHttpServletRequest
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				//通过当前请求定位到具体处理的handler--这里是handlerMethod
				mappedHandler = getHandler(processedRequest);
				....

我们本节的重点就在getHandler方法中:

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception 
		if (this.handlerMappings != null) 
			for (HandlerMapping mapping : this.handlerMappings) 
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) 
					return handler;
				
			
		
		return null;
	

getHandler方法中会遍历所有可用的HandlerMapping,然后尝试通过当前请求解析得到一个handler,如果不为空,说明找到了,否则借助下一个HandlerMapping继续寻找。

前面已经说过了,注解Controller的映射建立是通过RequestMappingHandlerMapping完成的,那么寻找映射当然也需要通过RequestMappingHandlerMapping完成,因此我们这里只关注RequestMappingHandlerMapping的getHandler流程链即可。


getHandler方法主要是由AbstractHandlerMapping顶层抽象基类提供了一个模板方法实现,具体根据request寻找handler的逻辑实现,是通过getHandlerInternal抽象方法交给子类实现的。

	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception 
	    //这里调用到的是RequestMappingInfoHandlerMapping子类提供的实现
		Object handler = getHandlerInternal(request);
		//如果没找到,尝试寻找兜底的默认handler
		if (handler == null) 
			handler = getDefaultHandler();
		
		//如果还是兜底也不管用,就返回null
		if (handler == null) 
			return null;
		
		//如果此时的handler拿到的还只是一个字符串名字,那么需要先去容器得到对应的实体对象
		if (handler instanceof String) 
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		

		 ...
        
        //构建拦截器链
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

		...
		//跨域处理
		if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) 
			...
		

		return executionChain;
	

RequestMappingInfoHandlerMapping提供的getHandlerInternal实现

  • RequestMappingInfoHandlerMapping主要作为RequestMappingInfo,Request和HandlerMethod三者之间沟通的桥梁,RequestMappingInfo提供请求匹配条件,判断当前Request是否应该交给当前HandlerMethod处理
	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception 
		request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
		try 
		    //调用父类AbstractHandlerMethodMapping的方法
			return super.getHandlerInternal(request);
		
		finally 
		     //把下面这个方法进行内联后,等价于 : request.removeAttribute(MEDIA_TYPES_ATTRIBUTE);
			ProducesRequestCondition.clearMediaTypesAttribute(request);
		
	

清除Request相关属性,主要是因为Request对象会被复用,因此使用前,需要清空上一次的数据,这也算是对象复用增加的代码复杂性吧。


AbstractHandlerMethodMapping提供的getHandlerInternal实现

RequestMappingInfoHandlerMapping重写了父类的getHandlerInternal方法,但只是对Request对象复用进行了相关数据清除工作,核心还是在AbstractHandlerMethodMapping提供的getHandlerInternal实现中。

	protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception 
	    //initLookupPath默认是返回Context-path后面的路径
	    //eg1: 没有设置context-path,请求路径为localhost:5200/volunteer/back/admin/pass/login,那这里返回的就是/volunteer/back/admin/pass/login
	    //eg2: 上面的例子中设置了context-path为/volunteer,那这里返回的就是/back/admin/pass/login
		String lookupPath = initLookupPath(request);
		//获取读锁
		this.mappingRegistry.acquireReadLock();
		try 
		   //通过请求路径去映射集合中寻找对应的handlerMethod
			HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
			return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
		
		finally 
			this.mappingRegistry.releaseReadLock();
		
	

initLookupPath方法中默认会返回的请求路径为剥离掉context-path后的路径,并且后续拦截器中进行路径匹配时,匹配的也是剥离掉context-path后的路径,这一点切记!


根据请求路径去映射集合中寻找HandlerMethod

lookupHandlerMethod是本文的核心关注点,该方法会通过Request定位到对应的HandlerMethod后返回。

具体处理过程,又可以分为三种情况:

  • 精确匹配到一个结果
  • 需要进行最佳匹配
  • 没有匹配到任何结果

因为这部分逻辑比较复杂,因此我们对三种情况分开讨论。


精确匹配到一个结果

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception 
		List<Match> matches = new ArrayList<>();
		//先通过请求路径去pathLookup集合中尝试进行精准匹配--这里的T指的是RequestMappingInfo
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		//精准匹配到了结果
		if (directPathMatches != null) 
			//将结果添加进matches集合中--还会经过RequstMappingInfo的条件校验环节
			addMatchingMappings(directPathMatches, matches, request);
		
		//如果上面精确匹配没有匹配到结果----
		if (matches.isEmpty()) 
		     //将register的keySet集合保存的所有RequestMappingInfo都加入matches集合中去
		     //然后依次遍历每个RequstMappingInfo,通过其自身提供的getMatchingCondition对当前requst请求进行条件匹配
		     //如果不满足条件,是不会加入到当前matches集合中去的
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		
		
		if (!matches.isEmpty()) 
		    //获取matches集合中第一个元素
			Match bestMatch = matches.get(0);
			//如果matches集合元素大于0,说明需要进一步进行模糊搜索
			if (matches.size() > 1) 
				...
			
			//在request对象的属性集合中设置处理当前请求的HandlerMethod
			request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
			//处理当前最佳匹配
			handleMatch(bestMatch.mapping, lookupPath, request);
			return bestMatch.getHandlerMethod();
		
		else 
		     //没有匹配结果
			return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
		
	

addMatchingMappings将得到的匹配结果RequestMappingInfo加入matches集合,但这个过程中还需要进行一些特殊处理,例如:

    @PostMapping(PASS+"login",PASS+"log")

此时PostMapping会映射到两种请求路径上,此时这里需要做的就是,搞清楚到底是哪一个路径匹配上了当前请求,然后修改RequestMappingInfo对应的patterns集合,将多余的请求路径去除掉。

还有就是一个请求路径可能会映射到多个RequestMappingInfo上,例如:

请求路径相同,只是请求方法不同。

	private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) 
	    //遍历每个RequestMappinginfo
		for (T mapping : mappings) 
		     //判断当前RequestMappingInfo是否能够真正映射到当前请求上
			T match = getMatchingMapping(mapping, request);
			//如果返回值不为空,表示可以映射,否则跳过处理下一个
			if (match != null) 
				matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
			
		
	

getMatchingMapping的判断还是通过RequestMappingInfo自身提供的条件进行进行匹配的:

	@Override
	protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) 
		return info.getMatchingCondition(request);
	

检查当前RequestMappingInfo 中的所有条件是否与提供的请求匹配,并返回一个新的RequestMappingInfo,其中包含针对当前请求量身定制的条件。

例如,返回的实例可能包含与当前请求匹配的 URL 模式的子集,并以最佳匹配模式在顶部进行排序。

	@Override
	@Nullable
	public RequestMappingInfo getMatchingCondition(HttpServletRequest request) 
	    //检查@RequestMapping注解提供的method请求方式是否与当前请求匹配,如果不匹配返回null
		RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
		if (methods == null) 
			return null;
		
		//判断设置的请求参数匹配条件是否匹配
		ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
		if (params == null) 
			return null;
		
		//请求头条件
		HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
		if (headers == null) 
			return null;
		
		//Consume条件检查
		ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
		if (consumes == null) 
			return null;
		
		//Produce条件检查
		ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
		if (produces == null) 
			return null;
		
		//PathPatternsRequestCondition一般为null
		PathPatternsRequestCondition pathPatterns = null;
		if (this.pathPatternsCondition != null) 
			pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
			if (pathPatterns == null) 
				return null;
			
		
		//@PostMapping(PASS+"login",PASS+"log")的情况处理 
	    //RequestMappingInfo中的patterns数组中如果存在多个请求路径,需要判断当前请求是具体映射到了那个路径上
	    //然后重新构造一个patternsCondition后返回,该patternsCondition内部包含的只有匹配当前请求路径的那个pattern
		PatternsRequestCondition patterns = null;
		if (this.patternsCondition != null) 
			patterns = this.patternsCondition.getMatchingCondition(request);
			if (patterns == null) 
				return null;
			
		
		//自定义请求限制
		RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
		if (custom == null) 
			return null;
		
		//上面这些条件其中一个不通过,那么返回的结果就为null
		//最后构造一个全新的RequestMappingInfo返回,该RequestMappingInfo中包含的都是匹配上当前请求路径的信息,排除了其他非匹配上的信息
		return new RequestMappingInfo(this.name, pathPatterns, patterns,
				methods, params, headers, consumes, produces, custom, this.options);
	

如果不清楚@ReuqestMapping注解中各个属性的作用,那么把上面每个条件判断过程看一遍就明白了。


handleMatch法主要是针对模糊匹配出来的结果进行相关处理,例如: URI template variables,matrix variables和producible media types处理等等…

上面这些名词关联的注解有: @PathVariable , @MatrixVariable ,producible media types对应的是@RequestMapping中produces设置。

	@Override
	protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) 
		super.handleMatch(info, lookupPath, request);
        //一般返回的就是@RequestMapping注解中的patterns属性,注意@RequestMapping注解可以映射到多个URL上
        //这里返回的就是patterns属性对应的patternsCondition请求匹配条件对象
		RequestCondition<?> condition = info.getActivePatternsCondition();
		//condition默认实现为patternsCondition,因此这里直接走else分支
		if (condition instanceof PathPatternsRequestCondition) 
			extractMatchDetails((PathPatternsRequestCondition) condition, lookupPath, request);
		
		else 
		    //抽取匹配细节,该方法内部会完成对上面这些模板变量,矩阵变量的处理
			extractMatchDetails((PatternsRequestCondition) condition, lookupPath, request);
		
        //如果我们设置了@RequestMapping注解中的produces属性,那么这里会进行处理
		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) 
			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
			//设置到request对象的属性集合中,不用想,肯定会在响应的时候用到
			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
		
	

父类AbstractHandlerMethodMapping中的handleMatch方法,主要是将lookup设置到当前请求对象的属性集合中去:

	protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) 
		request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
	

对模板变量和矩阵变量的抽取

	private void extractMatchDetails(
	       //传入的patternsCondition主要作用在于其内部的patterns属性集合,该集合封装了@RequestMapping注解的patterns属性值内容
			PatternsRequestCondition condition, String lookupPath, HttpServletRequest request) 

		String bestPattern;
		Map<String, String> uriVariables;
		//如果patterns属性集合为空--说明我们直接标注了一个@RequestMapping注解,但是没有指定任何属性限制
		if (condition.isEmptyPathMapping()) 
		    //那就不存在什么模糊匹配了,bestPattern 就是当前请求路径
			bestPattern = lookupPath;
			//模板变量和矩阵变量当然也就不存在了,直接一个空集合
			uriVariables = Collections.emptyMap();
		
		//我们需要考虑是否存在相关模板变量或者矩阵变量
		else 
		    //patterns集合中第一个属性为最佳匹配--这个在addMatchingMappings中被处理完成,不清楚回头看一下
			bestPattern = condition.getPatterns().iterator().next();
			//解析模板变量,eg: "/hotels/hotel" and path "/hotels/1" --> 返回的map就是"hotel"->"1"
			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
			//关于矩阵变量的处理---这里不展开,感兴趣自己debug看一下源码
			if (!getUrlPathHelper().shouldRemoveSemicolonContent()) 
				request.setAttribute(MATRIX_VARIABLES_ATTRIBUTE, extractMatrixVariables(request, uriVariables));
			
			uriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
		
		//设置最佳匹配路径和URL模板变量集合到request对象的属性集合中去
		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
		request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriVariables);
	

关于模板变量和矩阵变量的解析细节这里不多展开了,感兴趣可以按照当前思路自行debug源码。


最佳匹配

	@Nullable
	protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception 
		List<Match> matches = new ArrayList<>();
		List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
		if (directPathMatches != null) 
			addMatchingMappings(directPathMatches, matches, request);
		
		if (matches.isEmpty()) 
			addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
		
		if (!matches.isEmpty()) 
			Match bestMatch = matches.get(0);
			//如果能够处理当前请求的RequestMappingInfo存在多个,下面就需要进行最佳匹配
			if (matches.size() > 1) 
				Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
				matches.sort(comparator);
				bestMatch = matches.get(0);
				if (logger.isTraceEnabled()) 
					logger.trace(matches.size() + " matching mappings: " + matches);
				
				if (CorsUtils.isPreFlightRequest(request)) 
					for (Match match : matches) 
						if (match.hasCorsConfig()) 
							return PREFLIGHT_AMBIGUOUS_MATCH;
						
					Spring MVC注解Controller源码流程解析---请求匹配中的容错处理

Spring MVC注解Controller源码流程解析--HandlerAdapter执行流程--上

spring mvc的RequestMappingHandlerMapping注册HandlerMethod源码分析

Spring MVC注解版本--初识--12

Spring MVC 基础注解之@RequestMapping@Controller

Spring MVC 常用注解@Controller,@RequestMapping,Model和ModelAndView