SpringBoot實戰分析-Tomcat方式部署
在Spring Boot 初體驗一文中我們學習了以 JAR 形式快速啟動一個 Spring Boot
程式,而 Spring Boot
也支援傳統的部署方式: 將專案打包成 WAR
,然後由 Web
伺服器進行載入啟動,這次以 Tomcat
為例,我們就快速學習下如何以 WAR
方式部署一個 Spring Boot
專案,程式碼託管於 ofollow,noindex">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
程式就完成了. :tada::tada::tada:
原始碼分析
完成到這裡, 不禁有個疑問: 為何繼承了 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
.希望有所幫助,後續仍會更多的實戰和分析,敬請期待哈. :grin::grin::grin:.