SpringBoot實戰分析-Tomcat方式部署

前言

Spring Boot 初體驗一文中咱們學習了以 JAR 形式快速啓動一個Spring Boot程序,而 Spring Boot 也支持傳統的部署方式: 將項目打包成 WAR,而後由 Web 服務器進行加載啓動,此次以 Tomcat 爲例,咱們就快速學習下如何以 WAR 方式部署一個 Spring Boot 項目,代碼託管於 Github, 並作一些簡單的源碼分析.java

正文

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

image-20181014094106403

繼承 SpringBootServletInitializer 加載

打開下載的工程後,對啓動類 SpringbootTomcatApplication 進行修改, 繼承 SpringBootServletInitializer 這個抽象類,而且重寫父類方法 SpringApplicationBuilder configure(SpringApplicationBuilder builder) .github

@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 容器啓動程序時加載這個類.web

修改打包方式爲 WAR

接下來在 pom.xml 文件中,修改打包方式爲 WAR,讓 Maven 構建時以 WAR 方式生成.spring

<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 服務器加載了,在當前項目路徑下使用構建命令tomcat

mvn clean package
複製代碼

出現 BUILD SUCCESS 就說明打包成功了springboot

image-20181014101039797

而後就能夠項目的 target 目錄下看到生成的 WAR.bash

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.但願有所幫助,後續仍會更多的實戰和分析,敬請期待哈. 😁😁😁.

參考

相關文章
相關標籤/搜索