Spring源码之请求路径匹配路由方式

 更新时间:2021年09月08日 11:21:52   作者:lz710117239  
这篇文章主要介绍了Spring源码之请求路径匹配路由方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

请求路径匹配路由

在spring中,当一个请求过来的时候会做路径匹配,下面我们就从源码层面分析一下路径匹配。

示例:

@RequestMapping(value = "/user/{aid}/online/**", method = RequestMethod.GET)

我们一起看看这个方法是如何寻找的,和一些相应的工具类

入口

我的项目使用的是自动配置的RequestMappingHandlerMapping类,在getHandlerInternal()方法中:

HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);

上面这行是根据你的请求path和request去查找合适的method了。在项目启动的时候,Spring就把路径和对应的方法加载到了内存中。

进入上面方法

  List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
  if (directPathMatches != null) {
   addMatchingMappings(directPathMatches, matches, request);
  }
  if (matches.isEmpty()) {
   // No choice but to go through all mappings...
   addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
  }

可以看到如果根据lookupPath直接匹配上了,走第一个方法,如果没有,则需要根据规则匹配,走第二个方法。

mappingRegistry.getMappings().keySer()这个方法获取的类型为RequestMappingInfo类型,后面进入了RequestMappingInfo的getMatchingCondition()方法:

 public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
  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;
  }

可以看到代码里面会查看各种条件是否匹配,包括,请求方法methods,参数params,请求头headers,还出入参类型等相关的consumers,produces等,最后一行就是我们要找的路径匹配patternsCondition.getMatchingCondition(request)。

这个方法会走到PatternRequestCondition的getMatchingPattern方法,然后调用如下方法,获取pattern:

  if (this.pathMatcher.match(pattern, lookupPath)) {
   return pattern;
  }

上面这个pathMatcher的类型就是AntPathMatcher类,就是通过调用AntPathMatcher类的match方法,查看是否匹配,然后返回pattern。

SpringMVC 将请求找到匹配的处理

在SpringMVC的模式下,浏览器的一个请求是如何映射到指定的controller的呢?

初始化映射关系

在web服务器启动时,Spring容器中会保存一个map的数据结构,里边记录这controller和url请求中的对应关系。那么这个map中的数据是如何来的呢?

首先来看AbstractHandlerMethodMapping的initHandlerMethods方法(至于为什么直接找到这个方法,我也是网上搜索的,之前的调用链没去纠结)

protected void initHandlerMethods() {
 if (logger.isDebugEnabled()) {
   logger.debug("Looking for request mappings in application context: " + getApplicationContext());
  }
 
        //获取Spring容器装配的所有bean的名称
 String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
    BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
    getApplicationContext().getBeanNamesForType(Object.class));
 
           //遍历
 for (String beanName : beanNames) {
                //判断该bean是否有@controller或者@RequestMapping注解
  if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX) &&
    isHandler(getApplicationContext().getType(beanName))){
                        //如果有上述注解,则需要保存对应关系
   detectHandlerMethods(beanName);
  }
 }
 handlerMethodsInitialized(getHandlerMethods());
}
protected void detectHandlerMethods(final Object handler) {
        //获取传过来handler的类信息
 Class<?> handlerType =
   (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass());
 
 // Avoid repeated calls to getMappingForMethod which would rebuild RequestMappingInfo instances
        //初始化一个保存映射信息的map
 final Map<Method, T> mappings = new IdentityHashMap<Method, T>();
 final Class<?> userType = ClassUtils.getUserClass(handlerType);
 
 Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
  @Override
  public boolean matches(Method method) {
                //获取该类里所有方法的映射信息 T为RequestMappingInfo
                //mapping值的形式为{[/test/test1],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}
   T mapping = getMappingForMethod(method, userType);
   if (mapping != null) {
                                //将信息加入map
    mappings.put(method, mapping);
    return true;
   }
   else {
    return false;
   }
  }
 });
 
 for (Method method : methods) {
                //注册HandlerMethod,在里边进行一些重复验证等
  registerHandlerMethod(handler, method, mappings.get(method));
 }
}

