springboot集成springMVC原理

springboot集成springMVC原理

Springboot 十分流行,但是它是怎样集成springMVC 的呢,与之前springMVC 单独启动有什么不同呢?今天来探究一下

先了解一下servlet的演变

servlet演变

servlet3.0 以前的时代

为了体现出整个演进过程,还是来回顾下 n 年前我们是怎么写 servlet 和 filter 代码的。

项目结构(本文都采用 maven 项目结构)

image-20210829103841880

HelloWorldServlet

1
2
3
4
5
6
7
8
9
10
public class HelloWorldServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
out.println("hello world");
}

}

HelloWorldFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HelloWorldFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("触发 hello world 过滤器...");
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}
}

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>HelloWorldServlet</servlet-name>
<servlet-class>com.dxysun.servlet2x.servlet.HelloWorldServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>HelloWorldServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

<filter>
<filter-name>HelloWorldFilter</filter-name>
<filter-class>com.dxysun.servlet2x.filter.HelloWorldFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HelloWorldFilter</filter-name>
<url-pattern>/hello</url-pattern>
</filter-mapping>

</web-app>

这样,一个 java web hello world 就完成了。

servlet3.0 以前我们需要配置 web.xml ,添加servlet 和 filter 的相关配置项

而在servlet3.0 之后我们就可以抛弃 web.xml 了

servlet3.0 新特性

Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。

servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。

除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时动态注册 servlet ,filter,listener。

以 servlet 为例,过滤器与监听器与之类似。ServletContext 为动态配置 Servlet 增加了如下方法:

1
2
3
4
5
6
ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
ServletRegistration.Dynamic addServlet(String servletName, String className)
T createServlet(Class clazz)
ServletRegistration getServletRegistration(String servletName)
Map<String,? extends ServletRegistration> getServletRegistrations()

其中前三个方法的作用是相同的,只是参数类型不同而已;通过 createServlet()方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet() 方法来将其动态注册为一个可以用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增加映射信息,这等价于在 web.xml 中使用 标签为存在的 Servlet 增加映射信息。

以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中调用,要么是在ServletContainerInitializer 的 onStartup() 方法中调用。

ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现

ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup()方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup() 处理的类。

一个典型的 servlet3.0+ 的 web 项目结构如下:

image-20210829104555889

我们并未对 HelloWorldServlet 和 HelloWorldFilter 做任何改动,而是新增了一个 CustomServletContainerInitializer , 它实现了 javax.servlet.ServletContainerInitializer 接口,用来在 web 容器启动时加载指定的 servlet 和 filter,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@HandlesTypes(ApplicationInitializer.class)
public class CustomServletContainerInitializer implements ServletContainerInitializer
{

private final static String JAR_HELLO_URL = "/hello";

@Override
public void onStartup(Set<Class<?>> c, ServletContext servletContext)
{

if (c != null)
{
for (Class<?> waiClass : c)
{
if (WebApplicationInitializer.class.isAssignableFrom(waiClass))
{
try
{
WebApplicationInitializer webApplicationInitializer = (WebApplicationInitializer) waiClass
.newInstance();
webApplicationInitializer.init();
}
catch (Throwable ex)
{
throw new RuntimeException("Failed to instantiate WebApplicationInitializer class", ex);
}
}

}
}

System.out.println("创建 helloWorldServlet...");

ServletRegistration.Dynamic servlet = servletContext.addServlet(HelloWorldServlet.class.getSimpleName(),
HelloWorldServlet.class);
servlet.addMapping(JAR_HELLO_URL);

System.out.println("创建 helloWorldFilter...");

FilterRegistration.Dynamic filter = servletContext.addFilter(HelloWorldFilter.class.getSimpleName(),
HelloWorldFilter.class);

EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);

filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);

}
}

ServletContext 我们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener,以 servlet 为例,可以使用 servletContext.addServlet 等方法来添加 servlet。而方法入参中 Set<Class> c@HandlesTypes 注解在 demo 中我自定义了一个接口,接口定义如下

1
2
3
public interface ApplicationInitializer {
void init() throws ServletException;
}

实现类如下

1
2
3
4
5
6
7
8
public class WebApplicationInitializer implements ApplicationInitializer
{
@Override
public void init()
{
System.out.println("WebApplicationInitializer init");
}
}

@HandlesTypes 可以指定接口,接口实现类会被传入Set<Class> c参数,也可以指定具体类,但只有继承该具体类的继承类才会被传入Set<Class> c参数,指定的具体类不会传进去

