Spring MVC 揭秘:如何利用 Servlet 技术

  • 这是 Inside Spring 系列的第二部分。如果您还没有阅读第一部分 Servlet:Java Web 技术的基础,即使您熟悉该主题,我们也建议您阅读,因为它是很好的复习资料。
  • 我们混合了 XML 和基于注释的配置,以拓宽您对 Spring 和 Servlet API 的理解,并且出于说明目的,而不是对 XML 的偏好,现代实践更倾向于注释。
  • 本文附有 GitHub 上的源代码。阅读时跟随代码将有助于您更好地理解所讨论的概念。
  • 不幸的是,这里的图像质量较差,为了获得更好的质量,请查看 GitHub 存储库中的图像。
  • 介绍
  • 引导和配置 Spring MVC ApplicationContext 创建自配置编程初始化分层 WebApplicationContext 结构
  • 调度请求关键工作流步骤HandlerExecutionChainHandlerAdapter和调用Handler
  • 如何与 SpringBoot 配合使用
  • 结论
  • 介绍

    早在 2013 年,当我开始探索 Spring 时,我对 Servlet 技术有了基本的了解,但不太理解为什么部署 Spring MVC 应用程序需要 Tomcat 这个 Servlet 容器,尽管我没有编写过一个 Servlet。

    我还想知道 Spring 的 `ApplicationContext` 是如何初始化的,或者 Tomcat 如何在部署描述符中没有定义的情况下映射 HTTP 请求。快进 11 年,随着 Spring Boot 的兴起,回答这些问题只会变得更具挑战性。

    在本文中,我们将尝试通过探索 Servlet 技术如何引导和配置 Spring MVC 应用程序、Spring MVC 如何将请求分派到正确的处理程序以及最后所有这些如何与 Spring Boot 无缝集成来回答这些问题。

    我们不会详细介绍 MVC 模式、前端控制器的概念或 Spring 框架的基础知识。我们假设您已经了解“ApplicationContext”是什么以及它在 Spring 应用程序中的作用。如果这些概念对您来说很新,我鼓励您探索许多可用的优秀资源来了解它们。

    引导和配置 Spring MVC

    既然您在这里,您可能知道什么是 servlet,所以让我们从一个新的角度来看待 Spring MVC:它本质上是一个巨大、强大且高度可定制的 servlet。

    **📌 注意**

    Spring MVC 的核心是单个 servlet,即“DispatcherServlet”,它处理每个传入的请求。Spring 本质上告诉 Servlet API:

    我将利用你来引导和配置我自己。完成后,将所有请求转发给我——我会更好地处理它们。

    — Spring MVC 到 Servlet API

    spring mvc overview

  • 请求映射:servlet 容器将所有请求映射到 DispatcherServlet,使 Spring MVC 能够控制请求处理生命周期。
  • ApplicationContext 创建:servlet 容器实例化并初始化 DispatcherServlet,后者本身初始化 Spring ApplicationContext。
  • 自我配置:当 servlet 容器在 DispatcherServlet 上调用 init() 时,它会根据应用程序的设置进行自我配置。
  • 程序化初始化:Spring 允许开发人员使用 ServletContainerInitializer 接口和 @HandlesTypes 注释来影响初始化过程。
  • 分层 WebApplicationContext 结构:Spring 使用 ContextLoaderListener(ServletContextListener 的一个实现)来创建和初始化根 WebApplicationContext,从而建立一个层次结构,其中根上下文为所有 DispatcherServlet 实例提供共享 Bean。
  • 关于第一点,没什么好说的。Spring 使用``将请求映射到``DispatcherServlet```web.xml` 中的 `urlPatterns` 元素或者 `@WebServlet` 注解的 `urlPatterns` 属性。对于动态 servlet 注册,它依赖于 `ServletRegistration` 接口的 `addMapping()` 方法。

    现在,让我们仔细看看剩下的四点。

    创建 ApplicationContext

    create wep app context

    当通过 `web.xml` 或 `@WebServlet` 注释静态注册 `DispatcherServlet` 时,servlet 容器使用其默认的无参数构造函数来实例化它,然后 servlet 容器调用 servlet 的 `init()` 生命周期方法。

    Spring MVC 利用 `init()` 方法调用,从 WEB-INF 目录加载名为 `[servletName]-servlet.xml` 的配置文件,然后创建并初始化 `ApplicationContext` 的子接口 `WebApplicationContext` 的实例。

    Spring 通过 `contextConfigLocation` 初始化参数提供灵活性,该参数允许您指定其他 XML 配置文件。同样,`contextClass` 参数允许您定义要使用的 `ApplicationContext` 实现,通常是 `AnnotationConfigWebApplicationContext` 之类的类。

    为了更好地控制,`DispatcherServlet` 提供了设置这些值的方法。首选方法是扩展 `DispatcherServlet` 并覆盖其默认构造函数,以编程方式配置这些参数。

    实例:用于集成的专用 DispatcherServlet

    在我们的银行应用程序中,假设我们需要一个名为 `integrationAppServlet` 的 `DispatcherServlet`,专门用于第三方集成。我们可以在 `web.xml` 中注册它,如下所示:

    **web.xml**

    
                integrationAppServlet
                org.springframework.web.servlet.DispatcherServlet
                1
            
            
                integrationAppServlet
                /integration/*
            

    然后,Spring MVC 将查找名为“WEB-INF/integrationAppServlet-servlet.xml”的配置文件。它看起来可能像这样:

    `integrationAppServlet-servlet.xml`

    
        
            
        

    此设置指示 Spring MVC 扫描 `spring_mvc_unveiled.integration` 包中的 bean,使用它们创建 `WebApplicationContext`,并将该上下文与 `integrationAppServlet` 关联。此 servlet 将处理所有具有 `/integration` URL 模式的请求。

    **📌 注意**

    自我配置

    Spring 不仅在其 `init()` 方法调用期间为 `DispatcherServlet` 创建 `WebApplicationContext`,而且还注册了称为特殊 Bean 类型的必需 Bean。

    `DispatcherServlet` 将处理请求的实际工作委托给这些 bean,总共有 8 种这样的 bean 类型,但讨论所有这些类型超出了本文的范围。为了清楚地说明 Spring MVC 在 `DispatcherServlet` 中的配置,我们将简要介绍三个关键示例:

  • HandlerMapping:此 bean 可帮助 DispatcherServlet 确定将处理请求的 bean,例如在我们的银行应用程序中,在 BalanceController 类中,我们有使用 @GetMapping 注释进行注释的 balance() 方法,当 GET /customer/balance 请求到达 DispatcherServlet 时,它将要求 HandlerMapping bean 将请求映射到正确的处理程序,然后 HandlerMapping bean 会将请求映射到 BalanceController#balance 方法。支持使用 @RequestMapping 注释的方法映射的 HandlerMapping 实现是 RequestMappingHandlerMapping
  • HandlerExceptionResolver:如果在映射或处理请求期间抛出异常,DispatcherServlet 将要求 HandlerExceptionResolver 类型的 bean 确定异常处理程序。您可能已经使用 @ControllerAdvice 和 @ExceptionHandler 注释来全局处理应用程序中的异常,您可能认为调用方法是一种魔法,但我现在要告诉您,事实并非如此,它实际上是 HandlerExceptionResolver 的一个实现,名为 ExceptionHandlerExceptionResolver,它执行此操作
  • ViewResolver:当你的处理程序返回一个字符串时,Spring MVC 会认为它是一个视图的名称,而 DispatcherServlet 会要求 ViewResolver 类型的 bean 来解析将返回给客户端的真实视图
  • 正如前面提到的,Spring MVC 通过首先检查程序员是否定义了这些 bean 来配置 `DispatcherServlet`,如果没有,它将继续执行默认策略,即创建 `DispatcherServlet` 所需的所有 bean。

    如果需要定制,您可以提供这些类型的 bean,Spring 会选择它们,或者使用带有 `WebMvcConfigurer` 接口的 `@EnableWebMvc` 注释。这种方法允许对 Spring MVC 流程进行细粒度控制,同时仍在需要时利用框架的默认值。

    编程初始化

    程序员可以实现 `WebApplicationInitializer` 接口,该接口包含一个方法:`onStartup(ServletContext servletContext)`。Spring MVC 调用此方法并提供 `ServletContext` 实例,允许开发人员在应用程序启动期间执行任务。这让开发人员可以掌控初始化过程。

    但是 Spring 如何实现这一点呢?它使用 SpringServletContainerInitializer,它是 ServletContainerInitializer 的一个实现,使用 @HandlesTypes(WebApplicationInitializer.class) 注释。如 ServletContainerInitializer 部分所述,servlet 容器将 WebApplicationInitializer 的所有实例传递给 SpringServletContainerInitializer 的 onStartup 方法。然后 Spring 调用每个 WebApplicationInitializer 实例的 onStartup 方法并提供 ServletContext 实例。

    实例:后台专用的 DispatcherServlet

    鉴于我们的银行应用程序的多模块特性,需要一个专用的“DispatcherServlet”来处理“/back_office”URL 路径下的后台请求。通过实现“WebApplicationInitializer”接口,我们可以动态注册“DispatcherServlet”,而无需依赖注释或“web.xml”。以下是示例:

    `BackOfficeAppInitializer.java`

    @Override
            public void onStartup(ServletContext servletContext) {
                var webApplicationContext = new AnnotationConfigWebApplicationContext();
                webApplicationContext.register(BackOfficeWebConfig.class);
                var dispatcherServlet = new DispatcherServlet(webApplicationContext);
                var dispatcher = servletContext.addServlet("backOfficeAppServlet", dispatcherServlet);
                dispatcher.setLoadOnStartup(1);
                dispatcher.addMapping("/back_office/*");
            }

    这种编程方法可以完全控制如何创建和配置“DispatcherServlet”和“WebApplicationContext”,使其成为静态注册的有力替代方案。

    Spring 在 `AbstractAnnotationConfigDispatcherServletInitializer` 中提供了 `WebApplicationInitializer` 的便捷抽象实现,这使得基于 Java 配置类初始化 `DispatcherServlet` 变得容易。请查看我们扩展了抽象类的 `OthersAppServlet`。

    层次化的 WebApplicationContext 结构

    Spring 利用 ServletContextListener 接口来支持“WebApplicationContexts”的层次结构,从而实现跨多个上下文的共享 bean。这是通过“ContextLoaderListener”(Spring 的“ServletContextListener”实现)实现的。“ContextLoaderListener”创建一个根“WebApplicationContext”,作为应用程序中所有子上下文的父上下文。在根上下文中定义的 bean 可供其子上下文访问,但反之则不行。要使用此功能,开发人员需要通过以下方法之一将“ContextLoaderListener”注册为侦听器:

  • web.xml元素。
  • @WebListener 注释。
  • 以编程方式使用 ServletContext。
  • Spring 检查 `web.xml` 中的 `contextClass` 参数' 级别来确定要初始化的上下文类。如果未指定,则默认为 `contextConfigLocation` 参数。Spring 还提供了一个重载构造函数,您可以在其中传入 `WebApplicationContext`,这在动态注册 `ContextLoaderListener` 时很有用

    这是应用程序生命周期中 Spring 创建并配置“WebApplicationContext”的第二个点,第一个点是在“DispatcherServlet”中。

    hierarchical context

    实例:共享 DAO Bean

    在我们的银行应用程序中,尽管它是模块化的,但某些 bean 是跨模块共享的,例如基础架构或业务逻辑组件。例如,数据访问层 (DAO) bean 是所有模块所共有的,并且在每个“WebApplicationContext”中维护单独的版本效率很低。根应用程序上下文是共享这些 bean 的理想解决方案。

    要设置根上下文,可以在“web.xml”中注册“ContextLoaderListener”,如下所示:

    `web.xml`

    
                org.springframework.web.context.ContextLoaderListener
        
        
               contextClass
               spring_mvc_unveiled.root.RootApplicationContext
        

    **📌 注意**

    startup process

    调度请求

    `DispatcherServlet` 是 Spring MVC 的核心,充当应用程序的前端控制器。对于每个传入请求,servlet 容器都会调用 `DispatcherServlet` 的 `service()` 方法,就像它对 Servlet API 生命周期中的任何其他 servlet 所做的那样。从那里开始,`DispatcherServlet` 开始负责,利用这个基础 servlet 机制准备请求并将其分派给适当的处理程序,展示了 Spring MVC 是如何无缝构建在 Servlet API 之上的。

    关键工作流程步骤

    `DispatcherServlet` 通过将特定任务委托给专门的组件来组织其工作流程,遵循关注点分离原则。

  • 准备请求:执行跨切任务,例如确定语言环境、解析多部分请求以及将请求属性存储在 RequestContextHolder 中。
  • 确定并执行处理程序:使用 HandlerMapping 来识别处理程序并通过 HandlerAdapter 调用它。
  • 解决异常:将异常处理委托给 HandlerExceptionResolver。此处也提到
  • 准备响应:涉及通过 ViewResolver 解析视图以确定如何呈现响应等任务。此处也提到
  • request process flow

    本节重点介绍第二步:确定并执行处理程序,让我们看看此过程中涉及的组件。

    处理程序执行链

    `HandlerMapping` 并不直接返回处理程序,而是返回 `HandlerExecutionChain` 的实例,它捆绑了:

  • Handler:这是处理请求的对象。它可以是任何对象,从而提供设计灵活性。例如,HandlerMethod 是一个处理程序,它表示使用 @RequestMapping 和类似注释(@GetMapping、@PostMapping 等)注释的控制器方法。
  • 拦截器:这些是使用 HandlerInterceptor 实现的预处理和后处理钩子。preHandle() 方法在处理程序之前运行,可以阻止请求。postHandle() 方法在处理程序之后运行,可以修改响应。
  • 拦截器可重复使用,并可应用于多个处理程序。例如,安全拦截器可以验证所有处理程序中的凭据,以确保路径安全。虽然拦截器与 servlet 过滤器类似,但功能更强大,因为它们集成到了 Spring MVC 工作流中。

    handler execution chain

    示例:HandlerMethod

    处理程序被 `HandlerExecutionChain` 引用为 `Object`,这意味着,对于 `DispatcherServlet`,处理程序本质上只是一个对象。处理程序的实际类型可以是任何类型;例如,它可以是 `HandlerMethod`。`HandlerMethod` 表示使用 `@RequestMapping` 及其变体(如 `@GetMapping`、`@PostMapping`)注释的方法。当收到请求时,Spring 会为每个注释方法创建一个 `HandlerMethod` bean,允许适配器通过反射调用该方法。

    `HandlerMethod` 的作用不仅仅是包装对方法的引用,它还维护状态以表示方法的参数和返回值。这很重要,因为它允许 Spring 动态解析方法参数(例如 HTTP 请求数据)并在运行时使用正确的上下文调用该方法。

    示例:注册拦截器

    开发人员负责定义他们的拦截器并将其注册到 Spring MVC,这可以通过使用 `@EnableWebMvc` 注释和 `WebMvcConfigurer` 接口来实现。在我们的银行应用程序中,假设我们想要拦截 `/balance` 路径,我们将像下面这样注册它:

    `客户Web配置`

    @Configuration
        @EnableWebMvc
        public class CustomerWebConfig implements WebMvcConfigurer {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
        /*
                the path pattern is relative to the DispatcherServlet root path
                in this case /customer
        */
                registry.addInterceptor(new BalanceInterceptor()).addPathPatterns("/**");
            }
        }

    还有“BalanceInterceptor”:

    `BalanceInterceptor.java`

    public class BalanceInterceptor implements HandlerInterceptor {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                // Logic goes here
                return true; // Return true to continue; false to block the request
            }
        }

    HandlerAdapter 和调用处理程序

    由于处理程序是通用的“对象”实例,因此“DispatcherServlet”使用“HandlerAdapter”对象来调用它们。每个“HandlerAdapter”都知道如何处理特定类型的处理程序。例如,“RequestMappingHandlerAdapter”知道如何调用“HandlerMethod”,即使用“@RequestMapping”注释的方法的处理程序。

  • supports():确定适配器是否可以处理给定的处理程序类型。
  • handle():使用请求和响应对象调用处理程序,返回 ModelAndView。
  • 对于`DispatcherServlet`遇到的每个处理程序,必须有一个支持它的适配器,否则,`DispatcherServlet`将抛出`ServletException`。如前所述,`HandlerAdapter`是一种特殊的Bean类型,`DispatcherServlet`的默认策略会创建四个`HandlerAdapter`类型的bean。

    现在我们已经探索了`DispatcherServlet`用于定位和调用处理程序的关键组件,让我们看看整个流程:

    overall handling request

    以下是简化的代码流程:

    `DispatcherServlet.java`

    HandlerExecutionChain  executionChain = getExecutionChain(request);
                HandlerAdapter adapter = getHandlerAdapter(executionChain.getHandler());
                if (!executionChain.applyPreHandle(request, response)) {
                    return;// Request blocked by one of the preHandle() methods
                }
                // Use the adapter to invoke the handler
                ModelAndView  mv = adapter.handle(request, response, executionChain.getHandler());
                executionChain.applyPostHandle(request, response, mv);
    
                // continue with preparing the response

    **📌 注意**

    代码重点:

  • getExecutionChain(request):遍历所有 HandlerMapping bean 来找到请求的 HandlerExecutionChain。如果没有匹配则返回 null。
  • getHandlerAdapter(handler):遍历 HandlerAdapter bean,调用它们的 supports() 方法。如果没有适配器支持该处理程序,则抛出 ServletException。
  • applyPreHandle(request, response): 执行所有适用的 HandlerInterceptor 对象的 preHandle() 方法。如果任何一个返回 false,则请求被阻止。
  • adapter.handle():将处理程序的实际调用委托给适当的 HandlerAdapter,后者知道如何调用它。
  • applyPostHandle(request, response, mv):处理程序处理完请求后,执行所有拦截器的 postHandle() 方法。
  • 如何与 SpringBoot 配合使用

    如果不指出所有这些与 Spring Boot 的关系,本文就不完整。Spring Boot 通过删除与传统设置相关的大量样板配置,简化了设置和运行 Spring MVC 应用程序的过程。让我们探索 Spring Boot 如何实现这一点,以及它如何与我们迄今为止讨论过的 Servlet 技术无缝集成。

    嵌入式 Servlet 容器

    Spring Boot 的一个主要功能是它能够捆绑嵌入式 servlet 容器,例如 Tomcat、Jetty 或 Undertow。Spring Boot 不会将您的应用程序部署到外部 servlet 容器,而是将您的应用程序打包为“胖 JAR”,其中包含运行应用程序所需的一切。这允许您将应用程序作为独立的 Java 进程运行,从而简化部署并实现可移植性。

    当您的 Spring Boot 应用程序启动时,它会使用“EmbeddedServletContainerFactory”初始化嵌入式 servlet 容器。此工厂负责配置和启动 servlet 容器,允许 Spring Boot 在初始化过程中动态注册“DispatcherServlet”和其他组件。

    @EnableAutoConfiguration

    Spring Boot 通过 `@EnableAutoConfiguration` 注释进一步降低了复杂性。此注释会扫描类路径中的 Spring 组件和配置文件,自动创建和连接必要的 bean,包括 `DispatcherServlet`。

    例如:

  • 它检测 Spring MVC 相关库的存在并自动配置 DispatcherServlet。
  • 它创建并注册默认组件,例如 HandlerMapping、HandlerAdapter 和 ViewResolver。
  • 它甚至设置了默认错误处理、静态资源服务和其他开箱即用的便利功能。
  • 自动包含@EnableWebMvc

    当应用程序中存在 Spring MVC 时,Spring Boot 会自动包含 `@EnableWebMvc`。这可确保应用 Spring MVC 的默认配置,而无需明确包含注释。如果需要,开发人员仍可以通过实现 `WebMvcConfigurer` 接口来覆盖和自定义这些配置。

    整合所有

    使用 Spring Boot,设置 Spring MVC 应用程序不再需要大量配置。它充分利用了 Spring 核心框架的强大功能,同时让您更轻松地专注于业务逻辑而不是基础架构。通过将所有内容捆绑到一个大型 JAR 中、提供嵌入式 servlet 容器并自动配置基本组件,Spring Boot 彻底改变了开发和部署体验。

    这种简化,加上根据需要进行定制的灵活性,使得 Spring Boot 成为现代 Java Web 应用程序开发的自然选择。

    结论

    在本文中,我们探讨了 Spring 如何利用 Servlet 技术的关键特性来引导自身并高效地调度请求。从理解“DispatcherServlet”的基础作用到研究“RequestMapping”和“HandlerAdapter”如何协同工作以路由和处理请求,我们还深入研究了分层的“WebApplicationContext”结构和 Spring 的自配置机制。

    掌握这些概念是加深对 Spring 框架理解的重要一步,超越基本用法,了解底层架构和设计原理。

    为了巩固您的学习成果,我鼓励您尝试本文提供的随附代码。将其用作您的试验场,以测试和探索 Spring MVC 的后台工作原理。此外,对于进一步阅读和参考,Spring 框架参考文档是一项宝贵的资源。