在平時(shí)的工作和學(xué)習(xí)中經(jīng)常會(huì)構(gòu)建簡(jiǎn)單的web應(yīng)用程序。如果只是HelloWorld級(jí)別的程序,使用傳統(tǒng)的Spring+SpringMVC框架搭建得話會(huì)將大部分的時(shí)間花費(fèi)在搭建框架本身上面,比如引入SpringMVC,配置DispatcheherServlet等。并且這些配置文件都差不多,重復(fù)這些勞動(dòng)似乎意義不大。所以使用Springboot框架來搭建簡(jiǎn)單的應(yīng)用程序顯得十分的便捷和高效。
前兩天在工作中需要一個(gè)用于測(cè)試文件下載的簡(jiǎn)單web程序,條件是使用Tomcat Docker Image作為載體,所以為了方便就使用了SpringBoot框架快速搭建起來。
程序?qū)懗鰜碓诒緳C(jī)能夠正常的跑起來,準(zhǔn)備制作鏡像,但是聞?lì)}就接踵而來了。首先是部署的問題,SpringBoot Web程序默認(rèn)打的是jar包,運(yùn)行時(shí)使用命令 java -jar -Xms128m -Xmx128m xxx.jar,本機(jī)跑的沒問題。但是需求是使用外部的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運(yùn)行有兩步。第一,SpringBoot啟動(dòng)類繼承SpringBootServletInitializer,重寫configure方法。第二,將包管理軟件的打包方式改成war,并將Spring-boot-starter-tomcat設(shè)置為provided。但是,為什么應(yīng)該這么做?
根據(jù)Servlet3.0規(guī)范可知,Web容器啟動(dòng)時(shí)通過ServletContainerInitializer類實(shí)現(xiàn)第三方組件的初始化工作,如注冊(cè)servlet或filter等,每個(gè)框架要是用ServletContainerInitializer就必須在對(duì)應(yīng)的META-INF/services目錄下創(chuàng)建名為javax.servlet.ServletContainerInitializer的文件,文件內(nèi)容指定具體的ServletContainerInitializer實(shí)現(xiàn)類,在SpringMVC框架中為SpringServletContainerInitializer。一般伴隨著ServletContainerInitializer一起使用的還有HandlesTypes注解,通過HandlesTypes可以將感興趣的一些類注入到ServletContainerInitializerde的onStartup方法作為參數(shù)傳入。如下為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)標(biāo)注的所有這個(gè)類型的類都傳入到onStartup方法的Set>;為這些WebApplicationInitializer類型的類創(chuàng)建實(shí)例。
? ? ? ? ? ? ? ? ? ? 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) {
? ? ? ? //為每個(gè)WebApplicationInitializer調(diào)用自己的onStartup()
? ? ? ? initializer.onStartup(servletContext);
? ? }
}
}
SpringBootInitializer繼承WebApplicationInitializer,重寫的onStartup如下:
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
this.logger = LogFactory.getLog(getClass());
? // 調(diào)用自生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);
// 調(diào)用重寫方法,重寫方法傳入SpringBoot啟動(dòng)類
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));
}
//啟動(dòng)應(yīng)用程序,就是啟動(dòng)傳入的SpringBoot程序
return run(application);
}
在程序和Tomcat打通之后需做的就是將war打成一個(gè)Docker鏡像,如果每次都是復(fù)制war包,然后再docker build會(huì)很麻煩,在開源社區(qū)早有了解決方案–docker-maven-plugin,查看Github中的使用方法,將如下內(nèi)容加入pom.xml中:
com.spotify
docker-maven-plugin
1.1.1
? ? wanlinus/file-server
? ? ${project.basedir}
? ?
? ? ? ?
? ? ? ? ? ? /
? ? ? ? ? ? ${project.build.directory}
? ? ? ? ? ? ${project.build.finalName}.war
該配置中有個(gè)標(biāo)簽是用來指定構(gòu)建docker image的Dockerfile的位置,在項(xiàng)目的根目錄下新建一個(gè)Dockerfile,內(nèi)容如下:
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
在本地將會(huì)生成一個(gè)docker image,如果docker沒有運(yùn)行于本地,需要在標(biāo)簽中輸入遠(yuǎn)端地址和docker daemon端口。
最后在終端中運(yùn)行
docker run --rm -p 8080:8080 wanlinus/fileserver
在Tomcat啟動(dòng)后將會(huì)看到Spring Boot程序的啟動(dòng)日志,至此,Spring Boot Tomcat容器化完成。