SpringBoot項目的兩種打包方式分析
點擊上方 Java學(xué)習(xí)之道,選擇 設(shè)為星標(biāo)
作者: 枕邊書
來源: zhenbianshu.github.io
Part1前言
最近對 Spring 越來越感興趣,卻在閱讀它的源碼時很容易被類之間的跳轉(zhuǎn)和方法的嵌套繞暈,為了避免無盡的煩惱,我決定跟它做一個了斷,不再追求細節(jié),了解其啟動過程和重要組件即可,之后遇到細節(jié)問題再看對應(yīng)模塊的源碼。
我們都知道,一個 Java Web 服務(wù)進程,Web 服務(wù)器是其必不可少的組件之一,僅有 Spring 是無法受理系統(tǒng)的 HTTP 請求的。而且在 Java 的 Servlet 模型里,Spring 是作為 Web 服務(wù)器里的一個 Servlet 存在的,這就更能說明 Spring 和 Web 服務(wù)器的關(guān)系之親密了。
但每個 Java 進程都只有一個主類,進程從其靜態(tài)的 main 函數(shù)里啟動,那么在 Spring 和 Web 服務(wù)器配合的場景中,Spring 和 Web 服務(wù)器之間是誰先啟動的呢?Spring 容器和 Servlet 容器之間有包含關(guān)系嗎?
要回答這兩個問題,還需要分場景來看。
由于 Java Web 服務(wù)器里我們最熟悉 Tomcat,后面就以 Tomcat 來代表 Web 服務(wù)器了,而 Spring 相關(guān)我們都基于 SpringBoot 來說。
Part2Jar 包方式
實例
我們最??吹降氖窍旅胬又械膶懛?。
@SpringBootApplication
public?class?ServerStarter?{
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(ServerStarter.class,?args);
????}
}
這種方式非常符合我們對 Java 進程啟動的認知??梢钥吹剑覀冊?ServerStarter.main 方法里直接使用了 SpringApplication.run() 方法,而 Spring 啟動的流程都在 SpringApplication 類內(nèi),我們后續(xù)再詳細分析。
在 IDE 里我們可以直接執(zhí)行這個主類來啟動 Spring 應(yīng)用,而脫離了 IDE,我們就只能把整個應(yīng)用打成一個 fat jar,并且在 jar 包的 MANIFEST 文件內(nèi)設(shè)置 Main-Class 屬性值為 package.path.ServerStarter 來聲明主類。這樣,在使用 java -jar spring_starter.jar 時, JVM 就知道該從哪個類開始加載。
聯(lián)系
Spring 容器是啟動成功后, Tomcat Web 服務(wù)器的啟動就要靠 Spring 了,我查看了 SpringApplication 類的代碼整理出以下流程。
創(chuàng)建 SpringApplication 時使用在 deduceWebApplicationType()通過一些標(biāo)志類(如 org.springframework.web.servlet.DispatcherServlet) 是否存在,推斷是出應(yīng)用類型,一般是WebApplicationType.SERVLET。創(chuàng)建 createApplicationContext()方法內(nèi),根據(jù)應(yīng)用類型創(chuàng)建出一個AnnotationConfigServletWebServerApplicationContext類型的 ConfigurableApplicationContext。由于 AnnotationConfigServletWebServerApplicationContext 類是 ServletWebServerApplicationContext 的子類,在 ApplicationContext.refresh()時,會通過ServletWebServerApplicationContext.onRefresh()方法創(chuàng)建一個 webServer。DispatcherServlet 通過 ServletContextInitializer -> ServletRegistrationBean注冊到 Tomcat 容器內(nèi),完成兩者的關(guān)聯(lián)。
Part3War 包方式
實例
在生產(chǎn)環(huán)境部署服務(wù)時,我們更多地使用 war 包的方式,因為它可以很方便地支持 jsp,而通過 jsp 我們可以給生產(chǎn)環(huán)境調(diào)試添加一定的靈活性。在使用 docker 部署時,也能將 Tomcat 和服務(wù)分層,便于鏡像的維護。
我們知道,一個 war 包是沒有自運行能力的,必須要先啟動一個 Tomcat 進程,由 Tomcat 自動解壓并加載 webapps 目錄下的 war 包來啟動服務(wù),所以通過 war 包啟動的 Spring Web 主類一定是 Tomcat 類。
在主類已存在的情況下,我們的 Spring 入口類就不再需要 main 方法了,常見寫法如下:
@SpringBootApplication
public?class?ServerStarter?extends?SpringBootServletInitializer?{
????@Override
????protected?SpringApplicationBuilder?configure(SpringApplicationBuilder?application)?{
????????return?application.sources(ServerStarter.class);
????}
}
可以看到,Spring 入口類繼承了 SpringBootServletInitializer 類,最上層的接口是 org.springframework.web;.WebApplicationInitializer。
聯(lián)系
查看 Tomcat 的 web.xml 配置,沒有一絲 spring 的痕跡,要知道 Tomcat 是如何聯(lián)系上 Spring 的,還要從 WebApplicationInitializer 接口開始。
這種實現(xiàn)方式需要兩種新特性的支持:
Java 1.6 之后新添加了一個類 java.util.ServiceLoader,提供了一種新的有別于 ClassLoader 的類發(fā)現(xiàn)機制。當(dāng)我們在META-INF/services文件夾內(nèi)添加文件,文件名是全路徑的接口,文件內(nèi)容每一行是全路徑的接口實現(xiàn),就可以通過ServiceLoader.load(Class interface)方法加載到對應(yīng)的接口實現(xiàn),這種機制就是SPI(Service Provider Interface),使用這種機制,加載用戶實現(xiàn)的特定接口時就不需要類加載器掃描所有的類文件了。不過如果要加載的接口是 Java 底層類時,類加載器會是 BootstrapClassLoader 或 ExtClassLoader,加載子類時需要指定使用 classLoader 為 currentThreadClassLoader (一般是加載應(yīng)用的 AppClassLoader)。Servlet 3.0+ 提供了另一種替代 web.xml 的 Servlet 配置機制 ServletContainerInitializer接口,Tomcat 會在 Servlet 容器創(chuàng)建后調(diào)用 ServletContainerInitializer.onStartup() 方法,便于我們向容器內(nèi)注冊一些 Servlet 對象。
我一路跟隨文檔向上查看,總結(jié)了一下整個流程。
Spring 在 web 包的 META-INF/services 內(nèi)添加了文件 javax.servlet.ServletContainerInitializer,內(nèi)容為 org.springframework.web.SpringServletContainerInitializer。 Tomcat 容器啟動后,通過 ServiceLoader 加載到 SpringServletContainerInitializer 實例。 SpringServletContainerInitializer 通過 @HandlesTypes注解獲取到WebApplicationInitializer接口的所有實現(xiàn)(包括 SpringBootServletInitializer、AbstractDispatcherServletInitializer 等)。在 SpringBootServletInitializer.onStartup()方法內(nèi)初始化了 Spring 容器。在 AbstractDispatcherServletInitializer.registerDispatcherServlet()方法內(nèi)實現(xiàn)了 DispatcherServlet 與 Tomcat 的關(guān)聯(lián)。
Part4小結(jié)
看完了兩種服務(wù)打包方式下 Spring 容器被加載的過程,文章開頭的兩個問題應(yīng)該就有跡可循了。
首先 Spring 和 Web 服務(wù)器的啟動先后會根據(jù)服務(wù)打包方式有所不同,使用 jar 包時是 Spring 先啟動,而使用 war 包時是 Web 服務(wù)器先啟動。
而 Spring 容器與 Servlet 容器的包含關(guān)系,我理解是并不存在的,A 啟動了 B,所以 A 包含 B 的理論由上文也可以看出是站不住腳的。Servlet 容器和 Spring 之間只是存儲的元素不同,Servlet 容器內(nèi)存放著 Servlet 實例,而 Spring 中存放著各種環(huán)境變量、Bean 對象等。而它們之前的聯(lián)系就是 DispatcherServlet,它既是一個 Servlet 實例,又是一個 Bean,通過 DispatcherServlet,Tomcat 可以調(diào)用到 Spring 容器內(nèi)部的對象,從線程棧上來看是 Web 服務(wù)器在下,Spring 更往上。
-?
?| 更多精彩文章 -
▽加我微信,交個朋友 長按/掃碼添加↑↑↑



