SpringBoot2.x—定制HandlerMapping映射规则

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所示:


图1-设置请求头.png

可以精确的定位到Controller层的某个方法?


分析

在@RequestMapping中配置headers属性,也可以根据请求头来匹配controller的方法。但请求中参数必须和注解参数相同。不能实现我们场景中用户上送某个请求参数,都可以匹配到controller中的方法。

所以,我们需要自定义HandlerMapping的映射规则,来定制我们的业务。

重写HandlerMapping?

SpringMVC是通过RequestMappingHandlerMapping来完成请求到HandlerExecutionChain的映射的。我们要在映射过程中,加入我们自定义的映射逻辑,那么必须要重写RequestMappingHandlerMapping


1. 如何重写HandlerMapping

原理:RequestMappingHandlerMappingHandlerMapping实现类,根据请求来映射得到controller中带有@RequestMapping注解方法。实际上他会在项目启动时解析@RequestMapping注解,并且将注解的属性转换为RequestCondition以便和请求匹配。而SpringMVC给我们预留了获取自定义条件空实现方法。故我们需要重写方法,返回自定义的RequestCondition条件,那么该条件会影响请求的映射。

  1. 在含有@RequestMapping的方法/类上加上自定义注解,以便自定义RequestCondition得到参数。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {
    //该方法适配的版本号
    int[] value();
}
  1. 当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;
    }
}
  1. 该类继承了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;
    }
}
  1. 将自定义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经过筛选。

  1. 无@ApiVersion注解方法。
  2. 存在@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的处理规则是:

  1. @RequestMapping属性与请求匹配,但不存在自定义条件,那么也会和请求匹配;

  2. @RequestMapping属性优先级相同的请求下,若RequestMappingInfo的自定义条件为null,则优先级最低。

你可能感兴趣的:(SpringBoot2.x—定制HandlerMapping映射规则)