正常的流程是使用 @HandlesTypes 指定需要处理的 class,继承了该class的类会被当做入参传入 Set<Class> c参数,而后可以对 Set<Class> c进行判断是否属于该 class,正如前文所言,onStartup 会加载不需要被处理的一些 class。

这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制来指定该初始化类,这一步骤是通过在项目路径下创建 META-INF/services/javax.servlet.ServletContainerInitializer 来做到的,它只包含一行内容:

1
com.dxysun.servlet3x.CustomServletContainerInitializer

使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 web.xml 了。

Spring 是如何支持 servlet3.0 的?

spring 又是如何支持 servlet3.0 规范的呢?

寻找 spring 中 ServletContainerInitializer 的实现类并不困难,可以迅速定位到 SpringServletContainerInitializer 该实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {

List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
// <1>
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}

if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}

servletContext.log(initializers.size() + "Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// <2>
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}

<1> 英文注释是 spring 源码中自带的,它提示我们由于 servlet 厂商实现的差异,onStartup 方法会加载我们本不想处理的 class,所以进行了特判。

<2> spring 与之前的 demo 不同,并没有在 SpringServletContainerInitializer 中直接对 servlet 和 filter 进行注册,而是委托给了一个陌生的类 WebApplicationInitializer ,WebApplicationInitializer 类便是 spring 用来初始化 web 环境的委托者类,它通常有三个实现类:

image-20210829134420628

AbstractDispatcherServletInitializer#registerDispatcherServlet 便是无 web.xml 前提下创建 dispatcherServlet 的关键代码。

可以去项目中寻找一下 org.springframework:spring-web:version 的依赖,它下面就存在一个 servletContainerInitializer 的扩展,指向了 SpringServletContainerInitializer,这样只要在 servlet3.0 环境下部署,spring 便可以自动加载进行初始化:

image-20210829134850532

注意,上述这一切特性从 spring 3 就已经存在了,而如今 spring 5 已经伴随 springboot 2.0 一起发行了。

SpringBoot 如何加载 Servlet

springboot加载servlet并没有遵循servlet规范。

前面所讲述的 servlet 的规范,无论是 web.xml 中的配置,还是 servlet3.0 中的 ServletContainerInitializer 和 springboot 的加载流程都没有太大的关联。

先看看如何在 springboot 中注册 servlet 和 filter。

注册 servlet 和 filter

注册方式一:servlet3.0 注解 +@ServletComponentScan

springboot 依旧兼容 servlet3.0 一系列以 @Web* 开头的注解:@WebServlet,@WebFilter,@WebListener

1
2
@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet{}
1
2
@WebFilter("/hello/*")
public class HelloWorldFilter implements Filter {}

让启动类去扫描到这些注解

1
2
3
4
5
6
7
@SpringBootApplication
@ServletComponentScan
public class SpringBootServletApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootServletApplication.class, args);
}

注册方式二:RegistrationBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean
public ServletRegistrationBean helloWorldServlet() {
ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean();
myServlet.addUrlMappings("/hello");
myServlet.setServlet(new HelloWorldServlet());
return helloWorldServlet;
}

@Bean
public FilterRegistrationBean helloWorldFilter() {
FilterRegistrationBean helloWorldFilter = new FilterRegistrationBean();
myFilter.addUrlPatterns("/hello/*");
myFilter.setFilter(new HelloWorldFilter());
return helloWorldFilter;
}

ServletRegistrationBean 和 FilterRegistrationBean 都集成自 RegistrationBean ,RegistrationBean 是 springboot 中广泛应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,并且完成自身对 web 容器的注册。这种注册方式也值得推崇。

image-20210829142239405

从图中可以看出 RegistrationBean 的地位,它的几个实现类作用分别是:帮助容器注册 filter,servlet,listener,最后的 DelegatingFilterProxyRegistrationBean 使用的不多。另外 RegistrationBean 实现了 ServletContextInitializer 接口,这个接口将会是下面分析的核心接口。

SpringBoot 中 servlet 加载流程的源码分析

为什么说 springboot 没有完全遵守 servlet3.0 规范。讨论的前提是 springboot 环境下使用内嵌的容器,比如最典型的 tomcat。

Initializer 被替换为 TomcatStarter

当使用内嵌的 tomcat 时,会发现 springboot 完全走了另一套初始化流程,完全没有使用前面提到的 SpringServletContainerInitializer,而是使用了 TomcatStarter 这个类,这两个类都实现了ServletContainerInitializer这个接口。

image-20210829142720661

