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容器化完成。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,679评论 6 342
  • 最近的国创项目需要做一个交易平台app ,在这里把遇到的东西记录下来。 首先找到了几个资料,如下: 1 Andro...
    追忆__阅读 436评论 0 0
  • 亲爱的老婆: 如果给昨天晚上我们聊天下一个结论,应该算两个相爱的人,在夜深人静的时候,从历史和现实的角度,深刻的分...
    晓晓石头阅读 246评论 0 0
  • 沿着雨点探上 太阳光熄掉前的阴影 半响,半晌 孙陆辰 于2018.4.
    孙陆辰阅读 154评论 0 0