Spring 注解面面通 之 @RequestMapping produces 条件匹配源码解析

  @RequestMapping支持基于valuepathmethodparamsheadersconsumersproduces的匹配,本文对基于params的匹配过程进行分析。

  系列博文《Spring 注解面面通 之 @RequestMapping 请求匹配处理方法源码解析》中对请求匹配@RequestMapping注释方法流程进行了分析。

  AbstractHandlerMethodMapping.lookupHandlerMethod(...)方法负责查找请求最佳匹配的处理方法。ProducesRequestCondition类负责基于produces的匹配过程,可以配置多个标头条件,多个条件之间是逻辑或(||)的关系。

  源码解析

  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) ProducesRequestCondition.getMatchingCondition(...)方法。

  ① 如果请求是有效的CORS类型请求,则返回PRE_FLIGHT_MATCH,即new ProducesRequestCondition()

  ② 若映射配置媒体类型表达式为空,则返回当前实例,表示匹配通过。

  ③ 根据配置的内容协商策略ContentNegotiationStrategy解析前端可接收媒体类型。

  FixedContentNegotiationStrategy:从初始设置解析固定媒体类型。

  HeaderContentNegotiationStrategy:从请求携带Accept标头解析媒体类型。

  ParameterContentNegotiationStrategy:从查询参数解析媒体类型,默认参数名为format

​  PathExtensionContentNegotiationStrategy:从请求路径中的后缀名解析媒体类型。

  ServletPathExtensionContentNegotiationStrategyPathExtensionContentNegotiationStrategy的扩展,从ServletContext中解析媒体类型。

  ④ 映射配置媒体类型表达式与前端可接收媒体类型进行匹配。

/**
 * 映射配置的媒体类型表达式是否与前端可接收媒体类型匹配,
 * 	并返回一个保证仅包含匹配表达式的实例. 
 * 匹配通过MediaType.isCompatibleWith(MediaType)执行.
 * @param request 当前请求实例.
 * @return 如果条件不包含表达式,则为同一实例.
 * 	如果表达式匹配成功,返回包含匹配表达式的新条件实例.
 * 	如果没有表达式匹配,则为null.
 */
@Override
@Nullable
public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) {
    // 如果请求是有效的CORS类型请求,直接返回空的ProducesRequestCondition.
    if (CorsUtils.isPreFlightRequest(request)) {
        return PRE_FLIGHT_MATCH;
    }
    // 若映射配置媒体类型表达式为空,则返回当前实例.
    if (isEmpty()) {
        return this;
    }

    List<MediaType> acceptedMediaTypes;
    try {
        // 获取可接收的媒体类型.
        acceptedMediaTypes = getAcceptedMediaTypes(request);
    }
    catch (HttpMediaTypeException ex) {
        return null;
    }

    Set<ProduceMediaTypeExpression> result = new LinkedHashSet<>(this.expressions);
    // 根据映射配置媒体类型表达式与可接收媒体类型进行匹配.
    result.removeIf(expression -> !expression.match(acceptedMediaTypes));
    if (!result.isEmpty()) {
        return new ProducesRequestCondition(result, this.contentNegotiationManager);
    }
    else if (acceptedMediaTypes.contains(MediaType.ALL)) {
        return EMPTY_CONDITION;
    }
    else {
        return null;
    }
}

  4) ProducesRequestCondition.ProduceMediaTypeExpression.match(...)方法。

  ① 映射配置媒体类型与前端可接收媒体类型进行匹配。

  ② isNegated表示是否使用了!的否定操作,根据其对中的匹配结果进行转换操作。

/**
 * 根据映射配置媒体类型表达式与前端可接收媒体类型进行匹配.
 */
public final boolean match(List<MediaType> acceptedMediaTypes) {
    boolean match = matchMediaType(acceptedMediaTypes);
    // 是否使用!来表达否定操作.
    return (!isNegated() ? match : !match);
}

  5) MimeType.isCompatibleWith(...)方法。

​  ① 若前端可接收媒体类型为null,则返回false

​  ② 映射配置媒体类型的主类型或前端可接收媒体类型的主类型为通配符*时,则返回true

  ③ 当映射配置媒体类型的主类型与前端可接收媒体类型的主类型一致时,对子类型进行如下判断:

​  · 映射配置媒体类型的子类型与前端可接收媒体类型的子类型一致时,返回true

  · 映射配置媒体类型的子类型或前端可接收媒体类型的子类型为通配符*或后缀通配符*+或时,若两者均为通配符*,返回true。若两者均为后缀通配符*+,将映射配置媒体类型的子类型按照+分解为两个部分AB,将传入媒体类型的子类型按照+分解为两个部分CDBD一致且AC为通配符*时,返回true

  ④ MimeType.isCompatibleWith(...)的匹配是相互的,即比较的两者可以互为兼容。

/**
 * 指示此媒体类型是否与传入媒体类型兼容.
 * 例如: text/* 兼容 text/plain、text/html, 反之亦然. 
 * 此方法类似于includes.
 * @param other 要与之比较的引用媒体类型.
 * @return 如果当前媒体类型兼容给定的媒体类型,返回true,否则返回false.
 */
public boolean isCompatibleWith(@Nullable MimeType other) {
    // 若传入媒体类型为null,则返回false.
    if (other == null) {
        return false;
    }
    // 当前媒体类型或传入媒体类型是否为通配符*.
    if (isWildcardType() || other.isWildcardType()) {
        return true;
    }
    // 当前媒体类型与传入媒体类型 主类型相同的情况下.
    else if (getType().equals(other.getType())) {
        // 当前媒体类型与传入媒体类型 子类型相同,则返回true.
        if (getSubtype().equals(other.getSubtype())) {
            return true;
        }
        // 当前媒体类型或传入媒体类型子类型是否为通配符*
        // 或
        // 当前媒体类型或传入媒体类型子类型是否为后缀通配符*+.
        if (isWildcardSubtype() || other.isWildcardSubtype()) {
            int thisPlusIdx = getSubtype().lastIndexOf('+');
            int otherPlusIdx = other.getSubtype().lastIndexOf('+');
            if (thisPlusIdx == -1 && otherPlusIdx == -1) {
                return true;
            }
            // 将映射配置媒体类型子类型按照+分解为两个部分A和B,将传入媒体类型子类型按照+分解为两个部分C和D,
            // B和D一致且A或C为通配符*时,返回true.
            else if (thisPlusIdx != -1 && otherPlusIdx != -1) {
                String thisSubtypeNoSuffix = getSubtype().substring(0, thisPlusIdx);
                String otherSubtypeNoSuffix = other.getSubtype().substring(0, otherPlusIdx);
                String thisSubtypeSuffix = getSubtype().substring(thisPlusIdx + 1);
                String otherSubtypeSuffix = other.getSubtype().substring(otherPlusIdx + 1);
                if (thisSubtypeSuffix.equals(otherSubtypeSuffix) &&
                    (WILDCARD_TYPE.equals(thisSubtypeNoSuffix) || WILDCARD_TYPE.equals(otherSubtypeNoSuffix))) {
                    return true;
                }
            }
        }
    }
    return false;
}

  总结

  @RequestMappingproduces匹配重点在于媒体类型的匹配过程,详细的了解媒体类型匹配规则,有助于在实际开发中更精确的控制映射配置。

  源码解析基于spring-framework-5.0.5.RELEASE版本源码。

  若文中存在错误和不足,欢迎指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值