SpringMVC流程分析(四):SpringMVC中如何为一个请求选择合适的处理器

2023年 8月 1日 52.5k 0

本系列文章皆在分析SpringMVC的核心组件和工作原理,让你从SpringMVC浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC的工作原理.

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜

前言

在上一章SpringMVC流程分析(三):MultipartResolver组件——SpringMVC中处理上传请求的关键中,我们通过对doDispatch方法中checkMultipart方法分析由浅入深的分析了MultipartResolver组件。接下来,我们将继续沿着doDispatch的调用链进行深入分析,而getHandler背后的逻辑便是本文所讨论的内容。

image.png

下图展示了本系列文章重点分析的组件信息,其中 HandlerMapping是本文分析的重点。

image.png

getHandler的处理逻辑

getHandler的内部逻辑如下所示。首先,其会遍历全部的HanlderMapping信息,然后进行匹配,如果找到一个合适的处理器信息,则返回一个HandlerExecutionChain对象;如果未找到,则返回一个null对象。我们注意到此时会涉及到一个我们之前未曾接触过的HandlerMapping组件。

那这个HandlerMapping的结构关系是什么样的?其又有什么用?此外,我们在使用SpringMVC时我们并没有配置过HandlerMapping的相关组件信息,那SpringMVC默认又会在容器中添加那些HandlerMaping的实现类?

protected HandlerExecutionChain getHandler(HttpServletRequest request) 
                                                 throws Exception {
   if (this.handlerMappings != null) {
      // 遍历所有的HandlerMapping信息,寻找适配的处理器
      for (HandlerMapping mapping : this.handlerMappings) {
         // 根据请求寻找适合的处理器
         HandlerExecutionChain handler = mapping.getHandler(request);
         // 将适配的Hanlder进行返回,类型为HandlerExecutionChain
         if (handler != null) {
            return handler;
         }
      }
   }
   return null;
}

接下来,我们便带着这些问题进入到HanlderMapping的体系中,看看其在处理请求的过程中到底做了哪些工作。

(注:本文重点分析HandlerMappingHandlerExecutionChain的相关内容会在后续进行讨论)

走进HandlerMapping

在开始讨论HandlerMapping之前,我们还是通过一个餐厅点菜的例子来帮助大家理解HandlerMapping的功能。

当顾客来到一家餐厅吃饭,落座后向服务员点了几个餐厅的特色菜,服务员收到菜单后,随即便将菜谱分发给不同厨师进行烹饪,等烹调完毕后服务员将菜端到客人桌前。

在这一例子中,顾客点餐的这一动作可以看做是一个Http请求,而服务员则相当于一个DispatcherServlet。顾客点菜后,由服务将菜谱交给对应的厨师进行烹饪。而协助完成个这一动作的背后逻辑在于服务员的脑海中有一个记事本,其记录了厨师和菜系之间的对应关系,因此可以很快的将订单准确的交给相对应的厨师。

类似地,在SpringMVC中,HandlerMapping(处理器映射)便是用来充当记事本这一角色的一个组件,其主要用途在于将传入的HTTP请求映射到相应的处理器方法上。 具体而言,在SpringMVC中,DispatcherServlet需要处理分发很多请求,而每个请求通常对应一个特定的Handler来进行处理,而接收到一个请求后,具体使用哪个Handler来处理则需要通过HandlerMaping进行处理。

总的来看,HanlderMapping的主要工作就是根据前端传来的请求,然后找到合适的处理器。 这句话将贯穿全文,这是理解HandlerMapping作用的的关键。

HandlerMapping内部的方法

要想明白一个类的功能,首先关注便是其内部所具有方法。而HandlerMapping内部的方法信息如下所示。

public interface HandlerMapping {

    // ....省略一些其中的常量属性
   @Nullable
   HandlerExecutionChain getHandler(HttpServletRequest request) 
                                                   throws Exception;

}

对于HandlerMapping而言,其内部的getHandler方法就是通过request来获取一个HandlerExecutionChain

(注:HandlerExecutionChain相当于对处理器的一种封装,其内部会包含处理器handler和拦截器interceptors)。

HandlerMapping的体系结构

了解了类中的方法后,下一步便是观察其的体系结构。而HandlerMapping 接口的体系如下所示:

image.png

通过上图可以注意到,HanlderMapping为一个接口,其中提供的方法getHandler方法用于获取对应的处理器。这个继承关系虽然看着有点绕,其实仔细观察就两大类:

  • AbstractHandlerMethodMapping
  • AbstractUrlHandlerMapping

(注:MathableHandlerMapping 作为一个接口,主要用于判定请求url和定义路径是否匹配)