上述方法中调用了一个比较重要的方法,getMappingForMethod,通过这个方法生成后续我们一直会用到的一个RequestMappingInfo对象。具体方法如下:

@Override
//该方法接收两个参数,一个是具体方法,一个是方法所在的类
 protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
  RequestMappingInfo info = null;
//找到方法的@RequestMapping注解
  RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
  if (methodAnnotation != null) {
//这个方法返回null
   RequestCondition<?> methodCondition = getCustomMethodCondition(method);
//创建RequestMappingInfo对象
   info = createRequestMappingInfo(methodAnnotation, methodCondition);
//找到类的@RequestMapping注解
   RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
   if (typeAnnotation != null) {
//该方法也返回一个null
    RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
//如果类和方法都有@RequestMapping注解,则进行combine操作
    info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
   }
  }
  return info;
 }

那么上述方法中调用的createRequestMappingInfo方法有事如何真正的创建出一个RequestMappingInfo对象的呢?

protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
//拿到@RequestMapping注解上的value值
  String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
//创建一个RequestMappingInfo,参数为一堆condition,出了PatternsRequestCondition,其余全部使用@RequestMapping注解上的值
  return new RequestMappingInfo(
    annotation.name(),
    new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
      this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
    new RequestMethodsRequestCondition(annotation.method()),
    new ParamsRequestCondition(annotation.params()),
    new HeadersRequestCondition(annotation.headers()),
    new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
    new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
    customCondition);
 }
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
//handler此处为带有@controller或者@RequestMapping的类的名称
//初始化一个HandlerMethod,包含一些类的名称和方法等信息
  HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
  HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
//判断是否有handlerMethods是否有重复数据,有则抛异常,没有则将其加入handlerMethods map中
  if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
   throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() +
     "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
     oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
  }
 
  this.handlerMethods.put(mapping, newHandlerMethod);
  if (logger.isInfoEnabled()) {
   logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
  }
 
//将没有*号和问好的pattern加入到urlMap中
  Set<String> patterns = getMappingPathPatterns(mapping);
  for (String pattern : patterns) {
   if (!getPathMatcher().isPattern(pattern)) {
    this.urlMap.add(pattern, mapping);
   }
  }
 
//维护一个nameMap,key为名字,格式为congroller类大写字母+#+方法名
//比如TestBank类的getBank方法,可以为TB#getBank
  if (this.namingStrategy != null) {
   String name = this.namingStrategy.getName(newHandlerMethod, mapping);
   updateNameMap(name, newHandlerMethod);
  }
 }

由上述registerHandlerMethod方法我们可以看出,该方法共维护了三个map分别是:

  • handlermethods: key为RequestMappingInfo value为HandlerMethod
  • urlMap: key为没有*和?的pattern(比如/test/test1)value为RequestMappingInfo
  • nameMap: key为名字,格式为congroller类大写字母+#+方法名,比如TestBank类的getBank方法,key为TB#getBank

上述三个map在后续匹配浏览器请求用哪个方法来处理时会重点用到。

从映射关系中寻找匹配方法

那么DispatcherServlet是如何处理一个请求的呢?

我们从DispatcherServlet的doService方法来看起,该方法中,最终会调用到AbstractHandlerMethodMapping类的lookupHandlerMethod方法来确定这个请求应该由哪个方法处理,代码如下:

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  List<Match> matches = new ArrayList<Match>();
//从urlMap中寻找能匹配的处理方法
  List<T> directPathMatches = this.urlMap.get(lookupPath);
//如果从urlMap中找到匹配的处理方法,则调用addMatchingMappings方法,将匹配的方法放入matches集合
  if (directPathMatches != null) {
   addMatchingMappings(directPathMatches, matches, request);
  }
