SpringBoot實戰分析-Tomcat方式部署

聞人的技術部落格發表於2018-10-14

前言

Spring Boot 初體驗一文中我們學習了以 JAR 形式快速啟動一個Spring Boot程式,而 Spring Boot 也支援傳統的部署方式: 將專案打包成 WAR,然後由 Web 伺服器進行載入啟動,這次以 Tomcat 為例,我們就快速學習下如何以 WAR 方式部署一個 Spring Boot 專案,程式碼託管於 Github, 並做一些簡單的原始碼分析.

正文

利用Spring Initializr 工具下載基本的 Spring Boot 工程,選擇 Maven 方式構建, 版本為正式版1.5.16, 只選擇一個 Web 依賴.

image-20181014094106403

繼承 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 就說明打包成功了

image-20181014101039797

然後就可以專案的 target 目錄下看到生成的 WAR.

image-20181014101142382

部署 Tomcat

springboot-tomcat-0.0.1-SNAPSHOT.war 放在 Tomcat程式的資料夾 **webapps** 下,然後執行Tomcat, 啟動成功就可以在瀏覽器輸入 http://localhost:8080/springboot-tomcat-0.0.1-SNAPSHOT/ ,請求這個簡單 Web 程式了.

image-20181014101753493

到這裡, WAR 方式部署的 Spring Boot 程式就完成了. ???

原始碼分析

完成到這裡, 不禁有個疑問: 為何繼承了 SpringBootServletInitializer 類,並覆寫其 configure 方法就能以 war 方式去部署了呢 ? 帶著問題,我們從原始碼的角度上去尋找答案.

在啟動類 SpringbootTomcatApplication 覆寫的方法進行斷點,看下 Tomcat 執行專案時這個方法呼叫過程.

通過 Debug 方式執行專案,當執行到這行程式碼時,可以看到兩個重要的類 SpringBootServletInitializerSpringServletContainerInitializer .

image-20181014131101858

從圖可以看到 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 類就登場了.

image-20181014133828708

SpringServletContainerInitializer 類實現 Servlet 3.0 規範的 ServletContainerInitializer介面, 也就意味著當 Servlet 容器啟動時,就以呼叫 ServletContainerInitializer 介面的 onStartup方法通知實現了這個介面的類.

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
複製程式碼

現在我們來看下 SpringServletContainerInitializeronStarup 方法的具體實現如下, 關鍵程式碼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 例項的建立.

image-20181014135435492

看到這裡,我們總結下這幾個類呼叫流程,梳理下 Spring Boot 程式 WAR 方式啟動過程:

SpringServletContainerInitializer#onStartup

​ => SpringBootServletInitializer#onStartup

​ => ``SpringBootServletInitializer#createRootApplicationContext​ =>SpringbootTomcatApplication#configure`

另外,我還收穫了一點就是: 當執行 SpringBootServletInitializercreateRootApplicationContext 方法最後,呼叫了run(application).

這也說明了當 WAR方式部署 Spring Boot 專案時, 固定生成的 Main 方法不會再被執行到,是可以去掉.

//當專案以WAR方式部署時,這個方法就是無用程式碼
public static void main(String[] args) {
	SpringApplication.run(SpringbootTomcatApplication.class, args);
}
複製程式碼

結語

本文主要實戰學習如何讓 Spring BootWAR 方式啟動,並且進行簡單的原始碼分析,幫助我們更好地理解 Spring Boot.希望有所幫助,後續仍會更多的實戰和分析,敬請期待哈. ???.

參考

相關文章