具体来看,AbstractHandlerMethodMapping 体系下的都是根据方法名进行匹配的,而 AbstractUrlHandlerMapping 体系下的都是根据 URL 路径进行匹配的。虽然两者在匹配url时会使用不同的方式,但是这两者有一个共同的父类 AbstractHandlerMapping

更进一步,在Spring MVC中,常用的HandlerMapping实现包括:

  • RequestMappingHandlerMapping: 基于@RequestMapping注解来匹配处理器方法。它通过扫描@Controller注解和@RequestMapping注解来建立请求与处理器方法之间的映射关系。
  • SimpleUrlHandlerMapping: 基于URL路径的匹配。通过配置URL与处理器方法的映射关系,可以将指定URL请求映射到相应的处理器方法上。
  • BeanNameUrlHandlerMapping: 基于Bean名称的匹配。它将请求的URL路径与容器中的Bean名称进行匹配,将匹配成功的请求映射到相应的处理器方法上。
  • 公共父类:AbstractHandlerMapping

    通过上述的类图信息,我们注意到AbstractHandlerMethodMapping AbstractUrlHandlerMapping全部继承自AbstractHandlerMapping这一抽象类。虽然AbstractHandlerMethodMapping AbstractUrlHandlerMapping会通过两种不同的方式来进行URL信息的匹配,但其内部一定会存在某些公共特征。而AbstractHandlerMapping的作用便是对这些共性特征进行处理。同时留有扩展方法,方便子类对处理逻辑进行增强和扩展。

    接下来,我们便深入AbstractHandlerMapping内部,看看其究竟定义了哪些公共逻辑,该类相关内容如下:

    public abstract class AbstractHandlerMapping 
                extends WebApplicationObjectSupport
                implements HandlerMapping, Ordered, BeanNameAware {
      
        // ......省略其他方法和成员变量
    
        private final List adaptedInterceptors = new ArrayList();
    
    
        /***
        * 根据reqeust信息获取对应处理器
        */
        @Override
        public final HandlerExecutionChain getHandler(HttpServletRequest request) 
                                                            throws Exception {
           //  获得处理器(HandlerMethod 或者 HandlerExecutionChain),该方法是抽象方法,由子类实现
           Object handler = getHandlerInternal(request);
         
         //... 省略非空判断
          
           // 如果找到的处理器是 String 类型,则从 Spring 容器中找到对应的 Bean 作为处理器
           if (handler instanceof String) {
              String handlerName = (String) handler;
              handler = obtainApplicationContext().getBean(handlerName);
           }
           
           // 构建一个HandlerExecutionChain (包含处理器和拦截器)
           HandlerExecutionChain executionChain = 
                                            getHandlerExecutionChain(handler, request);
    
           return executionChain;
        }
     
        /***
        * 创建 HandlerExecutionChain 对象(包含处理器和拦截器)
        */
        protected HandlerExecutionChain getHandlerExecutionChain(Object handler, 
                                                         HttpServletRequest request) {
           //  创建 HandlerExecutionChain 对象
           HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
                 (HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
          //  获得请求路径
           String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
           //  遍历 adaptedInterceptors 数组,获得请求匹配的拦截器
           for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
              // 若路径匹配,则添加到 chain 中
              if (interceptor instanceof MappedInterceptor) {
                 MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
                 if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
                    chain.addInterceptor(mappedInterceptor.getInterceptor());
                 }
              }+
              else {
                 chain.addInterceptor(interceptor);
              }
           }
           return chain;
        }
    
    

    上述代码反映了如下信息:

  • AbstractHandlerMapping内部 维护了HandlerInterceptor列表。首先,AbstractHandlerMapping内部维护了一个HandlerInterceptor列表,用于存储应用于该HandlerMapping的拦截器。这些拦截器将应用于匹配到的请求处理器方法。

  • AbstractHandlerMapping定义了默认的请求匹配逻辑。首先,AbstractHandlerMapping实现了HandlerMapping接口中的核心方法,包括getHandler()getHandlerExecutionChain()等方法。它提供了一些默认的请求匹配的整体流程。而不同匹配模式下URL的匹配逻辑则交由子类实现,所以要分析不同方法获取处理的方式,分析子类的getHandlerInternal即可。

  • 正如之前所说,SpringMVC内部提供了两种不同的路径适配规则,一种是基于@ReqeustMapping注解来匹配的方式,一种是通过URL路径规则匹配的方式。

    至此,本文已经对HandlerMapping已经有了一个大致的介绍。具体如下,首先,我们以doDispatch方法中的getHandler为切入点, 分析了getHandler的主要逻辑在于遍历HandlerMapping,然后返回一个
    HandlerExecutionChain(其内部会包含处理器handler和拦截器interceptors)。

    因此我们又一次将注意力从getHandler方法转移到HandlerMapping之上,分析了HandlerMapping中定义的方法、研究了类的体系结构。进一步,通过分析HandlerMapping的类结构关系,注意到其相关子类有一个公共父类,即AbstractHandlerMapping。更进一步,研究了AbstractHandlerMapping的相关逻辑,发现了不同匹配模式下URL的匹配逻辑则交由子类实现,所以要分析不同匹配方式的区别,重点在于分析子类的getHandlerInternal

    接下来,我们便继续深入探究两种匹配方式,对比分析这两种匹配方式在路径匹配上的区别。

    基于路径的匹配

    Spring MVC中,AbstractUrlHandlerMappingHandlerMapping接口的一个抽象实现,用于基于URL路径进行请求处理器映射。

    在开始分析路径匹配方式之前,不妨先考虑一个问题,如果让我们来设计实现一个基于路径的匹配寻找处理器的对象我们该如何进行设计? 通过前面分析,我觉得应该大致从如下几个方面进行考虑:

  • 该类应该可以解析请求路径信息。 具体而言,当收到请求时该类会解析请求中的URL路径信息。
  • 该类应该可以将路径信息同处理器进行匹配的功能。 只有对象内部存储了URL与处理器的映射关系,当进行适配时,才能快速将请求的URL路径映射到对应的处理器方法上。
  • 该类可以提供URL路径匹配的功能。 其可以通过读取Http中的请求路径信息,并将请求的URL路径与处理器进行匹配。从而找到最适合处理当前URL请求的处理器。
  • 该类可以获取处理器方法。 根据请求获取处理器这是HandlerMapping接口所规定的功能。
  • 其中AbstractUrlHandlerMapping的代码如下:

    public class AbstractUrlHanlderMapping {
       
    
        public void initApplicationContext() 
                    throws ApplicationContextException {
           super.initApplicationContext();
           // 初始化处理器和url路径间的匹配
           detectHandlers();
        }
        
        
        //... 省略其他无关代码
        
        /**
        * 侦测容器的bean,获取url规则,进行url和处理器之间关系的映射处理
        * 通常子类会对这部分的逻辑进行重写
        */
        protected void detectHandlers() throws BeansException {
           //... 省略部分无关代码
           String[] beanNames =
                 applicationContext.getBeanNamesForType(Object.class));
    
           for (String beanName : beanNames) {
              String[] urls = determineUrlsForHandler(beanName);
              if (!ObjectUtils.isEmpty(urls)) {
                 // 完成url和处理器之间的映射关系处理
                 registerHandler(urls, beanName);
              }
           }
            //... 日志处理相关代码
        }
    
        //... 省略其他无关代码
       
        protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
            //  获得请求的路径
            String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
            //  根据请求信息,获取对应的Handler信息
            Object handler = lookupHandler(lookupPath, request);
            //  如果找不到处理器,则使用 rootHandler 或 defaultHandler 处理器
            if (handler == null) {
                Object rawHandler = null;
                //  如果是根路径,则使用 rootHandler 处理器
                if ("/".equals(lookupPath)) {
                    rawHandler = getRootHandler();
                }
                //  使用默认处理器
                if (rawHandler == null) {
                    rawHandler = getDefaultHandler();
                }
                if (rawHandler != null) {
                    // Bean name or resolved handler?
                    //  如果找到的处理器是 String 类型,则从容器中找到该 beanName 对应的 Bean 作为处理器
                    if (rawHandler instanceof String) {
                        String handlerName = (String) rawHandler;
                        rawHandler = obtainApplicationContext().getBean(handlerName);
                    }
                    //  空方法,校验处理器。目前暂无子类实现该方法
                    validateHandler(rawHandler, request);
                    //  创建处理器(HandlerExecutionChain 对象)
                    handler = buildPathExposingHandler(rawHandler, 
                                                       lookupPath, lookupPath, null);
                }
            }
            return handler;
        }
     }
    

    在上述逻辑中detectHandlers主要用于侦测容器中的处理器信息,并完成url和处理器之间的映射关系。而对于getHandlerInternal方法而则主要定义了根据url获取处理器的处理逻辑,其大致内容如下:

  • 获得请求路径
  • 调用 lookupHandler(String urlPath, HttpServletRequest request) 方法,获得处理器。
  • 如果找不到处理器,则使用 rootHandlerdefaultHandler 处理器
  • 根据路径规则进行不同适配
  • 如果是/根路径,则使用 rootHandler 处理器;反之,则使用默认处理器
  • 如果找到的处理器是 String 类型,则从容器中找到该 beanName 对应的 Bean 作为处理器
  • 调用validateHandler(Object handler, HttpServletRequest request) ,对处理器进行校验,空方法,暂无子类实现该方法
  • 调用 buildPathExposingHandler方法,创建 HandlerExecutionChain 处理器执行链,赋值给handler处理器,详情见下文
  • 返回请求对应的handler处理器
  • 通过上述分析可以知道,AbstractUrlHandlerMapping内部的getHandlerInternal定义了获取处理器的逻辑;而在initApplicationContextdetectHandlers方法则完成了url路径和处理器间的映射关系。这也符合当初我们对于AbstractUrlHanlderMapping功能的设计。

    此外,如果你接触 Spring MVC 较早,可能见过 SimpleUrlHandlerMappingBeanNameUrlHandlerMapping 中的使用示例的配置方式。当然,目前这种方式已经基本不用了,被 @RequestMapping 等注解的方式所取代。

    基于方法的匹配

    Springmvc提供了多种配置方式,除了通过url匹配对应处理器,还可以通过url匹配对应的方法来完成相应的处理逻辑。使用的注解信息如下所示:

    进一步,AbstractHandlerMethodMappingHandlerMapping接口的抽象实现之一,用于基于处理器方法的映射。进而确定应该由哪个处理器方法来处理该请求。相关代码如下:

    public abstract class AbstractHandlerMethodMapping extends
        AbstractHandlerMapping implements InitializingBean {
        /**
        * 是否只扫描可访问的 HandlerMethod 们
        */
        private boolean detectHandlerMethodsInAncestorContexts = false;
        /**
        * Mapping 命名策略
        */
        @Nullable
        private HandlerMethodMappingNamingStrategy namingStrategy;
        /**
        * Mapping 注册表,此为AbstractHandlerMethodMapping的一个内部类信息
        */
        private final MappingRegistry mappingRegistry = new MappingRegistry();
        
        /**
        * 获取处理器
        */
        @Override
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        //  获得请求的路径
        String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
          
        //  获得 HandlerMethod 对象
        // lookupHandlerMethod根据url请求适配合适的hanlder
       HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, 
                                                                request);
       //  进一步,获得一个新的 HandlerMethod 对象
       // 相当于从ioc容器中获取相应的处理器信息
       return (handlerMethod != null ?
                    handlerMethod.createWithResolvedBean() : null);
      
       }
         
        /**
        * 完成url路径信息和处理的映射关系适配
        * 相关实现逻辑交给子类的getMappingForMethod完成
        */
        protected void detectHandlerMethods(Object handler) {
            //  获得 Bean 对应的 Class 对象
            Class handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass());
            if (handlerType != null) {
                //  获得真实的 Class 对象,因为 `handlerType` 可能是代理类
                Class userType = ClassUtils.getUserClass(handlerType);
                //  获得匹配的方法和对应的 Mapping 对象
                // 此处为一个lambda表达式:主要逻辑委托于getMappingForMethod
                // 然后创建该方法对应的 Mapping 对象,例如根据 @RequestMapping 注解创建 RequestMappingInfo 对象
                // 交给子类去进行实现
                Map methods = MethodIntrospector.selectMethods(userType,
                        (MethodIntrospector.MetadataLookup) method -> {
                            return getMappingForMethod(method, userType);
                           
    
                //  遍历方法,逐个注册 HandlerMethod
                methods.forEach((method, mapping) -> {
                    Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                    registerHandlerMethod(handler, invocableMethod, mapping);
                });
            }
        }
    
    
    }
    

    可以看到,在AbstractHandlerMethodMapping的内部,其所完成的任务和之前分析AbstractUrlHanlderMapping时的功能基本相似。大致逻辑也是先注册url和处理器的映射信息,然后在getHandlerInternal中定义获取处理的逻辑。所以通过url匹配对应处理器和还可以通过url匹配对应的方法在匹配逻辑上本身没太大区别。两者的最大区别在于url地址信息和处理器进行映射时的处理机制有所不同。

    具体而言,在SpringMVC中,RequestMappingHandlerMapping使用MappingRegistry来维护处理方法和URL映射之间的关系,并用RequestMappingInfo则是用于表示RequestMapping的信息的对象。

    进一步,RequestMappingInfo是一个用于封装@RequestMapping注解及其派生注解(如:@GetMapping、@PostMapping等)的类。它包含了所有与请求映射相关的信息,例如URL模式HTTP请求方法、请求参数、请求头等。

    更进一步,方法和URL映射之间的关系通过MappingRegistry来维护。当请求到达时,根据请求的URLHTTP方法,从保存的RequestMappingInfo对象中找到合适的处理方法。它维护了一个数据结构,将请求的URLHTTP方法作为键,与相应的RequestMappingInfo对象。

    简而言之,RequestMappingHandlerMapping中的MappingRegistry使用RequestMappingInfo对象来建立请求映射与处理方法之间的对应关系。当请求到达时,RequestMappingHandlerMapping会查询MappingRegistry,找到匹配的RequestMappingInfo对象,并根据其中的处理方法信息将请求分发到相应的方法进行处理。

    (注:更具体的内容可查看RequstMappingHandlerMapping中的相关定义)

    SpringMVC默认配置的处理器

    SpringMVC这类框架在使用过程中,通常会遵守一种约定优于配置的原则,即通过一系列约定和默认配置来减少开发人员需要手动进行配置的工作,从而提高开发效率和降低代码复杂性。

    所以SpringMVC在初始化过程中,程序内部会加载很多的默认配置信息,而HandlerMapping的相关信息便会根据其内部的默认配置信息来进行加载。

    (注:SpringMVC中组件的初始化会在initStrategies中完成,而HandlerMapping的相关初始化则会在依托于initHandlerMappings方法进行实现,其中调用链如下所示。)

    image.png

    其中,initHandlerMappings方法的逻辑如下所示。

    DispatcherServlet#initHandlerMappings

    private void initHandlerMappings(ApplicationContext context) {
      
     // .....省略大量无关代码
     
    // 加载默认的HandlerMapping信息
    if (this.handlerMappings == null) {
       // 通过getDefaultStrategies方法进行加载相关内容
       this.handlerMappings = getDefaultStrategies(context, 
                                   HandlerMapping.class);
    }
    
    // .....省略大量无关代码
    
    }
    

    可以注意到,在initHandlerMappings内部,其会将加载HandlerMapping的逻辑委托于getDefaultStrategies进行实现,具体代码如下:

    DispatcherServlet#getDefaultStrategies

    public class DiapatcherServlet {
        
        // 配置文件的地址信息
        private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
    
        // 初始化加载配置文件
        static {
           // 加载资源加载器
           ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, 
                                                            DispatcherServlet.class);
           // 加载默认配置文件DispatcherServlet.properties
           defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        
        // ... 省略大量无关代码
        
        protected  List getDefaultStrategies(ApplicationContext context, Class strategyInterface) {
    
           String key = strategyInterface.getName();
           // 从配置文件中根据相关key信息获取对应value
           String value = defaultStrategies.getProperty(key);
           // 获取类信息,利用反射机制中的forName进行实例化处理
           if (value != null) {
              String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
              List strategies = new ArrayList(classNames.length);
              for (String className : classNames) {
                 try {
                    Class clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                    Object strategy = createDefaultStrategy(context, clazz);
                    // 将HandlerMapping相关信息添加到集合strategies中
                    strategies.add((T) strategy);
                 }
    
              // 返回HandlerMappings构建的集合
              return strategies;
           }
          // ... 省略大量的try-catch逻辑
        }
    }
    

    getDefaultStrategies的逻辑无非就是从配置DispatcherServlet.properties文件加载HandlerMapping的相关默认配置,然后利用反射机制对这些HandlerMapping进行实例化,其中DispatcherServlet.properties有关HandlerMapping的配置内容如下所示:

    DispatcherServlet.properties (路径:org.springmvc.web.servlet)

    org.springframework.web.servlet.HandlerMapping=
       org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,
       org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,
       org.springframework.web.servlet.function.support.RouterFunctionMapping
    

    不难发现,在SpringMVC中默认会为我们配置BeanNameUrlHandlerMapping,RequestMappingHandlerMapping等两个关键的HandlerMapping组件,以满足我们日常开发中对于HandlerMapping的使用。

    如上就是SpringMVC中即使不配置HandlerMapping也可使使用HandlerMapping功能的原理,这一切主要都归功于SpringMVC内部为我们已经进行了大量的默认配置,从而可以让我们将关注点集中业务逻辑的开发。

    总结

    本文以doDispahtch方法中调用的getHandler为切入点,由浅入深的分析了HandlerMapping的组件的功能、类结构关系等内容。在此基础上,我们深入讨论了SpringMVC中的不同匹配方式之间的区别。虽然在本文的讨论中会涉及到很多类的名称,但不要慌,这些类的么名称并不重要,你只需要记住: " HanlderMapping的主要工作就是根据前端传来的请求,然后找到合适的处理器。无论类中的体系结构如何复杂,其核心一定是服务于这一目标。"

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论