前言
在Spring Boot 初體驗一文中我們學習了以 JAR 形式快速啟動一個Spring Boot
程式,而 Spring Boot
也支援傳統的部署方式: 將專案打包成 WAR
,然後由 Web
伺服器進行載入啟動,這次以 Tomcat
為例,我們就快速學習下如何以 WAR
方式部署一個 Spring Boot
專案,程式碼託管於 Github, 並做一些簡單的原始碼分析.
正文
利用Spring Initializr 工具下載基本的 Spring Boot
工程,選擇 Maven
方式構建, 版本為正式版1.5.16, 只選擇一個 Web
依賴.
繼承 SpringBootServletInitializer
載入
開啟下載的工程後,對啟動類 SpringbootTomcatApplication
進行修改, 繼承 SpringBootServletInitializer
這個抽象類,並且重寫父類方法 SpringApplicationBuilder configure(SpringApplicationBuilder builder)
.
@SpringBootApplication
public class SpringbootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
}
複製程式碼
SpringBootServletInitializer
類將在 Servlet
容器啟動程式時允許我們對程式自定義配置,而這裡我們將需要讓 Servlet
容器啟動程式時載入這個類.
修改打包方式為 WAR
接下來在 pom.xml
檔案中,修改打包方式為 WAR
,讓 Maven
構建時以 WAR
方式生成.
<groupId>com.one</groupId>
<artifactId>springboot-tomcat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
複製程式碼
另外要注意的是:為了確保嵌入式 servlet
容器不會影響部署war檔案的servlet容器,此處為 Tomcat
。我們還需要將嵌入式 servlet
容器的依賴項標記為 provided
。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
複製程式碼
實現 Rest 請求處理
為了驗證 WAR
部署是否成功,我們實現一個最基礎的處理 Web
請求的功能,在啟動類新增一些 Spring MVC
的程式碼
@SpringBootApplication
@RestController
public class SpringbootTomcatApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(SpringbootTomcatApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
@RequestMapping(value = "/")
public String hello() {
return "hello tomcat";
}
}
複製程式碼
專案打包
現在就可以打包 Spring Boot
程式成 WAR
, 然後讓 Tomcat
伺服器載入了,在當前專案路徑下使用構建命令
mvn clean package
複製程式碼
出現 BUILD SUCCESS
就說明打包成功了
然後就可以專案的 target
目錄下看到生成的 WAR
.
部署 Tomcat
將 springboot-tomcat-0.0.1-SNAPSHOT.war
放在 Tomcat程式的資料夾 **webapps**
下,然後執行Tomcat
, 啟動成功就可以在瀏覽器輸入 http://localhost:8080/springboot-tomcat-0.0.1-SNAPSHOT/ ,請求這個簡單 Web
程式了.
到這裡, WAR
方式部署的 Spring Boot
程式就完成了. ???
原始碼分析
完成到這裡, 不禁有個疑問: 為何繼承了 SpringBootServletInitializer
類,並覆寫其 configure 方法就能以 war 方式去部署了呢 ? 帶著問題,我們從原始碼的角度上去尋找答案.
在啟動類 SpringbootTomcatApplication 覆寫的方法進行斷點,看下 Tomcat 執行專案時這個方法呼叫過程.
通過 Debug 方式執行專案,當執行到這行程式碼時,可以看到兩個重要的類 SpringBootServletInitializer
和 SpringServletContainerInitializer
.
從圖可以看到 configure 方法呼叫是在父類的 createRootApplicationContext
,具體程式碼如下,非關鍵部分已省略,重要的已註釋出來.
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder(); // 新建用於構建SpringApplication 例項的 builder
builder.main(getClass());
// ....
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
builder = configure(builder); // 呼叫子類方法,配置當前 builder
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
SpringApplication application = builder.build(); // 構建 SpringApplication 例項
if (application.getSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.getSources().add(getClass());
}
//...
return run(application); // 執行 SpringApplication 例項
}
複製程式碼
SpringApplicationBuilder
例項, 應該是遵循建造者設計模式,來完成SpringApplication
的構建組裝.
而 createRootApplicationContext
方法的呼叫還是在這個類內完成的,這個就比較熟悉, 因為傳統的 Spring Web
專案啟動也會建立一個 WebApplicationContext
例項.
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case a ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext); // 建立一個 WebApplicationContext 例項.
// ...
}
複製程式碼
問題又來了,這裡的 onStartup
方法又是如何執行到的呢? SpringServletContainerInitializer
類就登場了.
SpringServletContainerInitializer
類實現 Servlet 3.0
規範的 ServletContainerInitializer
介面, 也就意味著當 Servlet
容器啟動時,就以呼叫 ServletContainerInitializer
介面的 onStartup
方法通知實現了這個介面的類.
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
複製程式碼
現在我們來看下 SpringServletContainerInitializer
的 onStarup
方法的具體實現如下, 關鍵程式碼23~24行裡 initializers
是一個 LinkedList
集合,有著所有實現 WebApplicationInitializer
介面的例項,這裡進行迴圈遍歷將呼叫各自的 onStartup
方法傳遞 ServletContext
例項,以此來完成 Web
伺服器的啟動通知.
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 提取webAppInitializerClasses集合中 實現 WebApplicationInitializer 介面的例項
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
// ...
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext); // 呼叫所有實現 WebApplicationInitializer 例項的onStartup 方法
}
}
複製程式碼
追蹤執行到SpringServletContainerInitializer
類的22行, 我們可以看到集合裡就包含了我們的啟動類,因此最後呼叫了其父類的 onStartup
方法完成了 WebApplicationContext
例項的建立.
看到這裡,我們總結下這幾個類呼叫流程,梳理下 Spring Boot
程式 WAR
方式啟動過程:
SpringServletContainerInitializer#onStartup
=> SpringBootServletInitializer#onStartup
=> ``SpringBootServletInitializer#createRootApplicationContext =>
SpringbootTomcatApplication#configure`
另外,我還收穫了一點就是: 當執行 SpringBootServletInitializer
的 createRootApplicationContext
方法最後,呼叫了run(application)
.
這也說明了當 WAR
方式部署 Spring Boot
專案時, 固定生成的 Main
方法不會再被執行到,是可以去掉.
//當專案以WAR方式部署時,這個方法就是無用程式碼
public static void main(String[] args) {
SpringApplication.run(SpringbootTomcatApplication.class, args);
}
複製程式碼
結語
本文主要實戰學習如何讓 Spring Boot
以 WAR
方式啟動,並且進行簡單的原始碼分析,幫助我們更好地理解 Spring Boot
.希望有所幫助,後續仍會更多的實戰和分析,敬請期待哈. ???.