JAVA && Spring && SpringBoot2.x — 学习目录
Spring源码篇(1)—RequestMappingHandlerMapping(Handler的注册)
Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射)
SpringBoot2.x—定制HandlerMapping映射规则
业务场景
场景:项目中有三个业务方法@RequestMapping
配置完全相同。
//用户在请求中上送版本1或2时,调用该方法
@ApiVersion({1, 2})
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIV1(HttpServletResponse response) {
System.out.println("请求进入...V1");
return "success-V1";
}
//用户在请求头中上送版本3,调用版本3的方法
@ApiVersion(3)
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIV3(HttpServletResponse response) {
System.out.println("请求进入...V3");
return "success-V3";
}
//用户不指定版本号,则调用最新版本的方法
@RequestMapping(value = {"/testApi"})
@ResponseBody
public String testAPIVX(HttpServletResponse response) {
System.out.println("请求进入...VX");
return "success-VX";
}
我们理想是用户在请求头中,上送不同的参数,如图1所示:
可以精确的定位到Controller层的某个方法?
分析
在@RequestMapping中配置headers属性,也可以根据请求头来匹配controller的方法。但请求中参数必须和注解参数相同。不能实现我们场景中用户上送某个请求参数,都可以匹配到controller中的方法。
所以,我们需要自定义HandlerMapping的映射规则,来定制我们的业务。
重写HandlerMapping?
SpringMVC是通过RequestMappingHandlerMapping
来完成请求到HandlerExecutionChain
的映射的。我们要在映射过程中,加入我们自定义的映射逻辑,那么必须要重写RequestMappingHandlerMapping
。
1. 如何重写HandlerMapping
原理:RequestMappingHandlerMapping
是HandlerMapping
实现类,根据请求来映射得到controller
中带有@RequestMapping
注解方法。实际上他会在项目启动时解析@RequestMapping
注解,并且将注解的属性转换为RequestCondition
以便和请求匹配。而SpringMVC
给我们预留了获取自定义条件空实现方法。故我们需要重写方法,返回自定义的RequestCondition
条件,那么该条件会影响请求的映射。
- 在含有
@RequestMapping
的方法/类上加上自定义注解,以便自定义RequestCondition
得到参数。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
//该方法适配的版本号
int[] value();
}
- 当SpringMVC解析带有
@RequestMapping
方法/类时会调用下面的方法。若方法/类上存在自定义注解@ApiVersion
则生成自定义RequestCondition
对象,以便影响请求映射得到mapping。若不存在自定义注解,则返回null。
public class CustomRequestMappingHandlerMapping2 extends RequestMappingHandlerMapping {
@Override
protected RequestCondition> getCustomTypeCondition(Class> handlerType) {
//读取方法上参数配置
return get(handlerType);
}
@Override
protected RequestCondition> getCustomMethodCondition(Method method) {
//读取方法上参数配置
return get(method);
}
//每一个@RequestMapping都会解析
private ApiVersionCondition get(AnnotatedElement element) {
ApiVersion apiVersion = AnnotatedElementUtils.findMergedAnnotation(element, ApiVersion.class);
//有些@RequestMapping方法上没有@ApiVersion注解,故我们返回null。
return apiVersion != null ?
new ApiVersionCondition(Arrays.stream(apiVersion.value()).boxed().toArray(Integer[]::new)) : null;
}
}
- 该类继承了
RequestCondition
接口,完成的功能:(1)类上自定义注解和方法自定义注解的属性合并。(2)若RequestMappingInfo对象中@RequestMapping
属性均和请求匹配时,再与请求进行匹配。(3)若请求与多个mapping的自定义RequestCondition匹配,自定义RequestCondition需要指定一个最优的mapping。
public class ApiVersionCondition extends AbstractRequestCondition {
//版本号数组
private final Set versions;
public ApiVersionCondition(Integer... versions) {
this(Arrays.asList(versions));
}
private ApiVersionCondition(Collection versions) {
//将版本号进行倒序排序
LinkedHashSet integers = new LinkedHashSet<>(versions);
integers.stream().sorted(Comparator.reverseOrder());
this.versions = Collections.unmodifiableSet(integers);
}
@Override
protected Collection> getContent() {
return this.versions;
}
@Override
protected String getToStringInfix() {
return "||";
}
//参数的合并
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
return other.versions == null ? this : other;
}
//mapping对象属性与请求进行匹配
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
//查看该条件是否和请求匹配
String header = request.getHeader("Api-Version");
//若存在条件,但用户并未上送版本号,则该mapping不匹配
if (StringUtils.isBlank(header)) {
return null;
}
//若请求和条件匹配,则放行
for (Integer version : versions) {
if (header.equals(version.toString())) {
return this;
}
}
return null;
}
/**
* mapping中apiVersion(1,2) 另一个mapping中(2,3)
* 此时用户请求版本为2,那么优先执行(2,3)的mapping
*/
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
Integer thisVersion = this.versions.stream().findFirst().orElse(-1);
Integer otherVersion = other.versions.stream().findFirst().orElse(-1);
return otherVersion - thisVersion;
}
}
- 将自定义HandlerMapping替换RequestMappingHandlerMapping。
@Component
public class CustomWebMvcRegistrations implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
CustomRequestMappingHandlerMapping2 handlerMapping=new CustomRequestMappingHandlerMapping2();
handlerMapping.setOrder(0);
return handlerMapping;
}
}
2. 创建ApiVersionCondition注意事项
定制HandlerMapping的原理实际上是使用了模板方法模式,父类已经定义好了算法骨架(即自定义Condition如何执行)。那么我们在创建ApiVersionCondition时,需要注意些什么呢?
2.1 类注解和方法注解的合并问题
@RequestMapping
为例可以在类和方法上使用,而自定义注解@ApiVersion(可以将其看做@RequestMapping的属性)
,也是可以在方法/类上配置,那么他们如何进行合并处理呢?
//类级别mapping与方法级别mapping进行合并
protected RequestMappingInfo getMappingForMethod(Method method, Class> handlerType) {
//读取method上的@RequestMapping注解。
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
//读取类上的@RequestMapping注解。
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
//类上的mapping对象去合并方法的mapping对象。
info = typeInfo.combine(info);
}
...
return info;
}
按照RequestMappingHandlerMapping的处理,是类级别的RequestMapping调用的合并方法,那么在ApiVersionCondition
中,若想方法覆盖类级别自定义注解属性,需要返回other对象。
2.2 无@ApiVersion时的匹配问题
每个RequestMappingInfo的属性条件都会和Request进行匹配。当controller的Handler方法不存在@ApiVersion
注解时,既不会执行我们自定义的映射逻辑。那么该mapping与请求的匹配规则又是如何的。
//源码:org.springframework.web.servlet.mvc.condition.RequestConditionHolder#getMatchingCondition
public RequestConditionHolder getMatchingCondition(HttpServletRequest request) {
//若自定义条件为null,则为匹配成功
if (this.condition == null) {
return this;
}
//调用自定义条件的匹配方法。
RequestCondition> match = (RequestCondition>) this.condition.getMatchingCondition(request);
//若返回this,则证明匹配成功。否则,匹配失败。
return (match != null ? new RequestConditionHolder(match) : null);
}
在源码中,我们可以看到,若是@RequestMapping
的条件都匹配的情况下,会调用getMatchingCondition
执行自定义匹配条件,若该条件我null,则直接返回成功。即该mapping匹配该请求。
在@RequestMapping相同的情况下,即无@ApiVersion注解的Mapping在任何情况下都会匹配。
场景:用户未上送版本号时,执行无自定义注解的方法。
方案:getMatchingCondition
中,若用户未上送版本号返回null。这样只有无@ApiVersion的方法才会经过筛选。
2.3 多个mapping时自定义条件比较
对于自定义映射规则来说,我们的筛选方法可能会得到多个Mapping对象。
例如用户请求头携带的参数是版本3
,会有两个Mapping经过筛选。
- 无@ApiVersion注解方法。
- 存在@APiVersion(3)的注解的方法。
//一个mapping的自定义条件不存在,那么另一个mapping优先级高
//源码:org.springframework.web.servlet.mvc.condition.RequestConditionHolder#compareTo
@Override
public int compareTo(RequestConditionHolder other, HttpServletRequest request) {
if (this.condition == null && other.condition == null) {
return 0;
}
else if (this.condition == null) {
return 1;
}
else if (other.condition == null) {
return -1;
}
else {
assertEqualConditionTypes(this.condition, other.condition);
//若两个Mapping的自定义条件均存在,执行自定义的比较方法。
return this.condition.compareTo(other.condition, request);
}
}
那么不存在@ApiVersion注解的Controller方法,在@RequestMapping配置相同的情况下,优先级最低。
总结:若是使用自定义映射规则,SpringMVC的处理规则是:
@RequestMapping
属性与请求匹配,但不存在自定义条件,那么也会和请求匹配;@RequestMapping
属性优先级相同的请求下,若RequestMappingInfo的自定义条件为null,则优先级最低。