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 基础注解之@RequestMapping@Controller
Spring MVC 常用注解@Controller,@RequestMapping,Model和ModelAndView