Spring Boot Tomcat 容器化部署實踐與總結

Spring Boot Tomcat 容器化部署實踐與總結

前言

在平時的工作和學習中經常會構建簡單的web應用程序。如果只是HelloWorld級別的程序,使用傳統的Spring+SpringMVC框架搭建得話會將大部分的時間花費在搭建框架本身上面,比如引入SpringMVC,配置DispatcheherServlet等。並且這些配置文件都差不多,重複這些勞動似乎意義不大。所以使用Springboot框架來搭建簡單的應用程序顯得十分的便捷和高效。

前兩天在工作中需要一個用於測試文件下載的簡單web程序,條件是使用Tomcat Docker Image作為載體,所以為了方便就使用了SpringBoot框架快速搭建起來。

程序寫出來在本機能夠正常的跑起來,準備製作鏡像,但是聞題就接踵而來了。首先是部署的問題,SpringBoot Web程序默認打的是jar包,運行時使用命令 java -jar -Xms128m -Xmx128m xxx.jar,本機跑的沒問題。但是需求是使用外部的tomcat容器而不是tomcat-embed,所以查閱官方文檔如下:

The first step in producing a deployable war file is to provide a SpringBootServletInitializer subclass and override its configure method. Doing so makes use of Spring Framework’s Servlet 3.0 support and lets you configure your application when it is launched by the servlet container . Typically , you should update your application’s main class to extend SpringBootServletInitializer , as shown in the following example:

 @SpringBootApplication
public class Application extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}

public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);

}
}

The next step is to update your build configuration such that your project produces a war file rather than a jar file. If you use Maven and spring-boot-starter-parent(which configures Maven’s war plugin for you), all you need to do is to modify pom.xml to change the packaging to war, as follows:

 war

If you use Gradle, you need to modify build.gradle to apply the war plugin to the project, as follows:

apply plugin: 'war'

The final step in the process is to ensure that the embedded servlet container does not interfere with the servlet container to which the war file is deployed. To do so, you need to mark the embedded servlet container dependency as being provided.

If you use Maven, the following example marks the servlet container (Tomcat, in this case) as being provided:




org.springframework.boot
spring-boot-starter-tomcat
provided



If you use Gradle, the following example marks the servlet container (Tomcat, in this case) as being provided:

 dependencies {
// …
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
// …
}

綜上所述,將SpringBoot程序放入Tomcat運行有兩步。

第一,SpringBoot啟動類繼承SpringBootServletInitializer,重寫configure方法。

第二,將包管理軟件的打包方式改成war,並將Spring-boot-starter-tomcat設置為provided。


但是,為什麼應該這樣做?

根據Servlet3.0規範可知,Web容器啟動時通過ServletContainerInitializer類實現第三方組件的初始化工作,如註冊servlet或filter等,每個框架要是用ServletContainerInitializer就必須在對應的META-INF/services目錄下創建名為javax.servlet.ServletContainerInitializer的文件,文件內容指定具體的ServletContainerInitializer實現類,在SpringMVC框架中為SpringServletContainerInitializer。一般伴隨著ServletContainerInitializer一起使用的還有HandlesTypes註解,通過HandlesTypes可以將感興趣的一些類注入到ServletContainerInitializerde的onStartup方法作為參數傳入。如下為SpringServletContainerInitializer源代碼:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set> webAppInitializerClasses, ServletContext servletContext)throws ServletException {
List initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {

for (Class> waiClass : webAppInitializerClasses) {
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 將@HandlesTypes(WebApplicationInitializer.class)標註的所有這個類型的類都傳入到onStartup方法的Set>;為這些WebApplicationInitializer類型的類創建實例。
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
//為每個WebApplicationInitializer調用自己的onStartup()
initializer.onStartup(servletContext);
}
}
}


SpringBootInitializer繼承WebApplicationInitializer,重寫的onStartup如下:

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(getClass());
// 調用自生createRootApplicationContext()方法
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {

@Override
public void contextInitialized(ServletContextEvent event) {
}
});
}
else {
this.logger.debug("No ContextLoaderListener registered, as "
+ "createRootApplicationContext() did not "
+ "return an application context");
}
}
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(
new ServletContextApplicationContextInitializer(servletContext));
builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
// 調用重寫方法,重寫方法傳入SpringBoot啟動類
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty() && AnnotationUtils
.findAnnotation(getClass(), Configuration.class) != null) {
application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
if (this.registerErrorPageFilter) {
application.addPrimarySources(
Collections.singleton(ErrorPageFilterConfiguration.class));
}
//啟動應用程序,就是啟動傳入的SpringBoot程序
return run(application);
}

在程序和Tomcat打通之後需做的就是將war打成一個Docker鏡像,如果每次都是複製war包,然後再docker build會很麻煩,在開源社區早有了解決方案–docker-maven-plugin,查看Github中的使用方法,將如下內容加入pom.xml中:


com.spotify
docker-maven-plugin
1.1.1

wanlinus/file-server




${project.basedir}



/
${project.build.directory}
${project.build.finalName}.war





該配置中有個標籤是用來指定構建docker image的Dockerfile的位置,在項目的根目錄下新建一個Dockerfile,內容如下:

FROM tomcat
MAINTAINER wanlinus
WORKDIR /docker
COPY target/file-server-0.0.1-SNAPSHOT.war ./server.war
RUN mkdir $CATALINA_HOME/webapps/server \
&& mv /docker/server.war $CATALINA_HOME/webapps/server \
&& unzip $CATALINA_HOME/webapps/server/server.war -d $CATALINA_HOME/webapps/server/ \
&& rm $CATALINA_HOME/webapps/server/server.war \
&& cd $CATALINA_HOME/webapps/server && echo "asd" > a.txt
EXPOSE 8080

終端中輸入

mvn clean package docker:build

在本地將會生成一個docker image,如果docker沒有運行於本地,需要在標籤中輸入遠端地址和docker daemon端口。

最後在終端中運行

docker run --rm -p 8080:8080 wanlinus/fileserver

在Tomcat啟動後將會看到Spring Boot程序的啟動日誌,至此,Spring Boot Tomcat容器化完成。

關於Wise2C睿雲智合

深圳睿雲智合科技有限公司成立於2012年,總部位於深圳,並分別在成都、深圳設立了研發中心,北京、上海設立了分支機構,核心骨幹人員全部為來自金融、科技行業知名企業資深業務專家、技術專家。早期專注於為中國金融保險等大型企業提供創新技術、電子商務、CRM等領域專業諮詢服務。

自2016年始,在率先將容器技術引進到中國保險行業客戶後,公司組建了專業的容器技術產品研發和實施服務團隊,旨在幫助中國金融行業客戶將容器創新技術應用於企業信息技術支持業務發展的基礎能力改善與提升,成為中國金融保險行業容器技術服務領導品牌。

此外,憑藉多年來在呼叫中心領域的業務經驗與技術積累,睿雲智合率先在業界推出基於開源軟交換平臺FreeSwitch的微服務架構多媒體數字化業務平臺,將語音、視頻、webchat、微信、微博等多種客戶接觸渠道集成,實現客戶統一接入、精準識別、智能路由的CRM策略,並以容器化治理來支持平臺的全應用生命週期管理,顯著提升了數字化業務處理的靈活、高效、彈性、穩定等特性,為幫助傳統企業向“以客戶為中心”的數字化業務轉型提供完美的一站式整體解決方案。

客戶&合作伙伴

Spring Boot Tomcat 容器化部署實踐與總結

原文鏈接:https://mp.weixin.qq.com/s/Vb4VzO7AT9PZ6QPtCe4SLw


分享到:


相關文章: