該系列文章是筆者在學習 Spring Boot 程序中總結下來的,里面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼注釋 Spring Boot 原始碼分析 GitHub 地址 進行閱讀
Spring Boot 版本:2.2.x
最好對 Spring 原始碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章
如果該篇內容對您有幫助,麻煩點擊一下“推薦”,也可以關注博主,感激不盡~
該系列其他文章請查看:《精盡 Spring Boot 原始碼分析 - 文章導讀》
概述
我們知道 Spring Boot 應用能夠被打成 war 包,放入外部 Tomcat 容器中運行,你是否知道 Spring Boot 是如何整合 Spring MVC 的呢?
在上一篇 《Spring Boot 內嵌 Tomcat 容器的實作》 文章中分析了 Spring Boot 白打成 jar 包后是如何創建 Tomcat 容器并啟動的,那么這篇文章主要告訴你 Spring Boot 應用被打成 war 包后放入外部 Tomcat 容器是如何運行的,
如何使用
在我們的 Spring Boot 專案中通常會引入 spring-boot-starter-web 這個依賴,該模塊提供全堆疊的 WEB 開發特性,包括 Spring MVC 依賴和 Tomcat 容器,我們將內部 Tomcat 的 Starter 模塊排除掉,如下:
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
然后啟動類這樣寫:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
// 可不寫
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
這樣你打成 war 包就可以放入外部的 Servlet 容器中運行了,
實作原理
原理在分析 Spring MVC 原始碼的時候講過,參考我的 《精盡Spring MVC原始碼分析 - 尋找遺失的 web.xml》 這篇文章
借助于 Servlet 3.0 的一個新特性,新增的一個 javax.servlet.ServletContainerInitializer 介面,在 Servlet 容器啟動時會通過 Java 的 SPI 機制從 META-INF/services/javax.servlet.ServletContainerInitializer 檔案中找到這個介面的實作類,然后呼叫它的 onStartup(..) 方法,
在 Spring 的 spring-web 模塊中該檔案是這么配置的:
org.springframework.web.SpringServletContainerInitializer
一起來看看這個類:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(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);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
通過 @HandlesTypes 注解指定只處理 WebApplicationInitializer 型別的類
這個程序很簡單,實體化所有 WebApplicationInitializer 型別的物件,然后依次呼叫它們的 onStartup(ServletContext) 方法
通過打斷點你會發現,有一個 DemoApplication 就是我們的啟動類
這也就是為什么如果你的 Spring Boot 應用需要打成 war 包放入外部 Tomcat 容器運行的時候,你的啟動類需要繼承 SpringBootServletInitializer 這個抽象類,因為這個抽象類實作類 WebApplicationInitializer 介面,我們只需要繼承它即可
SpringBootServletInitializer
org.springframework.boot.web.servlet.support.SpringBootServletInitializer 抽象類,實作了 WebApplicationInitializer 介面,目的就是支持你將 Spring Boot 應用打包成 war 包放入外部的 Servlet 容器中運行
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
protected Log logger; // Don't initialize early
private boolean registerErrorPageFilter = true;
protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) {
this.registerErrorPageFilter = registerErrorPageFilter;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
// <1> 創建一個 WebApplicationContext 作為 Root Spring 應用背景關系
WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
if (rootAppContext != null) {
// <2> 添加一個 ContextLoaderListener 監聽器,會監聽到 ServletContext 的啟動事件
// 因為 Spring 應用背景關系在上面第 `1` 步已經準備好了,所以這里什么都不用做
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
@Override
public void contextInitialized(ServletContextEvent event) {
// no-op because the application context is already initialized
}
});
} else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
+ "return an application context");
}
}
}
在 onStartup(ServletContext) 方法中就兩步:
- 呼叫
createRootApplicationContext(ServletContext)方法,創建一個 WebApplicationContext 作為 Root Spring 應用背景關系 - 添加一個 ContextLoaderListener 監聽器,會監聽到 ServletContext 的啟動事件,因為 Spring 應用背景關系在上面第
1步已經準備好了,所以這里什么都不用做
第 1 步是不是和 Spring MVC 類似,同樣創建一個 Root WebApplicationContext 作為 Spring 應用背景關系的父物件
createRootApplicationContext 方法
createRootApplicationContext(ServletContext) 方法,創建一個 Root WebApplicationContext 物件,如下:
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// <1> 創建一個 SpringApplication 構造器
SpringApplicationBuilder builder = createSpringApplicationBuilder();
// <2> 設定 `mainApplicationClass`,主要用于列印日志
builder.main(getClass());
// <3> 從 ServletContext 背景關系中獲取最頂部的 Root ApplicationContext 應用背景關系
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
// <4> 如果已存在 Root ApplicationContext,則先置空,因為這里會創建一個 ApplicationContext 作為 Root
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
// <4.1> 添加一個 ApplicationContextInitializer 初始器,
// 用于設定現在要創建的 Root ApplicationContext 應用背景關系的父容器為 `parent`
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
/**
* <5> 添加一個 ApplicationContextInitializer 初始器
* 目的是往 ServletContext 背景關系中設定 Root ApplicationContext 為現在要創建的 Root ApplicationContext 應用背景關系
* 并將這個 ServletContext 保存至 ApplicationContext 中,參考 {@link ServletWebServerApplicationContext#createWebServer()} 方法,
* 如果獲取到了 ServletContext 那么直接呼叫其 {@link ServletWebServerApplicationContext#selfInitialize} 方法來注冊各個 Servlet、Filter
* 例如 {@link DispatcherServlet}
*/
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
// <6> 設定要創建的 Root ApplicationContext 應用背景關系的型別(Servlet)
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
// <7> 對 SpringApplicationBuilder 進行擴展
builder = configure(builder);
// <8> 添加一個 ApplicationListener 監聽器
// 用于將 ServletContext 中的相關屬性關聯到 Environment 環境中
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
// <9> 構建一個 SpringApplication 物件,用于啟動 Spring 應用
SpringApplication application = builder.build();
// <10> 如果沒有設定 `source` 源物件,那么這里嘗試設定為當前 Class 物件,需要有 `@Configuration` 注解
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(getClass()));
}
// <11> 因為 SpringApplication 在創建 ApplicationContext 應用背景關系的程序中需要優先注冊 `source` 源物件,如果為空則拋出例外
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
// <12> 添加一個錯誤頁面 Filter 作為 `sources`
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
// <13> 呼叫 `application` 的 `run` 方法啟動整個 Spring Boot 應用
return run(application);
}
程序如下:
-
創建一個 SpringApplication 構造器,目的就是啟動 Spring 應用咯
protected SpringApplicationBuilder createSpringApplicationBuilder() { return new SpringApplicationBuilder(); } -
設定
mainApplicationClass,也就是你的啟動類,主要用于列印日志 -
從 ServletContext 背景關系中獲取最頂部的 Root ApplicationContext 應用背景關系
parent,通常這里沒有父物件,所以為空 -
如果
parent不為空,則先 ServletContext 中的該屬性置空,因為這里會創建一個 ApplicationContext 作為 Root- 添加一個
ApplicationContextInitializer初始器,用于設定現在要創建的 Root ApplicationContext 應用背景關系的父容器為parent
- 添加一個
-
添加一個
ApplicationContextInitializer初始器,目的是往 ServletContext 背景關系中設定 Root ApplicationContext 為現在要創建的 Root ApplicationContext 應用背景關系,并將這個 ServletContext 保存至 ApplicationContext 中注意,這個物件很關鍵,會將當前 ServletContext 背景關系物件設定到 ApplicationContext 物件里面,那么后續就不會再創建 Spring Boot 內嵌的 Tomcat 了
-
設定要創建的 Root ApplicationContext 應用背景關系的型別(Servlet)
-
對 SpringApplicationBuilder 進行擴展,呼叫
configure(SpringApplicationBuilder)方法,這也就是為什么我們的啟動類可以重寫該方法,通常不用做什么 -
添加一個 ApplicationListener 監聽器,用于將 ServletContext 中的相關屬性關聯到 Environment 環境中
-
構建一個 SpringApplication 物件
application,用于啟動 Spring 應用 -
如果沒有設定
source源物件,那么這里嘗試設定為當前 Class 物件,需要有@Configuration注解 -
因為 SpringApplication 在創建 ApplicationContext 應用背景關系的程序中需要優先注冊
source源物件,如果為空則拋出例外 -
添加一個錯誤頁面 Filter 作為
sources -
呼叫
application的run方法啟動整個 Spring Boot 應用
整個程序不復雜,SpringApplication 相關的內容在前面的 《SpringApplication 啟動類的啟動程序》文章中已經分析過,這里的關鍵在于第 5 步
添加的 ServletContextApplicationContextInitializer 會將當前 ServletContext 背景關系物件設定到 ApplicationContext 物件里面
ServletContextApplicationContextInitializer
public class ServletContextApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableWebApplicationContext>, Ordered {
private int order = Ordered.HIGHEST_PRECEDENCE;
private final ServletContext servletContext;
private final boolean addApplicationContextAttribute;
public ServletContextApplicationContextInitializer(ServletContext servletContext) {
this(servletContext, false);
}
public ServletContextApplicationContextInitializer(ServletContext servletContext,
boolean addApplicationContextAttribute) {
this.servletContext = servletContext;
this.addApplicationContextAttribute = addApplicationContextAttribute;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
public void initialize(ConfigurableWebApplicationContext applicationContext) {
// 將這個 ServletContext 背景關系物件設定到 ApplicationContext 中
applicationContext.setServletContext(this.servletContext);
if (this.addApplicationContextAttribute) {
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
applicationContext);
}
}
}
可以看到會將這個 ServletContext 背景關系物件設定到 ApplicationContext 中
那么我們回顧到上一篇 《Spring Boot 內嵌 Tomcat 容器的實作》 文章的 1. onRefresh 方法小節呼叫的 createWebServer() 方法,如下:
// ServletWebServerApplicationContext.java
private void createWebServer() {
// <1> 獲取當前 `WebServer` 容器物件,首次進來為空
WebServer webServer = this.webServer;
// <2> 獲取 `ServletContext` 背景關系物件
ServletContext servletContext = getServletContext();
// <3> 如果 WebServer 和 ServletContext 都為空,則需要創建一個
// 使用 Spring Boot 內嵌 Tomcat 容器則會進入該分支
if (webServer == null && servletContext == null) {
// <3.1> 獲取 Servlet 容器工廠物件(默認為 Tomcat)`factory`
ServletWebServerFactory factory = getWebServerFactory();
/**
* <3.2> 先創建一個 {@link ServletContextInitializer} Servlet 背景關系初始器,實作也就是當前類的 {@link this#selfInitialize(ServletContext)} 方法
* 至于為什么不用 Servlet 3.0 新增的 {@link javax.servlet.ServletContainerInitializer} 這個類,我在
* [精盡Spring MVC原始碼分析 - 尋找遺失的 web.xml](https://www.cnblogs.com/lifullmoon/p/14122704.html)有提到過
*
* <3.3> 從 `factory` 工廠中創建一個 WebServer 容器物件
* 例如創建一個 {@link TomcatWebServer} 容器物件,并初始化 `ServletContext` 背景關系,創建 {@link Tomcat} 容器并啟動
* 啟動程序異步觸發了 {@link org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup} 方法
* 也就會呼叫這個傳入的 {@link ServletContextInitializer} 的 {@link #selfInitialize(ServletContext)} 方法
*/
this.webServer = factory.getWebServer(getSelfInitializer());
}
// <4> 否則,如果 ServletContext 不為空,說明使用了外部的 Servlet 容器(例如 Tomcat)
else if (servletContext != null) {
try {
/** 那么這里主動呼叫 {@link this#selfInitialize(ServletContext)} 方法來注冊各種 Servlet、Filter */
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// <5> 將 ServletContext 的一些初始化引數關聯到當前 Spring 應用的 Environment 環境中
initPropertySources();
}
我們看到上面第 4 步,如果從當前 Spring 應用背景關系獲取到了 ServletContext 物件,不會走上面的第 3 步,也就是不創建 Spring Boot 內嵌的 Tomcat
主動呼叫它的 getSelfInitializer() 方法來往這個 ServletContext 物件中注冊各種 Servlet、Filter 和 EventListener 物件,包括 Spring MVC 中的 DispatcherServlet 物件,該方法參考上一篇 《Spring Boot 內嵌 Tomcat 容器的實作》 文章的 2. selfInitialize 方法 小節
總結
本文分析了 Spring Boot 應用被打成 war 包后是如何支持放入外部 Tomcat 容器運行的,原理也比較簡單,借助 Spring MVC 中的 SpringServletContainerInitializer 這個類,它實作了 Servlet 3.0 新增的 javax.servlet.ServletContainerInitializer 介面
-
通過 Java 的 SPI 機制,在
META-INF/services/javax.servlet.ServletContainerInitializer檔案中寫入SpringServletContainerInitializer這個類,那么在 Servlet 容器啟動的時候會呼叫這個類的onStartup(..)方法,會找到WebApplicationInitializer型別的物件,并呼叫他們的onStartup(ServletContext)方法 -
在我們的 Spring Boot 應用中,如果需要打成
war包放入外部 Tomcat 容器運行,啟動類則需要繼承SpringBootServletInitializer抽象類,它實作了WebApplicationInitializer介面 -
在
SpringBootServletInitializer中會創建一個 WebApplicationContext 作為 Root Spring 應用背景關系,同時會將 ServletContext 物件設定到 Spring 應用背景關系中 -
這樣一來,因為已經存在 ServletContext 物件,那么不會再創建 Spring Boot 內嵌的 Tomcat 容器,而是對 ServletContext 進行一些初始化作業
好了,到這里關于 Spring Boot 啟動 Spring 應用的整個主流程,包括內嵌 Tomcat 容器的實作,以及支持運行在外部 Servlet 容器的實作都分析完了
那么接下來,我們一起來看看 @SpringBootApplication 這個注解,也就是 @EnableAutoConfiguration 自動配置注解的實作原理
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288850.html
標籤:其他
下一篇:整理一波Go工程化目錄結構~
