2024-12-16  阅读(71)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/2071337585

回答

以注解为例。

Spring 容器在启动时会扫描 @Controller@RestController,然后解析类和方法上面的注解,将路径和处理器方法(handler method)之间的映射关系存储起来。当请求到达时,我们只需要根据 request 从注册表中匹配对应的 handler method 即可。

详解

原理很简单,其实内部实现并不简单。我们直接看源码。

在面试题 Spring MVC 有哪些核心组件?中讲到,DispatcherServlet 中的 doDispatch() 是处理请求的核心逻辑,其中的 getHandler() 就是根据 request 获取对应的 Handler:

  protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
      for (HandlerMapping mapping : this.handlerMappings) {
        HandlerExecutionChain handler = mapping.getHandler(request);
        if (handler != null) {
          return handler;
        }
      }
    }
    return null;
  }

HandlerMapping 是来将请求 URL 映射到具体的 Handler 的核心组件,它内部包含了 url 和 Controller&Method 的映射关系。Spring MVC 内置了多种实现:

  • BeanNameUrlHandlerMapping:根据 Bean 的名称来匹配 URL。
  • ControllerClassNameHandlerMapping:已废弃。
  • RequestMappingHandlerMapping:Spring MVC 默认使用的 HandlerMapping 实现。通过注解 @RequestMapping 或其他请求映射注解(如 @GetMapping, @PostMapping)来映射处理器。
  • SimpleUrlHandlerMapping:使用配置文件中的 URL 映射,例如通过 bean 的配置来手动指定 URL 和 Handler 的对应关系。
  • DefaultAnnotationHandlerMapping:已废弃。
  • RouterFunctionMapping:使用函数式风格定义路由。Spring 5 引入,用于响应式 Web 应用(WebFlux)。
  • SimpleServletHandlerAdapter:辅助映射 Servlet 类型的 Bean。一般用于兼容传统的 Servlet 组件。

大明哥的应用中主要有四种,如下:

我们目前主要是使用 RequestMappingHandlerMapping,所以我们直接看它的 getHandler()

  public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    // 省略很多代码....
  }

调用 getHandlerInternal() ,该方法由子类实现,我们直接看 RequestMappingInfoHandlerMapping 就可以了:

  protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    try {
      return super.getHandlerInternal(request);
    }
    finally {
      ProducesRequestCondition.clearMediaTypesAttribute(request);
    }
  }

委托给父类的 getHandlerInternal()

  protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();
    try {
      HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
      return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
      this.mappingRegistry.releaseReadLock();
    }
  }

首先调用 initLookupPath() 根据 request 获取 lookupPath ,其实就是提取出 url,比如我们前端请求的是 http://localhost:9901/mianshi/baodian/detail/1077120523 ,则 lookupPath就是 /mianshi/baodian/detail/1077120523

提取 lookupPath 后,调用 lookupHandlerMethod() 获取HandlerMethod :

  protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    
    // 根据 lookupPath 获取所有的 RequestMappingInfo
    List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
      // 通过条件进行筛选,并把结果放到matches里
      addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
      // 如果还为空,则兜底遍历所有接口
      addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
    }
    if (!matches.isEmpty()) {
      Match bestMatch = matches.get(0);
      if (matches.size() > 1) {
        /*
         * 如果有多个,则进行优先级排序,寻找最优的
         */
        Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
        matches.sort(comparator);
        bestMatch = matches.get(0);
        if (logger.isTraceEnabled()) {
          logger.trace(matches.size() + " matching mappings: " + matches);
        }
        if (CorsUtils.isPreFlightRequest(request)) {
          for (Match match : matches) {
            if (match.hasCorsConfig()) {
              return PREFLIGHT_AMBIGUOUS_MATCH;
            }
          }
        }
        else {
          Match secondBestMatch = matches.get(1);
          if (comparator.compare(bestMatch, secondBestMatch) == 0) {
            Method m1 = bestMatch.getHandlerMethod().getMethod();
            Method m2 = secondBestMatch.getHandlerMethod().getMethod();
            String uri = request.getRequestURI();
            throw new IllegalStateException(
                "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
          }
        }
      }
      request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
      handleMatch(bestMatch.mapping, lookupPath, request);
      return bestMatch.getHandlerMethod();
    }
    else {
      // 没有找到
      return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
    }
  }