//如果urlMap中没有找到直接匹配的方法
  if (matches.isEmpty()) {
   // No choice but to go through all mappings...
   addMatchingMappings(this.handlerMethods.keySet(), matches, request);
  }
 
  if (!matches.isEmpty()) {
//如果找到了匹配的方法,先获取一个比较器
   Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
//将匹配到的方法按照比较器排序
   Collections.sort(matches, comparator);
   if (logger.isTraceEnabled()) {
    logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
   }
//如果成功匹配的方法只有一个,拿这个方法返回。如果匹配到多个方法,取最匹配的前两个进行比较。
//如果比较结果为0,则抛出没有找到唯一合适处理方法的异常
   Match bestMatch = matches.get(0);
   if (matches.size() > 1) {
    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 + "}");
    }
   }
   handleMatch(bestMatch.mapping, lookupPath, request);
   return bestMatch.handlerMethod;
  }
  else {
//没有找到匹配的则返回null
   return handleNoMatch(handlerMethods.keySet(), lookupPath, request);
  }
 }

从上述代码可以看出,程序会先从this.urlMap中寻找是否有匹配的方法,那么这个urlMap中的数据是从什么时候加载的呢?我们网上翻看registerHandlerMethod方法,在web服务器启动时,该方法初始化了urlMap中的数据。

通过上述分析,大致可以了解到Spring容器是如何维护url和方法之间的映射关系,以及当收到请求时又是如何将请求匹配到正确的方法的。

至于没有分析到的当类和方法都有@RequestMapping注解时触发的combine操作究竟做了什么,当找到多个匹配方法是又是如何通过比较器进行排序的,我们下次再分析。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • springboot使用dubbo和zookeeper代码实例

    springboot使用dubbo和zookeeper代码实例

    这篇文章主要介绍了springboot使用dubbo和zookeeper代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • IDEA中的.iml文件和.idea文件夹

    IDEA中的.iml文件和.idea文件夹

    这篇文章主要介绍了IDEA中的.iml文件和.idea文件夹,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • profiles.active多环境开发、测试、部署过程

    profiles.active多环境开发、测试、部署过程

    这篇文章主要介绍了profiles.active多环境开发、测试、部署,主要讲如何使用profiles.active这个变量,让我们在开发过程快速切换环境配置,以及如何使一个部署适配各种不同的环境,需要的朋友可以参考下
    2023-03-03
  • JVM原理之类加载的全过程

    JVM原理之类加载的全过程

    文章详细介绍了Java类加载过程,包括加载、链接、初始化、使用和卸载五个阶段,并解释了符号引用和直接引用的区别,以及类变量和实例变量的区别,此外,还介绍了Class.forName()方法的作用和使用场景
    2025-01-01
  • SpringBoot过滤器的使用

    SpringBoot过滤器的使用

    过滤器是对数据进行过滤,预处理过程,当我们访问网站时,有时候会发布一些敏感信息,发完以后有的会用*替代,还有就是登陆权限控制等,一个资源,没有经过授权,肯定是不能让用户随便访问的,这个时候,也可以用到过滤器,需要的朋友可以参考一下
    2021-11-11
  • springboot+maven多环境动态配置及编译失败的解决方案(步骤详解)

    springboot+maven多环境动态配置及编译失败的解决方案(步骤详解)

    这篇文章主要介绍了springboot+maven多环境动态配置及编译失败的解决方案,本文通过实例图文相结合给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-11-11
  • 高内聚低耦合法则实例解析

    高内聚低耦合法则实例解析

    这篇文章主要介绍了高内聚低耦合法则实例代码解析,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • springboot接入deepseek深度求索代码示例(java版)

    springboot接入deepseek深度求索代码示例(java版)

    这篇文章主要介绍了springboot接入deepseek深度求索的相关资料,包括创建APIKey,封装询问工具方法,传入问题,调用方法,但发现只能回答简单问题,需要的朋友可以参考下
    2025-01-01
  • java异步执行代码处理方法(先返回结果,后执行代码)

    java异步执行代码处理方法(先返回结果,后执行代码)

    这篇文章主要给大家介绍了关于java异步执行代码处理方法的相关资料,先返回结果,后执行代码,文中通过实例代码介绍的非常详细,对大家的学习或者工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-07-07
  • 详解Spring Boot 2.0.2+Ajax解决跨域请求的问题

    详解Spring Boot 2.0.2+Ajax解决跨域请求的问题

    这篇文章主要介绍了详解Spring Boot 2.0.2+Ajax解决跨域请求的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03

最新评论