SpringMVC流程分析(二):揭开DispatcherServlet的神秘面纱

2023年 7月 24日 78.8k 0

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

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

前言

在使用SpringMVC开发Web应用时,除了会在web.xml中配置之前讨论的ContextLoaderListener项以外(相关内容可参考:SpringMVC流程分析(一):从一行配置入手,搞懂web环境下Ioc容器的构建),还需要在配置一个Servlet,而这个Servlet正是本文所讨论的重点——DispatcherServlet.

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

image.png

Servlet的前世今生

随着Web开发工具的不断迭代, Servlet可能已经成为一个相对陌生的话题。接下来,将以餐厅点菜为例,带你快速熟悉Servlet.

当你走进一家餐厅,拿起菜单准备点菜时,总会有一个“服务员”会在你身旁,负责接收你的点菜信息,然后将你的餐单交给后厨去处理,等后厨烹调完毕后,“服务员”会将菜肴端到你的桌前。

类似的,当你在浏览器上访问一个网页或提交表单时,Servlet就像是一个厨师和服务员的组合。它接收你的订单(请求),然后根据你的选择(数据),然后告诉厨师要做什么(执行业务逻辑),最后将做好的食物端到你的桌子上(返回动态生成的页面),让你享受美食.

简而言之,Servlet的作用在于接收并处理客户端的HTTP请求,并生成响应结果.

Servlet的体系结构

image.png

  • Servlet:Servlet体系根接口
  • GenericServletServlet抽象实现类
  • HttpServlet: 对HTTP协议封装的Servlet实现类

Servlet作为一个接口,并不能通过new关键字来进行实例化,所以如果我们期待享受Servlet所提供的服务,则必须依赖于Servlet的实现类HttpServlet,其扩展了GenericServlet类,专门用于处理HTTP请求.

此外,HttpServlet提供了对HTTP请求的封装和处理方法,使得Servlet可以根据Http的请求方法的不同信息,转发至对应的处理方法,如Get请求转发至doGet()处理,Post请求转发至doPost()处理.

Servlet的生命周期

事实上,在Servlet对象的创建、初始化、接收请求、销毁等过程中,都会执行对应的方法,其分别为init()、service()、destory()方法. 而这些方法也被称为是Servlet的生命周期方法中. 这些方法的具体功能如下:

  • init():在Servlet对象被创建后,容器会调用init()方法进行初始化。该方法只会在Servlet的整个生命周期中被调用一次。可以在init()方法中进行一些初始化操作,例如加载配置文件、建立数据库连接等。
  • service():每当有HTTP请求到达Servlet时,容器会调用service()方法来处理请求,并据请求的类型(GET、POST等)来调用相应的doGet()doPost()等方法。在这些方法中,可以定义请求处理逻辑、业务操作逻辑等,并生成相应的HTTP响应.
  • destroy():在Servlet容器决定销毁Servlet对象时,会调用destroy()方法。这通常发生在Web应用程序关闭或Servlet容器关闭的时候。在destroy()方法中,你可以进行一些资源释放和清理的操作,例如关闭数据库连接、释放资源等.

总的来看,当Servlet对象创建时,会调用init()进行初始化;在接收HTTP请求时,则会调用service()方法处理请求;而在销毁时会调用destroy()进行资源清理.

Servlet中的请求处理

image.png

  • 首先,浏览器和服务器建立连接,生成请求数据包,将请求数据包发送给服务器;
  • 接着,服务器解析请求数据包,创建requestresponse对象,将请求数据存入request对象中;
  • 随后,服务器调用Servletservice()方法,会将requestresponse作为参数传进来,并将处理结果写入到response中;
  • 最后,浏览器解析response中的响应内容,在页面上生成响应内容.

揭秘DispatcherServlet

Spring MVC中,DispatcherServlet作为一个前端控制器,其负责接收所有的HTTP请求,同时,将请求分发给相应的处理器(Controller)来处理,并最终返回响应结果。

DispatcherServlet体系结构

通过上图不难看出,DispatcherServlet的体系结构可以分为两条脉络,一条以Servlet体系结构为基础,另一条则以Spring为基础,其中HttpServletBean则是两者的一个媒介,进而使得Spring中的某个Bean具有Servlet的相关功能.

但我们并不打算详细分析DispatcherServlet的体系结构,因为这样容易让我们陷入到具体细节中,不利于全局的视野建立. 所以,我们将精力集中在DispatcherServlet之上.

在接下来的分析中,请忘记掉上述DispatcherServlet复杂的继承关系结构,只需要记住:DispatcherServlet是一个Servlet. 所以,我们关注的内容聚焦在Servlet的初始化以及Http请求的处理上.

