当前位置: 代码迷 >> 综合 >> Spring 注解面面通 之 @RequestMapping method 条件匹配源码解析
  详细解决方案

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

热度:79   发布时间:2024-01-17 00:58:55.0

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

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

??AbstractHandlerMethodMapping.lookupHandlerMethod(...)方法负责查找请求最佳匹配的处理方法。RequestMethodsRequestCondition类负责基于method的匹配过程。

??源码解析

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

??① 当请求是有效的CORS类型请求时,若当前映射method匹配条件为空,则直接返回当前映射。若当前映射method匹配条件不为空时,将Access-Control-Request-Method与当前映射method匹配条件进行比较。

??② 当前映射无method匹配条件时,若请求方法是OPTIONS且请求转发无错误,则返回null,即表示条件匹配不通过。若请求方法非OPTIONS且请求转发类型为ERROR,则可匹配所有Http方法,返回当前映射实例。

??③ 当前映射存在method匹配条件时,通过给定Http方法字符串,取得RequestMethodsRequestCondition对象。

/*** 根据当前映射method匹配条件和请求,取得RequestMethodsRequestCondition实例.* @param request 当前请求实例.* @return 如果method匹配条件为空,Http OPTIONS方法不进行匹配,返回null,其他方法均可处理.* 如果method匹配条件不为空,则进行具体Http方法的匹配.*/
@Override
@Nullable
public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) {
    // 如果请求是有效的CORS类型请求,进行特殊处理.if (CorsUtils.isPreFlightRequest(request)) {
    return matchPreFlight(request);}// 若当前映射无method匹配条件,则进行特殊处理.if (getMethods().isEmpty()) {
    // 若请求方法是OPTIONS,且请求转发类型为ERROR,则返回null.if (RequestMethod.OPTIONS.name().equals(request.getMethod()) &&!DispatcherType.ERROR.equals(request.getDispatcherType())) {
    return null; }// 若是非OPTIONS方法,则返回当前条件对象.return this;}// 当前映射已设置method匹配条件,进行对请求进行匹配处理.return matchRequestMethod(request.getMethod());
}

??4) RequestMethodsRequestCondition.matchRequestMethod(...)方法。

??① 通过传入的字符串格式Http方法取得HttpMethod对象。

??② 若HttpMethod对象为null,返回null,即表示条件匹配不通过。

??③ 若HttpMethod对象不为null,遍历当前映射method列表,与传入的Http方法进行匹配。若匹配成功,返回new RequestMethodsRequestCondition(method)实例。

??④ 当无匹配结果时,若传入的Http方法是HEAD且当前映射method列表包含GET方法,则返回GET_CONDITION,即new RequestMethodsRequestCondition(RequestMethod.GET)

/*** 通过给定Http方法字符串,取得RequestMethodsRequestCondition对象.*/
@Nullable
private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) {
    // 通过Http方法字符串取得HttpMethod对象.HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue);if (httpMethod != null) {
    // getMethods()会返回当前映射支持的所有Http方法.// 比较当前映射是否支持给定的Http方法.// 遍历当前映射method匹配条件,与Http方法字符串进行匹配.for (RequestMethod method : getMethods()) {
    if (httpMethod.matches(method.name())) {
    // 若存在匹配成功的值,新建RequestMethodsRequestCondition,并返回.return new RequestMethodsRequestCondition(method);}}// 若传入Http方法是HEAD时且当前映射method匹配条件包括GET方法,返回GET_CONDITION.if (httpMethod == HttpMethod.HEAD && getMethods().contains(RequestMethod.GET)) {
    return GET_CONDITION;}}return null;
}

??总结

??@RequestMappingmethod匹配过程不是很复杂,虽然逻辑简单,但还是存在细节需要特别注意:

??① 当请求是有效的CORS类型请求时,若当前映射method匹配条件为空,则直接返回当前映射。若当前映射method匹配条件不为空时,将Access-Control-Request-Method与当前映射method匹配条件进行比较。

??② 若方法由@RequestMapping注释,且未设置method属性,则可处理GETHEADPOSTPUTPATCHDELETETRACE,若Http方法是OPTIONS且请求转发类型为ERROR,不可处理OPTIONS,其他情况OPTIONS均可处理。

??③ 某些情况下,若传入的Http方法是HEAD且当前映射method列表包含GET方法,则可以当做GET方法来处理。

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

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

  相关解决方案