SpringBoot使用外部tomcat


前言

大家都知道SpringBoot有内置Tomcat的功能,在启动原理中有介绍SpringBoot内置Tomcat的原理。那么如果我们不像使用SpringBoot的内置Tomcat,想用自己的web容器怎么操作。springboot支持默认三大容器,Tomcat,Jetty,Undertow,可以在pom包中通过依赖控制。

ServletWebServerFactoryAutoConfiguration注解上

这里就体现了SpringBoot内置容器有哪些

使用Undertow内置容器

pom

排除tomcat依赖,增加undertow依赖

<!--排除tomcat依赖-->
<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>
<!--使用undertow容器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

使用jetty内置容器

pom

排除tomcat依赖,增加jetty依赖

<!--排除tomcat依赖-->
<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>
<!--使用jetty容器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

如何使用外部容器

  1. 更改打包方式,排除tomcat pom依赖,这时候也不要加一些UnderTow或Jetty的包
  2. 编写Tomcat启动类,找到SpringBoot主程序
  3. 在tomcat容器中运行

pom

排除依赖是为了让ServletWebServerFactoryAutoConfiguration不在自动装配任何一个容器类,但这里是可以不用排除依赖其实也是可以使用外部容器的,从代码中可以看出如果使用了外部servlet会优先使用外部sevlet而不会使用自动装配的sevlet。

<packaging>war</packaging>

<!--排除tomcat依赖-->
<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>

编写Tomcat启动类

实现SpringBootServletInitializer重写configure方法

public class TomcatStartSpringBoot extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(MyApplication.class);
    }
}

装载到tomcat容器

使用外部容器原理

  1. Tomcat是使用SPI机制找到SpringServletContainerInitializer
  2. 在其上标注了注解@HandlesTypes(WebApplicationInitializer.class)
  3. 通过@HandlesTypes找到WebApplicationInitializer的所有实现类注入到onStartup方法的webAppInitializerClasses参数中
  4. 然后执行WebApplicationInitializer实现类中的onStartup方法,初始化ContextLoaderListener和DispatcherServlet和启动Spring

SPI机制SpringServletContainerInitializer

在META-INF/services文件夹中找到javax.servlet.ServletContainerInitializer, 这个文件里面是ServletContainerInitializer的实现类SpringServletContainerInitializer,创建它的实例调用onstartUp

HandlesTypes原理

HandlesTypes是在tomcat–ContextConfig#processServletContainerInitializers方法中注入的,主要过程如下

  1. 通过SPI机制找到所有的ServletContainerInitializer的实现类,放到ServletContainerInitializer集合下
  2. 如果在ServletContainerInitializer实现类上标注了HandlesTypes注解就继续,否则结束。找到之后这里只存储了注解的类名,这时候并没有HandlesTypes注解的接口所实现的类封装起来
  3. 通过ContextConfig#processAnnotationsStream方法中的ClassParser从classes中获取到类然后在checkHandlesTypes方法中封装进initializerClassMap中
  4. 在ContextConfig#webConfig方法中通过context#addServletContainerInitializer方法把initializerClassMap值放到了StandardContext中的initializers中
  5. 在StandardContext#startInternal中调用ServletContainerInitializer#onStart方法

SpringServletContainerInitializer启动原理

上面基本上是Tomcat和servlet的一些原理,下面才开始进入到Spring的一个底层了。

从上面我们可以看出SpringServletContainerInitializer会通过SPI进行运行,而且会把WebApplicationInitializer的所有实现类注入到webAppInitializerClasses方法中,那么我们就可以自定义WebApplicationInitializer实现类进行一些操作了。这里我们就加了一个Tomcat启动类,就是上面加的TomcatStartSpringBoot,在这个启动类里面我们配置类SpringBoot启动类,以便于我们启动Spring容器。

SpringServletContainerInitializer这个类其实也没什么就是把WebApplicationInitializer的实现类的onStartup方法挨个调一遍。

SpringBootServletInitializer启动原理

我们实现的Tomcat启动类就是继承的SpringBootServletInitializer,我们只是重写了config方法,通过SpringServletContainerInitializer发现我们一定会调用SpringBootServletInitializer#onStartup方法

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
    // Logger initialization is deferred in case an ordered
    // LogServletContextInitializer is being used
    this.logger = LogFactory.getLog(getClass());
    // ===========核心方法============
    WebApplicationContext rootAppContext = createRootApplicationContext(
        servletContext);
    if (rootAppContext != null) {
        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");
    }
}

onStart方法中只有createRootApplicationContext比较重要

SpringBootServletInitializer#createRootApplicationContext

  1. 构建SpringApplicationBuilder对象,创建SpringApplication对象
  2. 通过重写configure方法把SpringBoot启动类传到build中,通过build创建的SpringApplication调用run方法,成功来到SpringBoot的启动方法。
protected WebApplicationContext createRootApplicationContext(
      ServletContext servletContext) {
   // new一个SpringApplicationBuilder对象,用其生产SpringApplication
   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启动类传到build中构建SpringApplication对象
   //通过configure()方法扩展
   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");
   // Ensure error pages are registered
   if (this.registerErrorPageFilter) {
      application.addPrimarySources(
            Collections.singleton(ErrorPageFilterConfiguration.class));
   }
   // 运行SpringApplication run方法等价于SpringBoot启动类的SpringApplication.run(MyApplication.class);
   return run(application);
}

总结

外部容器启动无非就是通过SPI和HandlesType找到我们自定义的启动类,通过重写SpringBootServletInitializer#config方法把SpringBoot启动类传到SpringApplication中,通过SpringApplication#run创建容器。


文章作者: dm
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 dm !
评论
 上一篇
SpringBoot自动装配原理以及自定义starter SpringBoot自动装配原理以及自定义starter
前言大家都知道一个Spring项目的搭建是及其繁琐的,需要写很多xml配置文件,集成一个框架进Spring都需要增加一个xml配置文件。即使我们可以使用javaConfig的方式减少xml的配置,其实也是没有更加方便,也是要建立很多的con
2022-05-25
下一篇 
SpringBoot启动原理 SpringBoot启动原理
前言SpringBoot 的启动区别于传统的Spring需要搭建tomcat等相关的容器,SpringBoot默认是通过内置Tomcat启动,只需简单的运行java -jar xxx即可简单的启动一个SpringBoot工程。这里我们深入S
2022-04-17
  目录