DispatcherServlet初始化的秘密

因为DispatcherServlet是一个Servlet,所以在DispatcherServlet对象被创建后,Tomcat容器会调用其中的init()方法来完成Servlet的初始化工作,所以若要分析DispatcherServlet初始化的秘密,就应该研究其DispatcherServletinit()的方法.

image.png

DispatcherServlet中的init()方法的逻辑如上图所示,不难发现其中核心方法为initWebApplicationContext,即初始化一个web容器. 其相关代码如下:

(ps:initWebApplicationContext() 的相关逻辑定义在DispatcherServlet的父类FrameWorkServlet之中.)

FrameWorkServlet# initWebApplicationContext()


public abstract class FrameWorkServlet {

    // 成员变量 webApplicationContext 用于保存web容器
    private WebApplicationContext webApplicationContext;

    // ....省略其他无关方法
    protected WebApplicationContext initWebApplicationContext() {
       WebApplicationContext rootContext =
             WebApplicationContextUtils.getWebApplicationContext(getServletContext());
       WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
           //获取当前环境中的web容器信息,如果Spring和SpringMVC整合,则不需要重新创建
           wac = this.webApplicationContext;
           if (wac instanceof ConfigurableWebApplicationContext) {
              ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
              if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                   //设置父子容器将Spring容器设为SpringMVC的父容器
                   cwac.setParent(rootContext);
                }
                 // 容器刷新,将SpringMVC配置文件中的属性加载到容器中
                 configureAndRefreshWebApplicationContext(cwac);
              }
           }
        }
        // ....省略其他无关代码 
        if (wac == null) {
           // 如果当前上下文中没有web容器,则重新创建一个
           // 主要针对单独使用SpringMVC时的情况
           wac = createWebApplicationContext(rootContext);
        }

       if (!this.refreshEventReceived) {
          // 调用onRefresh方法,从而为容器中初始化一些组件信息
          // DispathcerServlet的核心组件都通过此处完成初始化
          synchronized (this.onRefreshMonitor) {
             onRefresh(wac);
          }
       }
       return wac;
    }
  
  // ....省略其他无关方法
}

通过FrameworkServlet的继承体系我们可以看到,其实现了ApplicationContextAware接口,这使得其具有获取到Spring中应用上下文(ApplicationContextContext)的能力. 而FrameworkServlet的成员变量webApplicationContext 可以将 Servlet 上下文与 Spring 容器上下文进行关联,其实际类型 ConfigurableWebApplicationContext.

事实上,如果在web.xml 中配置的 ContextLoaderListener 监听器初始化的容器上下文容器信息. 那么Spring SpringMVC的容器之间便会存在一种父子关系,即 Spring 的容器是父容器,SpringMVC的容器为子容器.

通过上述分析不难看出DispatcherServlet在初始化阶段做的最核心工作就是构建容器,然后加载DispatcherServlet的相关配置信息.

DispatcherServlet请求处理的秘密

Servlet的请求处理中,每当有Http请求到达Servlet时,都会调用其内部的service()方法来进行处理,并返回相应的处理结果. 接下来,我们将分析DispatcherServletservice()相关内容,从而分析清楚DispatcherServlet内部是如何来完成一个请求处理的.

在开始分析之前,我们应该明白当Http到达DispatcherServlet时,其首先会调用DispatcherServlet中的service()方法,但该方法的具体实现是在FrameworkServlet中,而FrameworkServletservice方法则会通过super.service()继续向上调用父类HttpServletBeanservice()的方法,但HttpServletBean中并未重写service()方法,所以只能继续向上找到其父类HttpServlet中的service()方法,而此时HttpServlet中的service()方法,逻辑如下:

HttpServlet #service()

protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {
    // 获取请求方法
    String method = req.getMethod();
    // 当请求方式为Get时,调用doGet处理请求
    if (method.equals(METHOD_GET)) {
        doGet(req, resp);
    }  else if (method.equals(METHOD_POST)) {
        doPost(req, resp);

    }
    // ....省略其他无关代码
}

不难发现HttpServletservice()的主要逻辑为:首先,获取到请求方法的类型,然后,调用doXXX()方法(例如doGet、doPost等)来处理请求,而这些方法的处理逻辑在FrameworkServlet中进行了重新定义,具体如下:

protected final void doGet(HttpServletRequest request, 
                            HttpServletResponse response)
      throws ServletException, IOException {
   // 交给DispatcherServlet进行实现
   processRequest(request, response);
}


protected final void processRequest(HttpServletRequest request, 
                                    HttpServletResponse response)
      throws ServletException, IOException {
    // ....省略其他无关代码
   
   // doService逻辑交给子类DispatcherServlet实现
   doService(request, response);
   
   // ....省略其他无关代码
}

