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

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

8 人贊了文章

在平時的工作和學習中經常會構建簡單的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:

@SpringBootApplicationpublic class Application extends SpringBootServletInitializer {@Overrideprotected 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:

<packaging>war</packaging>

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:

<dependencies><!-- … --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope></dependency><!-- … --></dependencies>

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 {@Overridepublic void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)throws ServletException { List<WebApplicationInitializer> 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<Class<?>>;為這些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如下:

@Overridepublic 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中:

<plugin><groupId>com.spotify</groupId><artifactId>docker-maven-plugin</artifactId><version>1.1.1</version><configuration> <imageName>wanlinus/file-server</imageName> <!--<imageTags>--> <!--<imageTag>v1</imageTag>--> <!--<tag>v2</tag>--> <!--</imageTags>--> <dockerDirectory>${project.basedir}</dockerDirectory> <!--<dockerHost>http://192.168.111.143:2375</dockerHost>--> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.war</include> </resource> </resources></configuration></plugin>

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

FROM tomcatMAINTAINER wanlinus <wanlinus@qq.com>WORKDIR /dockerCOPY target/file-server-0.0.1-SNAPSHOT.war ./server.warRUN 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.txtEXPOSE 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容器化完成。


推薦閱讀:

SpringMVC複習之SpringMVC 控制器
ApplicationContextAware
Spring(二):IoC控制反轉
Jenny Packham Spring 2012 bridal collection
Spring學習筆記_初識Bean(上)

TAG:SpringBoot | 科技 | Spring |