spring boot 源码解析16-spring boot外置tomcat部署揭秘

前言

spring boot 内嵌了一个servlet 容器,但是有的时候,可以还是希望将spring boot 应用部署到tomcat 中,通过war包的方式,那么该如何实现呢? 原理是什么呢? 我们从以下2点来说明:

  1. spring boot外置tomcat实现
  2. spring boot外置tomcat分析

spring boot外置tomcat实现

项目结构如下:

《spring boot 源码解析16-spring boot外置tomcat部署揭秘》

  1. pom 文件如下:

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.jihegupiao.demo</groupId>
    <artifactId>spring-boot-war-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    
    <build>
    
        <finalName>spring-boot-war-demo</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
    
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
    
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <url>http://localhost:8080/manager/text</url>
                    <server>Tomcat7</server>
                    <username>admin</username>
                    <password>admin</password>
                    <port>8082</port>
                    <uriEncoding>UTF-8</uriEncoding>
                    <path>/</path>
                    <warFile>${basedir}/target/${project.build.finalName}.war</warFile>
                </configuration>
            </plugin>
        </plugins>
    </build>
    </project>
  2. 将原先的启动类修改为如下:

    package com.example.demo;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.builder.SpringApplicationBuilder;
    import org.springframework.boot.web.support.SpringBootServletInitializer;
    @SpringBootApplication
    public class ServletInitializer extends SpringBootServletInitializer {
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(ServletInitializer.class);
    }
    
    public static void main(String[] args) {
        SpringApplication.run(ServletInitializer.class, args);
    }
    }
    

    其中configure方法 指定了启动类

  3. 测试controller如下:

    package com.example.demo.controller;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    @Controller
    public class TestController {
    
    
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    @ResponseBody
    public String test() {
        return "hi";
    }
    }
    
  4. 测试一下吧,执行 mvn:clean install tomcat7:run, 访问http://127.0.0.1:8082/test,如果正常的话,返回 hi.

spring boot外置tomcat分析

  1. 上篇文章有提到,spring 4 通过 servlet3.0 规范 实现了 spring mvc 零配置,其关键的核心是SpringServletContainerInitializer,其为加载类路径下所有WebApplicationInitializer的实现,此时有如下实现:

    • JerseyWebApplicationInitializer
    • ServletInitializer(我们的启动类继承了SpringBootServletInitializer,其实现了WebApplicationInitializer,因此,该类会自动被加载)

    JerseyWebApplicationInitializer#onStartup 代码如下:

    public void onStartup(ServletContext servletContext) throws ServletException {
            // We need to switch *off* the Jersey WebApplicationInitializer because it
            // will try and register a ContextLoaderListener which we don't need
            servletContext.setInitParameter("contextConfigLocation", "<NONE>");
        }

    向ServletContext 添加了一个初始化参数–>key:contextConfigLocation,value:

    SpringBootServletInitializer#onStartup,其代码如下:

    public void onStartup(ServletContext servletContext) throws ServletException {
        // Logger initialization is deferred in case a ordered
        // LogServletContextInitializer is being used
        // 1. 初始化log
        this.logger = LogFactory.getLog(getClass());
        // 2.创建WebApplicationContext
        WebApplicationContext rootAppContext = createRootApplicationContext(
                servletContext);
        if (rootAppContext != null) {
            // 3. 添加ContextLoaderListener,ContextLoaderListener 初始化时没有做任何事,
            servletContext.addListener(new ContextLoaderListener(rootAppContext) {
                @Override
                public void contextInitialized(ServletContextEvent event) {
                    // no-op because the application context is already initialized
                }
            });
        }
        else {
            this.logger.debug("No ContextLoaderListener registered, as "
                    + "createRootApplicationContext() did not "
                    + "return an application context");
        }
    }

    2件事:

    1. 初始化logger
    2. 调用createRootApplicationContext,创建WebApplicationContext,如果创建成功,则添加一个ContextLoaderListener,该Listener在contextInitialized中没有做任何事,因为ApplicationContext在创建的过程中已经初始化了.否则,打印日志. createRootApplicationContext代码如下:
    protected WebApplicationContext createRootApplicationContext(
            ServletContext servletContext) {
        // 1. 初始化SpringApplicationBuilder
        SpringApplicationBuilder builder = createSpringApplicationBuilder();
        // 2. 初始化StandardServletEnvironment
        StandardServletEnvironment environment = new StandardServletEnvironment();
        environment.initPropertySources(servletContext, null);
        builder.environment(environment);
        // 3. 设置启动类为当前类
        builder.main(getClass());
        // 4. 如果存在父容器,则添加一个ParentContextApplicationContextInitializer
        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));
        }
        // 5. 添加ServletContextApplicationContextInitializer
        builder.initializers(
                new ServletContextApplicationContextInitializer(servletContext));
        // 6. 设置contextClass 为 AnnotationConfigEmbeddedWebApplicationContext
        builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
        // 7. 个性化配置
        builder = configure(builder);
        SpringApplication application = builder.build();
        // 如果sources 为空,并且启动类有@Configuration 注解,则添加当前类到sources中
        if (application.getSources().isEmpty() && AnnotationUtils
                .findAnnotation(getClass(), Configuration.class) != null) {
            application.getSources().add(getClass());
        }
        Assert.state(!application.getSources().isEmpty(),
                "No SpringApplication sources have been defined. Either override the "
                        + "configure method or add an @Configuration annotation");
        // Ensure error pages are registered
        if (this.registerErrorPageFilter) {
            // 8. 如果registerErrorPageFilter 为true,默认为true,则向sources中添加ErrorPageFilterConfiguration
            application.getSources().add(ErrorPageFilterConfiguration.class);
        }
        // 9. 启动
        return run(application);
    }

    10件事:

    1. 创建SpringApplicationBuilder.代码如下:

      protected SpringApplicationBuilder createSpringApplicationBuilder() {
      return new SpringApplicationBuilder();
      }
    2. 实例化StandardServletEnvironment. StandardServletEnvironment初始化的过程我们之前的文章有分析过,其构造器会向其内部持有的propertySources 添加如下Source:

      1. 名为servletConfigInitParams 的StubPropertySource
      2. 名为servletContextInitParams 的StubPropertySource
      3. 如果jndi存在的话,则添加名为jndiProperties 的StubPropertySource,这个默认是会添加的
      4. 名为systemProperties,值为System#getProperties的返回值 的MapPropertySource
      5. 名为systemEnvironment,值为System#getenv的返回值 的SystemEnvironmentPropertySource

      接下来调用StandardServletEnvironment#initPropertySources进行初始化servletConfigInitParams, servletContextInitParams 所对应的Source.代码如下:

      @Override
      public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) {
      WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
      }

      调用

      public static void initServletPropertySources(
          MutablePropertySources propertySources, ServletContext servletContext, ServletConfig servletConfig) {
      
      Assert.notNull(propertySources, "'propertySources' must not be null");
      if (servletContext != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) &&
              propertySources.get(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) {
          propertySources.replace(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
                  new ServletContextPropertySource(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME, servletContext));
      }
      if (servletConfig != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) &&
              propertySources.get(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) {
          propertySources.replace(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
                  new ServletConfigPropertySource(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, servletConfig));
      }
      }

      注意,这里由于ServletConfig等于null,因此最终StandardServletEnvironment持有了servletContext.

    3. 设置启动类为当前类,也就是我们项目中的ServletInitializer.class

    4. 调用getExistingRootWebApplicationContext,获得父容器,如果存在,则添加一个ParentContextApplicationContextInitializer.代码如下:

      private ApplicationContext getExistingRootWebApplicationContext(
          ServletContext servletContext) {
      Object context = servletContext.getAttribute(
              WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
      if (context instanceof ApplicationContext) {
          return (ApplicationContext) context;
      }
      return null;
      }

      这里是获取不到的

    5. 添加ServletContextApplicationContextInitializer,代码如下:

      builder.initializers(
              new ServletContextApplicationContextInitializer(servletContext));

      其在SpringApplication#run中最终会调用其initialize方法,代码如下:

      public void initialize(ConfigurableWebApplicationContext applicationContext) {
      applicationContext.setServletContext(this.servletContext);
      if (this.addApplicationContextAttribute) {
          this.servletContext.setAttribute(
                  WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
                  applicationContext);
      }
      }
      1. 为ConfigurableWebApplicationContext也就是SpringApplication所持有的设置ServletContext
      2. 如果addApplicationContextAttribute(是否向servletContext中保存applicationContext)为true,则进行添加,由于我们在实例化ServletContextApplicationContextInitializer时传入的false,因此这步是不会执行的.

      问题: 我们知道,在spring mvc 中, applicationContext 是需要保存在servletContext中的,此时我们就可以调用WebApplicationContextUtils#getWebApplicationContext,从而在service层获得WebApplicationContext的实例,那么在外置tomcat中,是何时设置的呢?

      在SpringApplication的启动过程中,最终会调用 AbstractApplicationContext#refresh,在该方法中,调用了EmbeddedWebApplicationContext#onRefresh,最终调用了createEmbeddedServletContainer,代码如下:

      private void createEmbeddedServletContainer() {
      EmbeddedServletContainer localContainer = this.embeddedServletContainer;
      // 1. 获得ServletContext
      ServletContext localServletContext = getServletContext();
      if (localContainer == null && localServletContext == null) { // 2 内置Servlet容器和ServletContext都还没初始化的时候执行
          // 2.1 获取自动加载的工厂
          EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
          // 2.2 获取Servlet初始化器并创建Servlet容器,依次调用Servlet初始化器中的onStartup方法
          this.embeddedServletContainer = containerFactory
                  .getEmbeddedServletContainer(getSelfInitializer());
      }
      else if (localServletContext != null) { // 3. 内置Servlet容器已经初始化但是ServletContext还没初始化,则进行初始化.一般不会到这里
          try {
              getSelfInitializer().onStartup(localServletContext);
          }
          catch (ServletException ex) {
              throw new ApplicationContextException("Cannot initialize servlet context",
                      ex);
          }
      }
      // 4. 初始化PropertySources
      initPropertySources();
      }
      1. 获得ServletContext,
      2. 如果localContainer等于null并且ServletContext等于null,则意味着是内置容器的情况,这时只需获得嵌入容器就行了,在调用EmbeddedServletContainerFactory#getEmbeddedServletContainer时将ServletContextInitializer传入了进去,其onStartup方法调用了EmbeddedWebApplicationContext#selfInitialize,一般情况下,此时调用的是TomcatEmbeddedServletContainerFactory#getEmbeddedServletContainer,经过层层调用,最终实例化了TomcatStarter,其实现了ServletContainerInitializer接口,当容器初始化的时候,会调用其onStartup方法,而在TomcatStarter的实现中,会依次调用其内部持有的ServletContextInitializer的onStartup进行处理,代码如下:

        for (ServletContextInitializer initializer : this.initializers) {
                initializer.onStartup(servletContext);
            }

        因此,也就会调用到之前在EmbeddedServletContainerFactory#getEmbeddedServletContainer时实例化的ServletContextInitializer,也就会调用到EmbeddedWebApplicationContext#selfInitialize,代码如下:

        private void selfInitialize(ServletContext servletContext) throws ServletException {
        prepareEmbeddedWebApplicationContext(servletContext);
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();
        ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(
                beanFactory);
        // 注册了各种属于web的scope
        WebApplicationContextUtils.registerWebApplicationScopes(beanFactory,
                getServletContext());
        existingScopes.restore();
        // 注册了web特定的contextParameters,contextAttributes等
        WebApplicationContextUtils.registerEnvironmentBeans(beanFactory,
                getServletContext());
        for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
            beans.onStartup(servletContext); // servlet、filter和listener都会注册到ServletContext上
        }
        }
        

        其中 prepareEmbeddedWebApplicationContext方法中有

        servletContext.setAttribute(
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);

        从而向servletContext中保存了自己.

      3. 否则,就是外置tomcat的情况(对于当前情况,localContainer是等于Null的,因为要进行创建,而ServletContext是在ServletContextApplicationContextInitializer#initialize中赋值的).此时会最终调用selfInitialize方法.接下来同样也会调用prepareEmbeddedWebApplicationContext方法,在servletContext中保存了自己(同第2步)

    6. 设置contextClass 为 AnnotationConfigEmbeddedWebApplicationContext

    7. 个性化配置,这里我们复写了该方法,如下:

      @Override
      protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(ServletInitializer.class);
      }
      
    8. 构建出SpringApplication,如果SpringApplication 中的sources 为空,并且启动类有@Configuration 注解,则添加当前类到sources中,对于当前,由于我们在第7步已经加入了ServletInitializer.class,因此这步是不会执行的.
    9. 如果registerErrorPageFilter,默认为true,则向sources中添加ErrorPageFilterConfiguration. 在该类中声明了ErrorPageFilter.代码如下:

      @Bean
      public ErrorPageFilter errorPageFilter() {
      return new ErrorPageFilter();
      }

      是一个Filter,关于这个的作用我们在后续的文章进行分析

    10. 调用SpringApplication#run启动,后续的故事就和我们之前的分析一样了.这里就不在赘述了.
    原文作者:Spring Boot
    原文地址: https://blog.csdn.net/qq_26000415/article/details/78981592
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