因此,最终的所有处理逻辑都DispatcherServlet中的doService进行处理. 此时,请求处理的整个调用过程如下图所示. 所以如果要分析Http请求在DispatcherServlet的处理过程,只需要关注方法doService即可.

Service()的具体调用关系如下所示.

image.png

通过上图可以知道,当一个Http请求到达DispatcherServlet后,其会通过内部的service()方法来完成请求的处理,并最终委托于DispatcherServletdoService方法来进行处理.

(Ps:由于DispatcherServlet复杂的继承关系,所以service()的调用过程稍显复杂; 但通过梳理service()的调用链我们可以知道,所有Http请求的处理都会委托于DispatcherServlet中的doService.)

doService中,所有的处理逻辑又会委托于 doDispatch进行处理,其核心代码如下:

protected void doDispatch(HttpServletRequest request, 
                HttpServletResponse response) throws Exception {

        // 省略无关代码....
   
       // 检测请求是否为上传请求,如果是则通过 multipartResolver 将其封装成 MultipartHttpServletRequest对象
       processedRequest = checkMultipart(request);
 
       // 获得请求对应的 HandlerExecutionChain 
       mappedHandler = getHandler(processedRequest);
         
       // 如果获取不到,则根据配置抛出异常或返回 404 错误
       if (mappedHandler == null) {
           noHandlerFound(processedRequest, response);
           return;
       }

       //  获得当前 handler 对应的 HandlerAdapter 对象
       HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

       // 拦截器前置处理逻辑 
       if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;
       }

       // 调用 handler 方法,也就是执行对应的方法,并返回视图
       mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
       
       // 后置处理 拦截器
       mappedHandler.applyPostHandle(processedRequest, response, mv);
     
      // 处理正常和异常的请求调用结果
      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
   
     // 省略其他无关代码....
}

DispatcherServlet中的doDispatch方法是Spring MVC的核心方法,用于处理HTTP请求并进行请求的分发。它是根据请求的URL和请求类型(GET、POST等)来确定要调用哪个控制器(Controller)的方法,并处理控制器方法的返回结果,最终生成HTTP响应,其大致处理逻辑如下图所示:

image.png

  • 解析请求信息: 首先,doDispatch方法会解析HTTP请求的URL、请求类型、请求参数等信息,以确定要调用哪个控制器的哪个方法来处理请求。
  • 查找处理器(Handler): 接下来,doDispatch会根据请求的URL查找合适的处理器(通常是Controller对象)和处理器方法。这个过程是通过Spring的HandlerMapping组件来实现的,HandlerMapping会根据URL和配置的映射规则,找到对应的Controller和方法。
  • 执行处理器方法: 一旦找到处理器和方法,doDispatch会调用该方法,并将HTTP请求的参数传递给方法。Controller方法会执行业务逻辑,并返回一个包含响应数据的ModelAndView对象。
  • 处理返回结果: 接着,doDispatch会根据Controller方法返回的ModelAndView对象,来决定如何处理响应结果。如果返回结果是一个视图(View),doDispatch会通过ViewResolver将逻辑视图名称解析为真实的视图对象,然后使用视图对象来渲染生成HTML等内容。
  • 发送响应: 最后,doDispatch将生成的响应内容发送给客户端(通常是浏览器),完成HTTP响应的过程。
  • DispatcherServlet中的doDispatch方法可以根据HTTP请求的信息,找到合适的控制器并调用处理器方法,然后处理返回结果,最终生成HTTP响应。所以一定程度上,可以将doDispatch认为是Spring MVC的核心,因此后续我们的讨论都将围绕上图doDispatch的处理逻辑展开.

    总结

    本文首先介绍了Servlet的相关内容,并以此为基础分析了Spring MVC中的核心组件DispatcherServlet,在整个过程中,我们所秉持的理念一直都是忽视掉DispatcherServlet复杂的继承关系,而将其看做是一个Servlet,进而从Servlet的角度,并对DispatcherServlet的初始化和请求处理进行了详细的分析.

    不过在整个处理过程中涉及到SpringMVC中处理请求的组件还没有进行分析,或许你对于许多细节存在疑惑,但不要慌,后续文章将对 SpringMVC的其他核心组件进行分析. 而DispatcherServlet 中的doDispatch方法中的处理逻辑将贯彻整个分析过程,这样有利于加深我们对 SpringMVC 的理解,同时能将DispatcherServlet 组件同其他组件串联起来,进而更好的理解SpringMVC中对于一个请求的处理.

    相关文章

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

    发布评论