在 Spring 及 Spring Boot 的使用过程中,应用最广泛的当属 Web 应用,而 Web 应用又往往部署在像 Tomcat 这样的 Servlet 容器中。
本章将带领大家学习 Spring Boot 中 Web 应用的整合以及在此过程中与直接使用 Spring 的差别。
提到 Spring 的 Web 应用,我们首先想到的可能是 Spring MVC 框架和 web.xmI 等配置文件。而 Spring MVC 又是围绕 DispatcherServlet 这个核心类来展开的。
Spring Boot 当前是基于 Spring 5.2.x 版本,和传统的 Spring 启动有所不同。以前是通过在web.xml 中 配 置 Servlet 来 完 成 Spring MVC 的 启 动 , 但 在 Spring Boot 中 是 通 过Dispatch-erServletAutoConfiguration 来完成初始化工作的。在此过程中,web.xml 遗失了。
我们先回顾一下 Servlet 3.0 之前版本的操作。当我们创建一个 web 项目时,往往会在resources/WEB-INF 目录下创建一个 web.xml 文件, 该文件内配置了 Servlet 和 Filter 等功能。当 Spring MVC 出现后,web.xml 中 便有了 DispatcherServlet 的配置。
随着 Java EE 6 的发布,Servlet 3.0 作为 Java EE 6 规范体系的一员,也被慢慢推广并被用户接受。 Servlet 3.0 在 Servlet 25 的基础. 上提供了一些简化 Web 应用的开发和部署的新特性,无 xml 配置便是其中一项。
Servlet 3.0 提供了@WebServlet、@WebFilter 等注解, 可以使用注解来声明 Servlet 和Filter,这便有了抛弃 web.xml 的基础。同时,它还提供了可以在运行时动态注册 Servlet、Filter、 Listener 等更加强大的功能。 关于动态配置 Servlet,如果翻看 Servlet 3.0 中servlet-api 包下 ServletContext 接口定义的方法,你会看到如下方法定义。
public ServletRegistration. Dynamic addServlet(String servletName, String clas sName);
public ServletRegistration . Dynamic addServlet(String servletName, Servlet s
ervlet);
public ServletRegistration . Dynamic addServlet(String servletName,Class e
xtends Servlet> servletClass);public
eption;
public ServletRegistration getServletRegistration(String servletName);
public Map
();
}
在 Servlet 3.0 中还新增了 ServletContainerlnitializer 接口,在容器启动时使用 JAR 服务 API来发现其实现类,并将容器 WEB-INF/ib 目录下 jar 包中的类都交由该类的 onStartup 方法来处理。而 Servlet 和 Filter 在 Web 应用启动时可借助该接口的实现类和 Java 的 SPI 机制完成加载。
在Spring中 提 供 了ServletContainerlnitializer接 口 的 实 现 类SpringServletContainerlni-tializer, 该 类 在 其 onStartup 方 法 中 会 调 用 所 有WebApplicationInitializer 实现类的 onStartup 方法,将相关组件注册到容器中。而 Servlet和 Filter 也是 通过 WebApplicationitializer 的实现类完成创建和加载的。
基于以上新特性和演变,当我们使用 Spring Boot 时,Spring Boot 已经不知不觉地开始使用这些新特性了。至此,我们从发展的角度了解了 web.xml 消失的过程,在后面的章节我会详细讲解 Spring Boot 是如何进行 Web 应用的自动配置的。
在 Spring Boot 项目中引入 spring-boot-starter-web 的依赖,Spring Boot 的自动配置机制便会加载并初始化其相关组件。整个自动配置原理在前面章节已经讲过,这里针对 Web 应用再进行一-次梳理。
在上一节中我们已经提到,Servlet3.0 中新增了ServletContainerlnitializer 接 口 , 而 在 Spring 中 又 提 供 了 其 实 现 类SpringServletContainerlnitializer 来 初 始 化 Servlet 。 但 在 Spring Boot 中 , 当 引 入spring-boot-starter-web 的依赖之后,Spring Boot 并未完全遵守 Servlet 3.0 的规范,也没有使用 Spring 中提供的 SpringServletContainerlnitializer 类,而是完全选择另外一套初始化流程。下面, 我们看一下初始化流程的源码。
根据自动配置的原理,我们在 spring-boot-autoconfigure 包中的 META-INF/spring.factories配置文件中找到了针对 Servlet 自动配置的 EnableAutoConfiguration。
#自动配置
org. springframework . boot . autoconfigure . EnableAutoConfiguration=\
org. springframework. boot . autoconfigure . web. servlet . DispatcherServletAutoCon下面对 EnableAutoConfiguration 类中的自动配置项目进行逐步分析。
DispatcherServlet 自动配置
DispatcherServlet 自动配置位于 DispatcherServletAutoConfiguration 类中。下面我们通过DispatcherServletAutoConfiguration 的源码来 了解其自动配置的过程。
首先来看 DispatcherServletAutoConfiguration 上面的注解。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type . SERVLET)
@ConditionalOnClass(DispatcherServlet. class)
@AutoConfigureAfter(ServletwWebServerFactoryAutoConfiguration. class)
public class DispatcherServletAutoConfiguration {}
这些注解的基本功能在前面章节已经提到过,我们再来温习一下。
关于 ServletWebServerFactoryAutoConfiguration 的加载会在下一章中详细讲解。
从整体上来看,在 DispatcherServletAutoConfiguration 内部主要提供 了 4 个静态内部类。
主要针对注册 DispatcherServlet 进行一些逻辑判断。
针对以上概述,下面来看具体的源代码实现。首先是内部类 DispatcherServletConfiguration的源码及功能。
@Configuration(proxyBeanMethods = false)
//实例化配置类
@Conditional (DefaultDispatcherServletCondition.class) //实例化条件:通过该
@ConditionalOnClass (ServletRegistration. class)
//存在指定的 ServletRe
gistration 类//加裁 HttpProperties 和 webMvcProperties
@EnableConfigurationProperties({ HttpProperties. class, WebMvcProperties .cla
s })
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT DISPATCHER_ SERVLET_ BEAN NAME)
public DispatcherServlet dispatcherServlet (HttpProperties httpProperties ,
WebMvcProperties webMvcProperties) {
//创建 DispatcherServlet
DispatcherServlet dispatcherServlet = new DispatcherServlet();
//初始化 DispatcherServlet 各项配置
dispatcherServlet . setDispatchOptionsRequest (webMvcProperties . isDispatch
OptionsRequest());
dispatcherServlet . setDispatchTraceRequest (webMvcProperties . isDispatch-
TraceRequest());
dispatcherServlet. setThrowExceptionI fNoHandlerFound(webMvcProperties. is
ThrowExcept ionIfNoHandlerFound();
di spatcherServlet . setPublishEvents (webMvcProperties . isPublishRequestHan
dledEvents());
dispatcherServlet . setEnableLoggingReques tDetails (httpProperties. isLog-
RequestDetails());
return dispatcherServlet;
//初始化上传文件的解析器
@Bean
@ConditionalOnBean(MultipartResolver . class)
@ConditionalOnMissingBean(name = DispatcherServlet . MULTIPART_ RESOLVER_
BEA
NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//检测用户是否创建了 MultipartResolver, 但命名不正确
return resolver;
}
}
通过以上源码可以看出,当满足指定的条件后,会对 DispatcherServletConfiguration 进行实例化,而该类内部通过@Bean 注解的方法会被实例化,生成 Bean 并注入 Spring 容器中。
其中,在 DispatcherServlet 方法中完成 了 DispatcherServlet 的实例化和基本设置。这里既没有用到 SPI 机制,也没用到 Spring 提供的 SpringServletContainerlnitializer 和 Servlet3.0 的 ServletContainerlnitializer 接口,从而验证了 6.2 节提到的说法。
DispatcherServlet 方法将 DispatcherServlet 实例化的 Bean 注入 Spring 容器中,并且指定Bean 的 name 为 dispatcherServlet。该名称默认会被映射到根 URL 的/访问路径。
DispatcherServlet 作为前端控制器设计模式的实现,提供了 Spring WebMVC 的集中访问点,负责职责的分派,与 Spring loc 容器无缝集成,可以获得 Spring 的所有好处。它的主要作用包括:文件上传解析、请求映射到处理器、通过 ViewResolver 解析逻辑视图名到具体视图实现、本地化解析、渲染具体视图等。
在未使用 Spring Boot 时,通常 DispatcherServlet 类的配置需要开发人员在 web.xml 当中进行配置。这里 Spring Boot 通过自动配置完成了 DispatcherServlet 类的配置和初始化,并 在 此 过 程 中 设 置 了 一 些 初 始 化 的 参 数 。 这 些 参 数 通 过 HttpProperties 和WebMvcProperties 获得。
DispatcherServletConfiguration 中还定义了上传文件的解析器 MultipartResolver 的 Bean初始化操作,准确来说是 Bean 名称转化的操作。通过条件注解判断,当 MultipartResolver的 Bean 存在,但 Bean 的名称不为“multipartResolver”时,将其重命名为“multipartResolver'我们再回到 DispatcherServletConfiguration 的注解部分,@Conditional 指定的限定条件类为 DefaultDispatcherServletCondition,该类是 DispatcherServletAutoConfiguration 的另外一个内部类,代码如下。
@Order (Ordered. LOWEST_ PRECEDENCE- 10)
private static class DefaultDispatcherServletCondition extends SpringBootCo
ndition {
@Override
public ConditionOutcome getMatchOutcome (ConditionContext context ,
AnnotatedTypeMetadata metadata) {
ConditionMessage . Builder message = ConditionMessage
. forCondition("Default DispatcherServlet");
ConfigurablelistableBeanFactory beanFactory = context. getBeanFactory();
//获取类型为 Di spatcherServlet 的 Bean 名称列表
List
. getBeanNamesForType
(DispatcherServlet.class, false, false));
//如果 Bean 名称列表中包含 dispatcherServlet,则返回不匹配
if (dispatchServletBeans. contains (DEFAULT_ DISPATCHER_ SERVLET_ BEAN_ NAM
E)) {
return ConditionOutcome . noMatch (message . found("dispatcher servlet bea
n")
. items (DEFAULT_ DISPATCHER_ SERVLET_ BEAN_ NAME));
// 如果 beanFactory 中包含名称为 Di spatcherServlet 的 Bean, 则返回不匹配
if (beanFactory. containsBean(DEFAULT_ DISPATCHER_ SERVLET_ BEAN NAME)) {
return ConditionOutcome
. noMatch(message . found("non dispatcher servlet bean")
. items (DEFAULT_ DISPATCHER_ SERVLET_ BEAN_ NAME));
//如果 Bean 名称列表为空,则返回匹配
if (dispatchServletBeans . isEmpty()) {
return ConditionOutcome
. match(message . didNotFind(”dispatcher servlet beans").atAll());
//其他情况则返回匹配
return ConditionOutcome . match(message
. found("dispatcher servlet bean", "dispat
cher servlet beans")
. items (Style . QUOTE, dispatchServletBeans)
. append("and none is named ”+ DEFAULT_ DI
SPATCHER_ SERVLET_ BEAN_
NAME));
}
}
DefaultDispatcherServletCondition 的最核心业务逻辑只做了一件事,那就是:防止重复生成DispatcherServlet。具体实现流程为:从上下文中获得 beanFactory, 然后通过 beanFactory获取类型为 DispatcherServlet 的 Bean 的 name 列表,然后分别 判断 name 列表和beanFactory 中是否包含名称为“dispatcherServlet"的字符串或 Bean,如果包含则返回不匹配(已经存在,不匹配则不会重复实例化),否则返回匹配。
DispatcherServletConfiguration 类中还有-个注解@EnableConfigurationProperties,该注解指定了两个配置类:
HttpProperties 和 WebMvcProperties。这两个配置类正是上面初始化 DispatcherServlet 时用于初始化的参数值,查看这两个类的源代码就会发现,它们分别对应加载了以“spring.http"和"spring.mvc"为前缀的配置项,可以在 application properties 文件中进行配置。
@ConfigurationProperties(prefix = "spring . http" )
public class HttpProperties {}
@ConfigurationProperties(prefix = "spring . mvc")
public class WebMvcProperties {}
DispatcherServletRegistrationBean 自动配置
下面我们再来看用于注册的 DispatcherServletRegistrationConfiguration 类。关于该类的注解功能可以参考 DispatcherServletConfiguration 的说明,我们主要看:业务逻辑处理。
@Configuration( proxyBeanMethods = false)
@Conditional (DispatcherServletRegistrationCondition. class)
@ConditionalOnClass (ServletRegistration. class)
@EnableConfigurationProperties (WebMvcProperties .class)
@Import (DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_ DISPATCHER_ SERVLET_ REGISTRATION_ BEAN AME)
@ConditionalOnBean(value=
DispatcherServlet.class,
name=DEFAULT_
DISPATCHER_
SERVLET_ BEAN_ NAME)
public
DispatcherServletRegistrationBean
di
spatcherServletRegistration(DispatcherServlet dispatcherServlet,
n We
bMvcProperties webMvcProperties, objectProvider
tipartConfig) {
//通过 ServletRegistrat ionBean 将 dispatcherServlet 注册为 Servlet,这样 serv
Let 才会生效
Di spatcherServletRegistrationBean registration = new DispatcherServletR
egistrationBean( dispatcherServlet ,
webMvcProperties . getServlet(). getPath());
//设置名称为 di spatcherServlet
registration. setName(DEFAULT_ DISPATCHER_ SERVLET_ BEAN NAME);
// 没 置 加 载 优 先 级 , 没 置 值 默 认 为 -1 , 存 在 FwebMvcProperties 类 中 registration.
setLoadOnStartup(webMvcProperties . getServlet(). getLoadOn-Startup());
multipartConfig. ifAvailable(registration::setMultipartConfig);
return registration;
}
}
DispatcherServletRegistrationConfiguration 类的核心功能就是注册 dispatcherServlet,使其生效并设置一一些初始化的参数。
其 中 DispatcherServletRegistrationBean 继 承 自 ServletRegistrationBean , 主 要 为DispatcherServlet 提供服务。
DispatcherServletRegistrationBean 和 DispatcherServlet 都提 供了注 册 Servlet 并公开DispatcherServletPath 信息的功能。
在 dispatcherServletRegistration 方 法 中 直 接 通 过 new 来 创 建DispatcherServletRegistrationBean,第一个 参数为 DispatcherServlet,第二个参数为application 配置文件中配置的 path 值,相关代码如下。
public class DispatcherServletRegistrationBean extends ServletRegistratiomB
ean
private final String path;
//根据指定的 DispatcherServlet 和给定的 path 创建 DispatcherServletRegistratio
nBean
public DispatcherServletRegistrationBean(DispatcherServlet servlet, Strin3 path) {
super(servlet);
Assert . notNull(path, "Path must not be null");
this.path = path;
super . addUrlMappings (getServletUrlMapping());
/重写父类的方法,但抛出异常,相当于禁用该操作
@Override
public void setUrlMappings (Collection
throw new UnsupportedOperationException("URL Mapping cannot be changed
on a DispatcherServlet registration");
//重写父类的方法,但抛出异常,相当于禁用该操作
@verride
public void addUrlMappings(String... urlMappings) {
throw new UnsupportedOperationException("URL Mapping cannot be changed
on a DispatcherServlet registration");
}
}
在 DispatcherServletRegistrationBean 中实现了 setUrlMappings 和 addlUrlManninas 两种方法,但均直接抛出异常,这相当于禁用了该子类中这两项操作。而在构造方法中除了成员变 量 赋 值 之 外 , 还 调 用 了 父 类 ServletRegistrationBean 的 构 造 方 法ServletRegistrationBean(Tservlet,String...urlMappings)和addUrlMappings(String...urlMappings)方法。
关于 ServletRegistrationBean 的构造方法和 addUrlMappings 只是进行成员变量的赋值和设 置 , 我 们 重 点 看 DispatcherServletRegistrationBean 在 构 造 方 法 中 调 用 的getServletUrlMapping 方法。顾名思义,该方法的功能是获取 Servlet 的 URL 的匹配。该方法具体在 DispatcherServletRegistrationBean 实现的接口 DispatcherServletPath 中定义。
DispatcherServletPath 的 主 要 功 能 是 提 供 自 动 配 置 所 需 的 path 信 息 , 而DispatcherServletRegistrationBean 中的 path 信息正是构造方法传入的。
下面我们来看 DispatcherServletPath 中 getServletUrlMapping 方法的具体实现。
@FunctionalInterface
public interface DispatcherServletPath {
//返回一个 URL 匹配表达式,用 FServletRegistrat ionBean 映射对应的 DispatcherSe
rvlet
default String getServletUrlMapping() {
if (getPath(). equals("") || getPath(). equals("/")) {
return "/";
if (getPath(). contains("*")) {
return getPath();
if (getPath(). endsWith("/")) {
return getPath() + *";
return getPath() + "/*";
}
}
getServletUrlMapping 方法为接口的默认实现方法,返回一个用于 ServletRegistrationBean映射 DispatcherServlet 的 URL 表达式。判断逻辑比较简单:如果获得的 path 为空或“/”,则返回“/”; 如果 path 中包含“”,则直接返回 path;如果 path 以“/"结尾,则对 path 后追加“”;其他 情 况 下 , path 后 面 追 加 关 于 dispatcherServletRegistration 方 法 中 对DispatcherServletRegistrationBean 其他属性的简单赋值操作就不再赘述了。
我们再回到针对 DispatcherServletRegistrationConfiguration 的注解@Conditional 指定的限定条件类 DispatcherServletRegistrationCondition,该限定条件类主要用来判断进行dis-patcherServlet 和 dispatcherServletRegistration 是否存在等。
DispatcherServletRegistrationCondition 中判断条件及代码较多,这里只展示顶层判断方法的源代码。
@Order (Ordered. LOWEST_ PRECEDENCE - 10)
private static class DispatcherServletRegistrationCondition extends SpringBoot-Condition
{
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, Annotated-
TypeMetadata metadata) {
ConfigurableListableBeanFactory beanFactory = context . getBeanFactory();
//判断是否重复存在 dispatcherServlet
ConditionOutcome outcome = checkDefaultDispatcherName (beanFactory);
if (!outcome . isMatch()) {
return outcome ;//判断是否重复存在 dispatcherServletRegistration
return checkServletRegistration(beanFactory);
}
DispatcherServletRegistrationCondition 的 getMatchOutcome 方 法 中 分 别 判 断 了dispatcherServlet 和 dispatcherServletRegistration 是否已经存在对应的 Bean。具体判断方法基本与上节讲到的 DefaultDispatcherServletCondition 的判断方法一致。
至此,在该自动配置类中,DispatcherServlet 的创建、 简单初始化和注册已经完成。当第一次接收到网络请求时,DispatcherServlet 内部会进行一 系列的初始化操作,这些更多属于 Spring 的内容,就不再展开了。