但是有 没有SPI 文件对应到 TomcatStarter。所以内嵌 tomcat 的加载不依赖于 servlet3.0 规范和 SPI!它完全走了一套独立的逻辑。

https://github.com/spring-projects/spring-boot/issues/321 ,这里是作者的回答。

springboot 这么做是有意而为之。springboot 考虑到了如下的问题,在使用 springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者提供了一个替代选项:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 长得特别像,别搞混淆了,前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer,后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。

TomcatStarter 中的 ServletContextInitializer 是关键

TomcatStarter 中的 org.springframework.boot.context.embedded.ServletContextInitializer 是 springboot 初始化 servlet,filter,listener 的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TomcatStarter implements ServletContainerInitializer {

private final ServletContextInitializer[] initializers;

TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}

@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
throws ServletException {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
}

可以看出 TomcatStarter 的主要逻辑,它其实就是负责调用一系列 ServletContextInitializer 的 onStartup 方法。

ServletContextInitializer[] initializers 到底包含了哪些类呢?会不会有我们前面介绍的 RegisterBean 呢?

image-20210829144045291

RegisterBean 并没有出现在 TomcatStarter 的 debug 信息中,initializers 只包含了三个类,其中只有最后一个类看上去比较核心,注意这个类不是 ServletWebServerApplicationContext!而是这个类中的 $1 匿名类,进一步debug对应其selfInitialize这个方法

1
2
3
4
5
6
7
8
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}

那么问题来了 ServletWebServerApplicationContext的selfInitialize是何时放到initializers的数组里呢?

ServletWebServerApplicationContext

我们从springboot的启动入手,AnnotationConfigServletWebServerApplicationContext 继承了ServletWebServerApplicationContext,springboot而springboot在servlet环境中默认加载的ApplicationContext就是AnnotationConfigServletWebServerApplicationContext

org.springframework.boot.SpringApplication#run(java.lang.String…) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 这个地方创建的ApplicationContext就是AnnotationConfigServletWebServerApplicationContext
context = createApplicationContext();
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新加载ApplicationContext,也就是创建ioc容器
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

进入refreshContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
refresh((ApplicationContext) context);
}
...
protected void refresh(ConfigurableApplicationContext applicationContext) {
applicationContext.refresh();
}

refresh 一路走进去,最后到applicationContext.refresh();

因为AnnotationConfigServletWebServerApplicationContext 继承了ServletWebServerApplicationContext,最后会进入到ServletWebServerApplicationContext#refresh()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
// 调用父类org.springframework.context.support.AbstractApplicationContext的refresh()
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}

父类org.springframework.context.support.AbstractApplicationContext的refresh()如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

这个方法是spring IOC容器的加载过程,这里不再过多深入,我们只要关注onRefresh();方法,这个会调用子类的onRefresh方法

在这里 onRefresh() 会调用ServletWebServerApplicationContext#onRefresh()方法

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onRefresh() {
super.onRefresh();
try {
// 关键方法,创建了内嵌的 servlet 容器
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

createWebServer()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
ServletWebServerFactory factory = getWebServerFactory();
// 此处的factory是TomcatServletWebServerFactory
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}

this.webServer = factory.getWebServer(getSelfInitializer());关键代码在这一行,factory此时为TomcatServletWebServerFactory,至于为什么是TomcatServletWebServerFactory,后面再介绍。

getSelfInitializer() 便涉及到了我们最为关心的初始化流程。

1
2
3
4
5
6
7
8
9
10
11
12
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}

private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}

还记得前面 TomcatStarter 的 debug 信息中,最后一个 ServletContextInitializer 就是出现在 ServletWebServerApplicationContext 中的一个匿名类,没错了,就是这里的 getSelfInitializer()方法创建的!

解释下这里的 getSelfInitializer() 和 selfInitialize(ServletContext servletContext) 为什么要这么设计:这是典型的回调式方式,当匿名 ServletContextInitializer 类被 TomcatStarter 的 onStartup 方法调用,设计上是触发了 selfInitialize(ServletContext servletContext) 的调用。

所以这下就清晰了,为什么 TomcatStarter 中没有出现 RegisterBean ,其实是隐式触发了 ServletWebServerApplicationContext 中的 selfInitialize 方法。selfInitialize 方法中的 getServletContextInitializerBeans() 成了关键。

1
2
3
4
5
6
7
8
9
10
/**
* Returns {@link ServletContextInitializer}s that should be used with the embedded
* Servlet context. By default this method will first attempt to find
* {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
* {@link EventListener} beans.
* @return the servlet initializer beans
*/
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
return new ServletContextInitializerBeans(getBeanFactory());
}