this.mappingRegistry 是 Spring MVC 中的注册表,用于存储和管理各种请求 URL(或其他匹配条件)到处理方法(handler method)之间的映射关系。它内部有四个映射关系:

private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();

private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
  • registry:用于将某种类型 T(可能是请求路径或其他匹配条件)与 MappingRegistration<T> 关联。MappingRegistration<T> 是一个包含注册信息的对象,可能包括 URL、HTTP 方法、处理方法、优先级等信息。当我们注册一个 @RequestMapping 注解时,Spring MVC 会将请求路径及其相关属性存储在这个 registry 中。
  • pathLookup:存储了每个请求路径(作为 String)与多个映射目标 T 之间的关系。MultiValueMap 允许每个请求路径关联多个映射目标。
  • nameLookup:存储了每个方法名称(String)与多个处理方法(HandlerMethod)的对应关系。通常是根据方法的名称或标识来查找所有匹配的处理方法。
  • corsLookup:用于存储每个处理方法(HandlerMethod)的 CORS 配置信息(CorsConfiguration)。

首先调用 getMappingsByDirectPath()pathLookup 注册表中获取:

    public List<T> getMappingsByDirectPath(String urlPath) {
      return this.pathLookup.get(urlPath);
    }

大明哥个人网站 pathLookup 注册表内容如下:

我们请求的 urlPath 为 /mianshi/baodian/detail/1077120523 ,所以这个注册表肯定取不到。则调用 addMatchingMappings()registry 注册表中获取:

  private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
    for (T mapping : mappings) {
      T match = getMatchingMapping(mapping, request);
      if (match != null) {
        matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
      }
    }
  }

这里是迭代 registry 注册表中的元素,一次调用 getMatchingMapping() 进行匹配:

  public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
    
    // 请求方法 requestMapping 是否匹配
    RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
    if (methods == null) {
      return null;
    }
    
    // 请求参数是否匹配
    ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
    if (params == null) {
      return null;
    }
    
    // header 是否匹配
    HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
    if (headers == null) {
      return null;
    }
    
    //consums Content-Type 是否匹配
    ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
    if (consumes == null) {
      return null;
    }
    
    // produces Content-Type 是否匹配
    ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
    if (produces == null) {
      return null;
    }
    
    // 匹配 directPath 和通配符的请求映射匹配关系
    PathPatternsRequestCondition pathPatterns = null;
    if (this.pathPatternsCondition != null) {
      pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
      if (pathPatterns == null) {
        return null;
      }
    }
    
    // 匹配 ant 风格的请求
    PatternsRequestCondition patterns = null;
    if (this.patternsCondition != null) {
      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, pathPatterns, patterns,
        methods, params, headers, consumes, produces, custom, this.options);
  }

我们看 registry 注册表中的内容:

经过 getMatchingCondition() 对比我们就可以找到上图的 RequestMappingInfo。但是我们有可能会找到多个,则先进行排序,然后获取第 1 、2 个,对比两个优先级是否是一样的,如果一样则抛出异常,否则获取第一个。对比条件:

  public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
    int result;
    // Automatic vs explicit HTTP HEAD mapping
    if (HttpMethod.HEAD.matches(request.getMethod())) {
      result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
      if (result != 0) {
        return result;
      }
    }
    result = getActivePatternsCondition().compareTo(other.getActivePatternsCondition(), request);
    if (result != 0) {
      return result;
    }
    result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
    if (result != 0) {
      return result;
    }
    result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
    if (result != 0) {
      return result;
    }
    result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
    if (result != 0) {
      return result;
    }
    result = this.producesCondition.compareTo(other.getProducesCondition(), request);
    if (result != 0) {
      return result;
    }
    // Implicit (no method) vs explicit HTTP method mappings
    result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
    if (result != 0) {
      return result;
    }
    result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
    if (result != 0) {
      return result;
    }
    return 0;
  }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文