??@RequestMapping
支持基于value
、path
、method
、params
、headers
、consumers
、produces
的匹配,本文对基于params
的匹配过程进行分析。
??系列博文《Spring 注解面面通 之 @RequestMapping 请求匹配处理方法源码解析》中对请求匹配@RequestMapping
注释方法流程进行了分析。
??AbstractHandlerMethodMapping.lookupHandlerMethod(...)
方法负责查找请求最佳匹配的处理方法。ConsumesRequestCondition
类负责基于consumers
的匹配过程,可以配置多个标头条件,多个条件之间是逻辑或(||
)的关系。
???源码解析
??1) AbstractHandlerMethodMapping.lookupHandlerMethod(...)
方法。
??① 在mappingRegistry.urlLookup
中查找与请求路径完全匹配的映射。
??② 在①
中查找结果,查找与请求完全匹配的匹配器。
??③ 若②
中查找无结果,则遍历注册的所有映射,进行进一步匹配,以查找合适的匹配器。
??④ 若③
中查找匹配器列表不为空,则从中通过MatchComparator
比较器选择最优匹配器。
??⑤ 若请求是有效的CORS
类型的请求,则返回指定的处理方法,默认是PREFLIGHT_AMBIGUOUS_MATCH
,即new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"))
。
??⑥ 判断③
中查找匹配器列表,最优和次优匹配器是否一致,若两者一致,则违背映射规则,抛出异常。
??⑦ 处理查找到最优匹配器的情况,这步骤大致包括:存储到最优匹配到请求属性、解析URI模板变量,存储到请求属性、解析矩阵变量,存储到请求属性、解析可生成媒体类型,存储到请求属性。
??⑧ 处理未查找到匹配器的情况,这步骤大致包括:为确保无误,再次进行匹配查找、若仍无匹配结果,则抛出异常。
/*** 查找请求最优匹配的处理方法.* 如果找到多个处理方法,则选择最优匹配的处理方法.* @param lookupPath 在当前Servlet映射中映射查找路径.* @param request 当前请求实例.* @return 最优匹配的处理方法,如果没有匹配处理方法,返回null.*/
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();// 查找与请求路径完全匹配的映射.List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);// 查找与请求完全匹配的匹配器.if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);}// 若无完全匹配器,则需遍历所有的映射,进行进一步匹配.if (matches.isEmpty()) {
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);}// 若查找的匹配器列表不为空,则从中选择最优的匹配器.if (!matches.isEmpty()) {
// 获取匹配比较器.Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));// 根据比较器对匹配器进行排序.matches.sort(comparator);if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);}// 取得第一个匹配器,用于选取最优匹配器.Match bestMatch = matches.get(0);if (matches.size() > 1) {
// 如果请求是有效的CORS类型请求,返回指定的处理方法.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();throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");}}// 处理匹配器.// 1.存储到最优匹配到请求属性.// 2.解析URI模板变量,存储到请求属性.// 3.解析矩阵变量,存储到请求属性.// 4.解析可生成媒体类型,存储到请求属性.handleMatch(bestMatch.mapping, lookupPath, request);// 返回最优匹配器的处理方法.return bestMatch.handlerMethod;}else {
// 处理无匹配器的情况.// 1.再次进行匹配查找.// 2.若仍无匹配,抛出异常.return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);}
}
??2) AbstractHandlerMethodMapping.addMatchingMappings(...)
-> RequestMappingInfoHandlerMapping.getMatchingMapping(...)
-> RequestMappingInfo.getMatchingCondition(...)
。
??AbstractHandlerMethodMapping.addMatchingMappings(...)
-> RequestMappingInfoHandlerMapping.getMatchingMapping(...)
-> RequestMappingInfo.getMatchingCondition(...)
中处理的中间流程,其中并没有涉及过多步骤,在此不做深入分析。
??3) ConsumesRequestCondition.getMatchingCondition(...)
方法。
??① 如果请求是有效的CORS
类型请求,则返回PRE_FLIGHT_MATCH
,即new ConsumesRequestCondition()
。
??② 若映射配置媒体类型表达式为空,则返回当前实例,表示匹配通过。
??③ 解析请求携带Content-type
标头内的媒体类型。若请求未携带Content-type
标头,则默认为使用application/octet-stream
。
??④ 映射配置媒体类型表达式与请求携带Content-type
标头内媒体类型进行匹配。
/*** 映射配置的媒体类型表达式是否与请求的"Content-type"标头匹配,* 并返回一个保证仅包含匹配表达式的实例. * 匹配通过MediaType.includes(MediaType)执行.* @param request 当前请求实例.* @return 如果条件不包含表达式,则为同一实例.* 如果表达式匹配成功,返回包含匹配表达式的新条件实例.* 如果没有表达式匹配,则为null.*/
@Override
@Nullable
public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) {
// 如果请求是有效的CORS类型请求,直接返回空的ConsumesRequestCondition.if (CorsUtils.isPreFlightRequest(request)) {
return PRE_FLIGHT_MATCH;}// 若映射配置媒体类型表达式为空,则返回当前实例.if (isEmpty()) {
return this;}MediaType contentType;try {
// 解析"Content-type"标头内的媒体类型.若无"Content-type"标头,则使用application/octet-stream.contentType = (StringUtils.hasLength(request.getContentType()) ?MediaType.parseMediaType(request.getContentType()) :MediaType.APPLICATION_OCTET_STREAM);}catch (InvalidMediaTypeException ex) {
return null;}Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<>(this.expressions);// 映射配置的媒体类型表达式与"Content-type"标头内媒体类型进行匹配.result.removeIf(expression -> !expression.match(contentType));return (!result.isEmpty() ? new ConsumesRequestCondition(result) : null);
}
??4) ConsumesRequestCondition.ConsumeMediaTypeExpression.match(...)
方法。
??① 映射配置媒体类型与请求携带Content-Type
标头内媒体类型进行匹配。
??② isNegated
表示是否使用了!
的否定操作,根据其对①
中的匹配结果进行转换操作。
/*** 映射配置的媒体类型表达式与"Content-type"标头内媒体类型进行匹配.*/
public final boolean match(MediaType contentType) {
boolean match = getMediaType().includes(contentType);// 是否使用!来表达否定操作.return (!isNegated() ? match : !match);
}
??5) MimeType.includes(...)
方法。
??请求携带Content-Type
标头内媒体类型以下简称请求发送媒体类型。
??① 若请求发送媒体类型为null
,则返回false
。
??② 映射配置媒体类型的主类型为通配符*
时,包含所有媒体类型,则返回true
。
??③ 当映射配置媒体类型的主类型与请求发送媒体类型的主类型一致时,对子类型进行如下判断:
??· 映射配置媒体类型的子类型与请求发送媒体类型的子类型一致时,返回true
。
??· 映射配置媒体类型的子类型为通配符*
或后缀通配符*+
时,若映射配置媒体类型的子类型为通配符*
,返回true
。若映射配置媒体类型的子类型为后缀通配符*+
,将映射配置媒体类型的子类型按照+
分解为两个部分A
和B
,将请求发送媒体类型的子类型自尾端截取B
的长度得到C
,B
与c
一致且A
为通配符*
时,返回true
。
/*** 指示此媒体类型是否包含给定的媒体类型.* 例如:text/*包含text/plain和text/html,application/*+xml包含application/soap+xml等.* @param other 要与之比较的引用媒体类型.* @return 如果当前媒体类型包含给定的媒体类型,返回true,否则返回false.*/
public boolean includes(@Nullable MimeType other) {
// 若传入媒体类型为null,则返回false.if (other == null) {
return false;}// 当前媒体类型是否为通配符*.if (isWildcardType()) {
// */* 包含所有类型.return true;}// 当前媒体类型与传入媒体类型 主类型相同的情况下.else if (getType().equals(other.getType())) {
// 当前媒体类型与传入媒体类型 子类型相同,则返回true.if (getSubtype().equals(other.getSubtype())) {
return true;}// 当前媒体类型子类型是否为通配符*// 或// 当前媒体类型子类型是否为后缀通配符*+.if (isWildcardSubtype()) {
// 当前媒体类型 子类型不包含+,表明是通配符*,返回true.int thisPlusIdx = getSubtype().lastIndexOf('+');if (thisPlusIdx == -1) {
return true;}// 当前媒体类型 子类型包含+,表明是后缀通配符*+.else {
// application/*+xml 包含 application/soap+xml.int otherPlusIdx = other.getSubtype().lastIndexOf('+');if (otherPlusIdx != -1) {
// 截取后缀通配符的后缀.String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx);// 截取后缀通配符的通配符.String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1);// 将传入媒体类型子类型进行截取,从后开始截取,截取长度为后缀通配符中后缀的长度.String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1);// 当前媒体类型与传入媒体类型截取后的子类型是否匹配.if (thisSubtypeSuffix.equals(otherSubtypeSuffix) && WILDCARD_TYPE.equals(thisSubtypeNoSuffix)) {
return true;}}}}}return false;
}
??总结
???@RequestMapping
的consumers
匹配重点在于媒体类型的匹配过程,详细的了解媒体类型匹配规则,有助于在实际开发中更精确的控制映射配置。
??源码解析基于spring-framework-5.0.5.RELEASE
版本源码。
??若文中存在错误和不足,欢迎指正!