在平时的工作和学习中经常会构建简单的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容器化完成。