注释都告诉我们,这个 ServletContextInitializerBeans 是用来加载 Servlet 和 Filter 的。

ServletContextInitializerBeans 的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SafeVarargs
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class);
addServletContextInitializerBeans(beanFactory);
addAdaptableBeans(beanFactory);
List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
logMappings(this.initializers);
}

进入addServletContextInitializerBeans(beanFactory)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
initializerType)) {
addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
}
}
}

private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
ListableBeanFactory beanFactory) {
if (initializer instanceof ServletRegistrationBean) {
Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof FilterRegistrationBean) {
Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
}
else if (initializer instanceof ServletListenerRegistrationBean) {
EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
}
else {
addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
initializer);
}
}

getOrderedBeansOfType 方法便是去容器中寻找注册过得 ServletContextInitializer ,这时候就可以把之前那些 RegisterBean 全部加载出来了,并且 RegisterBean 还实现了 Ordered 接口,在这儿用于排序。addServletContextInitializerBean这个方法中我们便看到了要找的RegistrationBean。

ServletWebServerApplicationContext 加载流程总结

总结如下:

  • ServletWebServerApplicationContext 的 onRefresh 方法触发配置了一个匿名的 ServletContextInitializer。
  • 这个匿名的 ServletContextInitializer 的 onStartup 方法会去容器中搜索到了所有的 RegisterBean 并按照顺序加载到 ServletContext 中。
  • 这个匿名的 ServletContextInitializer 最终传递给 TomcatStarter,由 TomcatStarter 的 onStartup 方法去触发 ServletContextInitializer 的 onStartup 方法,最终完成装配!

看到这里ServletContextInitializer是怎么加载RegisterBean 我们搞明白了,但是还有一些疑问?TomcatServletWebServerFactory是哪里加载的?TomcatStarter是怎样创建的?

TomcatServletWebServerFactory的加载

TomcatServletWebServerFactory其实是通过springboot的自动装配加载的

image-20210829155317658

红圈中还有我们熟悉的DispatcherServlet的自动装配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

@Bean
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new ServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/**
* The bean name for a DispatcherServlet that will be mapped to the root URL "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/**
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {

@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}

TomcatStarter的创建

TomcatStarter是被 new 出来的,当调用TomcatServletWebServerFactory.getWebServer()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
// 这个方法会调用configureContext方法
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = getValidDocumentRoot();
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
}
catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
// 在这里得到ServletContextInitializer数组
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
host.addChild(context);
// TomcatStarter 在这个方法里被示例化出来
configureContext(context, initializersToUse);
postProcessContext(context);
}

最后会调到TomcatServletWebServerFactory.configureContext ,TomcatStarter 是被主动实例化出来的,并且还传入了 ServletContextInitializer 的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
// new 一个 TomcatStarter
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}

mergeInitializers,会添加三个 ServletContextInitializer,包含了 ServletWebServerApplicationContext 中的匿名实现。

1
2
3
4
5
6
7
8
protected final ServletContextInitializer[] mergeInitializers(ServletContextInitializer... initializers) {
List<ServletContextInitializer> mergedInitializers = new ArrayList<>();
mergedInitializers.add((servletContext) -> this.initParameters.forEach(servletContext::setInitParameter));
mergedInitializers.add(new SessionConfiguringInitializer(this.session));
mergedInitializers.addAll(Arrays.asList(initializers));
mergedInitializers.addAll(this.initializers);
return mergedInitializers.toArray(new ServletContextInitializer[0]);
}

至此,springboot中springMVC的加载分析完成

第三种注册 Servlet 的方式

研究完了上述 springboot 启动的内部原理,可以发现 ServletContextInitializer 其实是 spring 中 ServletContainerInitializer 的代理,虽然 springboot 中 Servlet3.0 不起作用了,但它的代理还是会被加载的,于是我们有了第三种方式注册 servlet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
public class CustomServletContextInitializer implements ServletContextInitializer {

private final static String JAR_HELLO_URL = "/hello";

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("创建 helloWorldServlet...");

ServletRegistration.Dynamic servlet = servletContext.addServlet(
HelloWorldServlet.class.getSimpleName(),
HelloWorldServlet.class);
servlet.addMapping(JAR_HELLO_URL);

System.out.println("创建 helloWorldFilter...");

FilterRegistration.Dynamic filter = servletContext.addFilter(
HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);

EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);

filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
}
}

参考

Spring 揭秘 – 寻找遗失的 web.xml

打赏

请我喝杯咖啡吧~

支付宝
微信