Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射)

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如下图所示:

筛选方法和择优方法.png

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)匹配。

  1. 源码: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;
}
  1. 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";
    }
根据url获取一组mapping信息.png

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 condition;
    //condition的条件是否匹配请求?
    public RequestConditionHolder getMatchingCondition(HttpServletRequest request) {
        if (this.condition == null) {
            return this;
        }
        RequestCondition match = (RequestCondition) this.condition.getMatchingCondition(request);
        return (match != null ? new RequestConditionHolder(match) : null);
    }
}
 
 

在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接口。

你可能感兴趣的:(Spring源码篇(2)—RequestMappingInfo与RequestCondition(Handler的映射))