Spring MVC注解Controller源码流程解析–映射建立
上一篇中,我们对映射建立的过程做了详细的分析,既然映射关系已经建立完毕了,那么下面就是当请求来临时,如何通过请求去映射集合中寻找出对应的HandlerMethod,然后再交给RequestMappingHandlerAdapter完成请求最终处理。
如果是通过请求路径去映射集合中通过精确匹配进行查询的话,其实实现起来就很简单了,但是因为要加入@RequestMapping中相关请求限制,包括通配符匹配和占位符匹配等等内容,会让寻找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;
}
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对象会被复用,因此使用前,需要清空上一次的数据,这也算是对象复用增加的代码复杂性吧。
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后的路径,这一点切记!
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;
}
}
}
else {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.getHandlerMethod().getMethod();
Method m2 = secondBestMatch.getHandlerMethod().getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
}
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);
}
}
是如何完成最佳匹配的,这个过程本文不展开论述,感兴趣自行研究。
如果没有寻找当前一个RequestMappingInfo能够处理当前request,那么进入handleNoMatch阶段。
handleNoMatch会再次迭代所有 RequestMappingInfo,至少通过 URL 查看是否有任何匹配,并根据不匹配的内容引发异常。
说人话就是找出不匹配的原因,然后抛出对应的异常,告诉用户。
@Override
protected HandlerMethod handleNoMatch(
Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request) throws ServletException {
//借助PartialMatchHelper来分析那些部分匹配的请求,是因为什么原因而无法匹配成功的
//部分匹配的意思就是请求路径匹配上了,但是因为其他条件匹配失败了,例如: 请求头限制等
PartialMatchHelper helper = new PartialMatchHelper(infos, request);
//如果返回的集合为空,表示连请求路径匹配上的都没有---不存在部分匹配现象
if (helper.isEmpty()) {
return null;
}
//请求方式没匹配上
if (helper.hasMethodsMismatch()) {
Set<String> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
Set<MediaType> mediaTypes = helper.getConsumablePatchMediaTypes();
HttpOptionsHandler handler = new HttpOptionsHandler(methods, mediaTypes);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
}
//consume条件不满足
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
//produces条件不满足
if (helper.hasProducesMismatch()) {
Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
}
//请求参数条件不满足
if (helper.hasParamsMismatch()) {
List<String[]> conditions = helper.getParamConditions();
throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
}
return null;
}
PartialMatchHelper会首先获取到所有请求路径匹配成功的RequestMappingInfo:
public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
for (RequestMappingInfo info : infos) {
if (info.getActivePatternsCondition().getMatchingCondition(request) != null) {
this.partialMatches.add(new PartialMatch(info, request));
}
}
}
PartialMatch的构造方法会判断当前RequestMappingInfo不匹配的原因是什么:
public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
this.info = info;
this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null);
this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null);
this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null);
this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null);
}
相关has*Mismatch就是遍历partialMatches集合,然后挨个判断是否存在对应的不匹配原因:
public boolean hasMethodsMismatch() {
for (PartialMatch match : this.partialMatches) {
if (match.hasMethodsMatch()) {
return false;
}
}
return true;
}
到此为止,根据request请求去HandlerMethod注册中心寻找对应HandlerMethod的过程就分析完毕了,下一节,会对handlerMethod的调用过程进行分析。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有