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

扫码关注公众号:Java 技术驿站

发送:vip
将链接复制到本浏览器,永久解锁本站全部文章

【公众号:Java 技术驿站】 【加作者微信交流技术,拉技术群】

前言

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

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

spring boot外置tomcat实现

项目结构如下:

20191017100467\_1.png

  1. pom 文件如下:

        
        4.0.0
        com.jihegupiao.demo
        spring-boot-war-demo
        0.0.1-SNAPSHOT
        war
    
        
            org.springframework.boot
            spring-boot-starter-parent
            1.5.9.RELEASE
             
        
    
        
            UTF-8
            UTF-8
            1.8
        
    
        
            
                org.springframework.boot
                spring-boot-starter-web
                
                    
                        org.springframework.boot
                        spring-boot-starter-tomcat
                    
                
            
    
            
                org.springframework.boot
                spring-boot-starter-test
                test
            
    
            
                javax.servlet
                javax.servlet-api
                provided
            
        
    
        
    
            spring-boot-war-demo
            
                
                    org.springframework.boot
                    spring-boot-maven-plugin
                
    
                
                    org.apache.maven.plugins
                    maven-war-plugin
                    
                        false
                    
                
    
                
                    org.apache.tomcat.maven
                    tomcat7-maven-plugin
                    2.2
                    
                        http://localhost:8080/manager/text
                        Tomcat7
                        admin
                        admin
                        8082
                        UTF-8
                        /
                        ${basedir}/target/${project.build.finalName}.war
                    
                
            
        
        
  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", "");
            }

    向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启动,后续的故事就和我们之前的分析一样了.这里就不在赘述了.

来源:[]()

赞(0) 打赏
版权归原创作者所有,任何形式的转载请联系博主:daming_90:Java 技术驿站 » spring boot 源码解析16-spring boot外置tomcat部署揭秘

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