JAVA && Spring && SpringBoot2.x — 学习目录
Spring源码篇(1)—RequestMappingHandlerMapping(Handler的注册)
Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射)
SpringBoot2.x—定制HandlerMapping映射规则
1. 测试:
1. 在两个方法上使用完全相同的@RequestMapping注解
@RequestMapping(value = "/testApi")
@ResponseBody
public String testAPIV1(HttpServletResponse response) {
System.out.println("请求进入...V1");
return "success-V1";
}
@RequestMapping(value = "/testApi")
@ResponseBody
public String testAPIV2(HttpServletResponse response) {
System.out.println("请求进入...V2");
return "success-V2";
}
效果:启动项目,出现异常:模棱两可的异常,映射的bean 方法已经存在。
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'systemController' method
public java.lang.String com.tellme.controller.SystemController.testAPIV1(javax.servlet.http.HttpServletResponse)
to { /testApi}: There is already 'systemController' bean method
2. @RequestMapping的value相同,但是其他参数(以headers为例)不同。
@RequestMapping(value = "/testApi",headers = {"Api-Version=0.1"})
@ResponseBody
public String testAPIV1(HttpServletResponse response) {
System.out.println("请求进入...V1");
return "success-V1";
}
@RequestMapping(value = "/testApi",headers = {"Api-Version=0.2"})
@ResponseBody
public String testAPIV2(HttpServletResponse response) {
System.out.println("请求进入...V2");
return "success-V2";
}
正常启动项目后,发起请求,请求头中携带Api-Version=0.2
调用V2版本。
结论:(正常情况下)同一个项目中,不能存在含有完全相同@RequestMapping注解的方法。
3. @RequestMapping中的value是通配符
@RequestMapping(value = {"/testA*"})
@ResponseBody
public String testAPIV1(HttpServletResponse response) {
System.out.println("请求进入...V1");
return "success-V1";
}
@RequestMapping(value = "/testAp*")
@ResponseBody
public String testAPIV2(HttpServletResponse response) {
System.out.println("请求进入...V2");
return "success-V2";
}
效果:当请求路径为http://localhost:8082/testAp1
时,调用的是V2版本,当请求路径为http://localhost:8082/testA1
是调用的是V1版本。
结论:即使一个请求可以获取到多个mapping对象时,依旧会返回一个最佳的HandlerMethod。
2. 根据请求映射Handler对象
源码:org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#lookupHandlerMethod
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List matches = new ArrayList<>();
//根据请求的url,在mappingRegistry中获取到mapping对象。
List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
//将获取到的mapping存放到List中
addMatchingMappings(directPathMatches, matches, request);
}
//根据请求的url,未在mappingRegistry映射出mapping对象
if (matches.isEmpty()) {
//遍历mappingRegistry所有的mapping对象。检查请求url是否匹配mapping的通配符路径。
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
//获取到多个符合条件的mapping对象
if (!matches.isEmpty()) {
//创建多个Match对象的比较器。
Comparator comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
//选择最优的Match对象
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);
//检查是否为最优的Match对象
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 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
//返回最优mapping的HandlerMethod对象。
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
HandlerMapping根据request映射出最优的HandlerMethod如下图所示:
2. RequestCondition接口以及作用
RequestCondition
顾名思义“请求条件”。完成的功能是合并mapping对象,匹配mapping对象,以及比较mapping对象。
源码:RequestCondition
/**
* RequestCondition的泛型是可以完成合并和比较方法。
*/
public interface RequestCondition {
/**
* (合并方法)与另外一个条件合并,具体逻辑由子类提供
* 该方法在Handler注册的时候被调用,作用就是将类上的@RequestMapping注解
* 和方法上的@RequestMapping注解属性进行合并。
*/
T combine(T other);
/**
* (筛选方法)检查http属性与给定的属性是否匹配
* 若不匹配返回null;
* 若匹配返回当前对象(this)
*/
@Nullable
T getMatchingCondition(HttpServletRequest request);
/**
* (择优方法)最终用来确定两个匹配条件谁更匹配。
* 需要使用other-this(降序排列)。后续会取第一位。
*/
int compareTo(T other, HttpServletRequest request);
2. RequestMappingInfo类
根据request筛选出最优的RequestMappingInfo
类。实际上是请求条件对一组url相同的@RequestMapping
标签的筛选。即路径(url)匹配,header匹配,method匹配,params匹配,consums(content-type)匹配,produces(accept)匹配。
- 源码:RequestMappingInfo的属性。
public final class RequestMappingInfo implements RequestCondition {
@Nullable
private final String name;
//请求路径条件(url合并,筛选url,择优url)
private final PatternsRequestCondition patternsCondition;
//请求方法条件
private final RequestMethodsRequestCondition methodsCondition;
//请求参数条件
private final ParamsRequestCondition paramsCondition;
//请求头条件
private final HeadersRequestCondition headersCondition;
//请求content-type条件
private final ConsumesRequestCondition consumesCondition;
//请求accept条件
private final ProducesRequestCondition producesCondition;
//自定义请求条件
private final RequestConditionHolder customConditionHolder;
}
- mapping属性与@RequestMapping注解的关系
@RequestMapping(value = {"/testAll/T1", "/testAll/T2"},
method = {RequestMethod.GET, RequestMethod.POST},
headers = {"xx=1", "yy=2"},
consumes = {"text/plain", "application/*"})
@ResponseBody
public String testT1() {
System.out.println("请求进入...T1/T2");
return "success-T1/T2";
}
1. 合并方法:请求条件进行合并
该方法是在项目启动时,handler注册时执行的。目的是将类级别的@RequestMapping注解和方法级别的@RequestMapping注解属性进行合并。
源码位置:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#getMappingForMethod
protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) {
//获取方法上的@RequestMapping注解并将其转换为RequestMappingInfo对象。
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//获取类上的@RequestMapping注解并将其转换为RequestMappingInfo对象。
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//注:typeinfo信息为this对象,而methodInfo信息为other对象。两个条件进行合并。
//*此处可以实现方法级别属性覆盖类级别的属性*
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
以ConsumesRequestCondition#combine
为例,我们可以看下他如何实现方法注解属性
覆盖类注解属性
。
@Override
public ConsumesRequestCondition combine(ConsumesRequestCondition other) {
return (!other.expressions.isEmpty() ? other : this);
}
2. 筛选方法:获取匹配的条件
在注册Handler时,将@RequestMapping
对象解析为RequestMappingInfo对象,之后每一个RequestMappingInfo
对象均和request进行匹配。
@RequestMapping
属性进行匹配时,匹配规则均是固定的。若要自定义匹配规则,需要重写RequestConditionHolder
匹配规则。
源码位置:org.springframework.web.servlet.mvc.method.RequestMappingInfo#getMatchingCondition
//检查http的request属性与给定的属性是否匹配
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
//method匹配,针对于预请求(Options),
//若是真实请求和mapping中method不匹配,返回null;
//否则返回RequestMethodsRequestCondition对象。
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());
}
public final class RequestConditionHolder extends AbstractRequestCondition {
@Nullable
private final RequestCondition
在Handler的注册时,RequestMappingHandlerMapping
在解析带有@RequestMapping
的方法/类时,会调用该扩展方法,默认返回null。用户可以在该方法中解析(自定义标签),返回condition属性,自定义匹配规则。
3. 择优方法:选择唯一的mapping
若是请求与多个mapping匹配,但是该接口只会返回一个HandlerMethod对象。那么就需要使用Comparator([肯派瑞特]
比较器)来降序排列。获取优先级最高的mapping对象。
Comparator返回值与升序和降序排列的规则中我们知道,默认情况下(o1(this)-o2)
会导致升序排列。那我们在方法中必须使用(o2-o1(this))
以完成降序排列。
源码:org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition#compareTo
@Override
public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) {
if (other.methods.size() != this.methods.size()) {
//o2-this降序排列,谁大谁在前面(优先级高)
return other.methods.size() - this.methods.size();
}
else if (this.methods.size() == 1) {
if (this.methods.contains(RequestMethod.HEAD) && other.methods.contains(RequestMethod.GET)) {
//返回-1,那么this在前面(优先级高),也就是HEAD优先级高于GET优先级
return -1;
}
else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) {
return 1;
}
}
return 0;
}
可以看到RequestMappingInfo对象实现了RequestCondition
接口。