Spring MVC 揭秘:如何利用 Servlet 技术
介绍
早在 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 使用``将请求映射到``DispatcherServlet```web.xml` 中的 `urlPatterns` 元素或者 `@WebServlet` 注解的 `urlPatterns` 属性。对于动态 servlet 注册,它依赖于 `ServletRegistration` 接口的 `addMapping()` 方法。
现在,让我们仔细看看剩下的四点。
创建 ApplicationContext

当通过 `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` 中的配置,我们将简要介绍三个关键示例:
正如前面提到的,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”注册为侦听器:
Spring 检查 `web.xml` 中的 `contextClass` 参数' 级别来确定要初始化的上下文类。如果未指定,则默认为 `contextConfigLocation` 参数。Spring 还提供了一个重载构造函数,您可以在其中传入 `WebApplicationContext`,这在动态注册 `ContextLoaderListener` 时很有用
这是应用程序生命周期中 Spring 创建并配置“WebApplicationContext”的第二个点,第一个点是在“DispatcherServlet”中。

实例:共享 DAO Bean
在我们的银行应用程序中,尽管它是模块化的,但某些 bean 是跨模块共享的,例如基础架构或业务逻辑组件。例如,数据访问层 (DAO) bean 是所有模块所共有的,并且在每个“WebApplicationContext”中维护单独的版本效率很低。根应用程序上下文是共享这些 bean 的理想解决方案。
要设置根上下文,可以在“web.xml”中注册“ContextLoaderListener”,如下所示:
`web.xml`
org.springframework.web.context.ContextLoaderListener contextClass spring_mvc_unveiled.root.RootApplicationContext
**📌 注意**

调度请求
`DispatcherServlet` 是 Spring MVC 的核心,充当应用程序的前端控制器。对于每个传入请求,servlet 容器都会调用 `DispatcherServlet` 的 `service()` 方法,就像它对 Servlet API 生命周期中的任何其他 servlet 所做的那样。从那里开始,`DispatcherServlet` 开始负责,利用这个基础 servlet 机制准备请求并将其分派给适当的处理程序,展示了 Spring MVC 是如何无缝构建在 Servlet API 之上的。
关键工作流程步骤
`DispatcherServlet` 通过将特定任务委托给专门的组件来组织其工作流程,遵循关注点分离原则。

本节重点介绍第二步:确定并执行处理程序,让我们看看此过程中涉及的组件。
处理程序执行链
`HandlerMapping` 并不直接返回处理程序,而是返回 `HandlerExecutionChain` 的实例,它捆绑了:
拦截器可重复使用,并可应用于多个处理程序。例如,安全拦截器可以验证所有处理程序中的凭据,以确保路径安全。虽然拦截器与 servlet 过滤器类似,但功能更强大,因为它们集成到了 Spring MVC 工作流中。

示例: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”注释的方法的处理程序。
对于`DispatcherServlet`遇到的每个处理程序,必须有一个支持它的适配器,否则,`DispatcherServlet`将抛出`ServletException`。如前所述,`HandlerAdapter`是一种特殊的Bean类型,`DispatcherServlet`的默认策略会创建四个`HandlerAdapter`类型的bean。
现在我们已经探索了`DispatcherServlet`用于定位和调用处理程序的关键组件,让我们看看整个流程:

以下是简化的代码流程:
`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**📌 注意**
代码重点:
如何与 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`。
例如:
自动包含@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 框架参考文档是一项宝贵的资源。
。