后面的文章中,咱们介绍了Tomcat容器的要害组件和类加载器,然而当初的J2EE开发中更多的是应用SpringBoot内嵌的Tomcat容器,而不是独自装置Tomcat利用。那么Spring是怎么和Tomcat容器进行集成?Spring和Tomcat容器的生命周期是如何同步?本文会具体介绍Spring和Tomcat容器的集成。

SpringBoot与Tomcat

应用SpringBoot搭建一个网页,应该是很多Spring学习者入门的案例。咱们只须要在pom增加Spring的web-starter依赖,并增加对应的Controller,一键启动之后就能够失去一个残缺的Web利用示例。

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-web</artifactId>   <version>2.1.6.RELEASE</version></dependency>
@RestController@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }    @RequestMapping("/")    public String hello(){        return "hello";    }}

既然是一个Web利用,那么该利用必然启动了对应的Servlet容器,常见的Servlet容器有Tomcat/Undertow/jetty/netty等,SpringBoot对这些容器都有集成。本文会重点剖析SpringBoot是如何集成Tomcat容器的。

如何判断是不是Web利用

咱们晓得SpringBoot不肯定以Web利用的模式运行,还能够以桌面程序的模式运行,那么SpringBoot在利用中如何判断利用是不是一个Web应用程序,是不是须要启动Tomcat容器的呢?

Spring容器在容器启动的时候,会调用WebApplicationType.deduceFromClasspath()办法来推断以后的应用程序类型,从办法名字就能够看出,该办法是通过以后我的项目中的类来判断是不是Web我的项目的。以下为该办法的源码,当咱们在我的项目中增加了spring-boot-starter-web的依赖之后,我的项目门路中会蕴含webMvc的类,对应的Spring利用也会被辨认为Web利用。

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" };private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";static WebApplicationType deduceFromClasspath() {    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {        return WebApplicationType.REACTIVE;    }    for (String className : SERVLET_INDICATOR_CLASSES) {        if (!ClassUtils.isPresent(className, null)) {            return WebApplicationType.NONE;        }    }    return WebApplicationType.SERVLET;}

依据利用类型创立利用

通过我的项目中蕴含类的类型,Spring能够判断出以后利用的类型,之后Spring就须要依据利用类型去创立对应的ApplicationContext。从上面的程序中能够看进去,对于咱们关注的一般web利用,Spring会创立一个AnnotationConfigServletWebServerApplicationContext

ApplicationContextFactory DEFAULT = (webApplicationType) -> {    try {        switch (webApplicationType) {            case SERVLET:                return new AnnotationConfigServletWebServerApplicationContext();            case REACTIVE:                return new AnnotationConfigReactiveWebServerApplicationContext();            default:                return new AnnotationConfigApplicationContext();            }        }        catch (Exception ex) {            throw new IllegalStateException("Unable create a default ApplicationContext instance, "            + "you may need a custom ApplicationContextFactory", ex);        }};

AnnotationConfigServletWebServerApplicationContext是Web利用的Spring容器,咱们能够推断,这个ApplicationContext容器中必然蕴含了servlet容器的初始化。去查看容器初始化的源码能够发现,在容器Refresh阶段会初始化WebServer,源码如下:

@Overrideprotected void onRefresh() {    super.onRefresh();    try {        // Spring容器Refresh阶段创立WebServer        createWebServer();     }    catch (Throwable ex) {        throw new ApplicationContextException("Unable to start web server", ex);    }}private void createWebServer() {        WebServer webServer = this.webServer;        ServletContext servletContext = getServletContext();        // 没有初始化好的WebServer就须要初始化一个        if (webServer == null && servletContext == null) {            StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");            // 获取ServletWebServerFactory,对于Tomcat来说获取到的就是TomcatServletWebServerFactory            ServletWebServerFactory factory = getWebServerFactory();            createWebServer.tag("factory", factory.getClass().toString());            // 创立Tomcat容器的WebServer            this.webServer = factory.getWebServer(getSelfInitializer());            createWebServer.end();            getBeanFactory().registerSingleton("webServerGracefulShutdown",                    new WebServerGracefulShutdownLifecycle(this.webServer));            getBeanFactory().registerSingleton("webServerStartStop",                    new WebServerStartStopLifecycle(this, this.webServer));        }        else if (servletContext != null) {            try {                getSelfInitializer().onStartup(servletContext);            }            catch (ServletException ex) {                throw new ApplicationContextException("Cannot initialize servlet context", ex);            }        }        initPropertySources();    }

Tomcat的初始化

通过下面的内容,咱们晓得SpringBoot会在启动的时候判断是不是Web利用并创立对应类型的Spring容器,对于Web利用会创立Web类型的ApplicationContext。 在Spring容器启动的时候会初始化WebServer,也就是初始化Tomcat容器。本节咱们会剖析Tomcat容器初始化源码的各个步骤。

获取ServletWebServerFactory

初始化Tomcat容器的过程中,第一步是获取创立Tomcat WebServer的工厂类TomcatServletWebServerFactory,剖析源码可知,Spring是间接通过Bean的类型从Spring容器中获取ServletWebServerFactory的,所以Tomcat容器类型的SpringBoot应该在启动时向容器中注册TomcatServletWebServerFactory的实例作为一个Bean。

// 获取ServletWebServerFactory要害代码factory = getWebServerFactory();// 要害代码波及的函数protected ServletWebServerFactory getWebServerFactory() {    // Use bean names so that we don't consider the hierarchy    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);    if (beanNames.length == 0) {        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "                + "ServletWebServerFactory bean.");    }    if (beanNames.length > 1) {        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "                + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));    }    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);}

创立WebServer的实例

拿到用于创立WebServer的ServletWebServerFactory,咱们就能够开始着手创立WebServer了,创立WebServer的要害代码如下所示。

// 创立WebServer的实例要害代码this.webServer = factory.getWebServer(getSelfInitializer());

创立WebServer的第一步是拿到创立时须要的参数,这个参数的类型是ServletContextInitializer,ServletContextInitializer的作用是用于初始化ServletContext,接口源码如下,从接口的正文中咱们就能够看到,这个参数能够用于配置servlet容器的filters,listeners等信息。

@FunctionalInterfacepublic interface ServletContextInitializer {    /**     * Configure the given {@link ServletContext} with any servlets, filters, listeners     * context-params and attributes necessary for initialization.     * @param servletContext the {@code ServletContext} to initialize     * @throws ServletException if any call against the given {@code ServletContext}     * throws a {@code ServletException}     */    void onStartup(ServletContext servletContext) throws ServletException;}

Spring是通过getSelfInitializer()办法来获取初始化参数,查看getSelfInitializer()办法,能够发现该办法实现了如下性能:

  1. 绑定SpringBoot应用程序和ServletContext;
  2. 向SpringBoot注册ServletContext,Socpe为Application级别;
  3. 向SpringBoot上下文环境注册ServletContext环境相干的Bean;
  4. 获取容器中所有的ServletContextInitializer,顺次解决ServletContext。
private ServletContextInitializer getSelfInitializer() {    return this::selfInitialize;}private void selfInitialize(ServletContext servletContext) throws ServletException {    prepareWebApplicationContext(servletContext);    registerApplicationScope(servletContext);    WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);    for (ServletContextInitializer beans : getServletContextInitializerBeans()) {        beans.onStartup(servletContext);    }}

获取到用于创立WebServer的参数之后,Spring就会调用工厂办法去创立Tomcat对应的WebServer。

    @Override    public WebServer getWebServer(ServletContextInitializer... initializers) {        if (this.disableMBeanRegistry) {            Registry.disableRegistry();        }        Tomcat tomcat = new Tomcat();        File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");        tomcat.setBaseDir(baseDir.getAbsolutePath());        Connector connector = new Connector(this.protocol);        connector.setThrowOnFailure(true);        tomcat.getService().addConnector(connector);        customizeConnector(connector);        tomcat.setConnector(connector);        tomcat.getHost().setAutoDeploy(false);        configureEngine(tomcat.getEngine());        for (Connector additionalConnector : this.additionalTomcatConnectors) {            tomcat.getService().addConnector(additionalConnector);        }        prepareContext(tomcat.getHost(), initializers);        return getTomcatWebServer(tomcat);    }

Tomcat生命周期

咱们在应用基于Spring MVC利用框架,只须要启动/敞开Spring利用,就能够同步启动/敞开Tomcat容器,那么Spring是如何做到的呢?从上面初始化Web容器的代码能够看到,Spring容器会注册两个和WebServer容器相干的生命周期Bean:

  1. 容器的优雅敞开Bea——webServerGracefulShutdown。
  2. 容器的生命周期治理的Bean——webServerStartStop
    getBeanFactory().registerSingleton("webServerGracefulShutdown",                    new WebServerGracefulShutdownLifecycle(this.webServer));    getBeanFactory().registerSingleton("webServerStartStop",                    new WebServerStartStopLifecycle(this, this.webServer));

Tomcat容器优雅敞开

这是SpringBoot在最新的2.X.X版本中新增的优雅停机性能, 优雅停机指的是Java我的项目在停机时须要做好断后工作。如果间接应用kill -9 形式暴力的将我的项目停掉,可能会导致失常解决的申请、定时工作、RMI、登记注册核心等呈现数据不统一问题。如何解决优雅停机呢?大抵须要解决如下问题:

  • 首先要确保不会再有新的申请进来,所以须要设置一个流量挡板
  • 保障失常解决已进来的申请线程,能够通过计数形式记录我的项目中的申请数量
  • 如果波及到注册核心,则须要在第一步完结后登记注册核心
  • 进行我的项目中的定时工作
  • 进行线程池
  • 敞开其余须要敞开资源等等等

SpringBoot优雅停机呈现之前,个别须要通过自研形式来保障优雅停机。我也见过有项目组应用 kill -9 或者执行 shutdown脚本间接进行运行的我的项目,当然这种形式不够优雅。

Spring提供Tomcat优雅敞开的外围类是WebServerGracefulShutdownLifecycle,能够期待用户的所有申请解决实现之后再敞开Tomcat容器,咱们查看WebServerGracefulShutdownLifecycle的的关机要害源码如下:

    // WebServerGracefulShutdownLifecycle停机源码    @Override    public void stop(Runnable callback) {        this.running = false;        this.webServer.shutDownGracefully((result) -> callback.run());    }    // tomcat web server shutDownGracefully源码     @Override     public void shutDownGracefully(GracefulShutdownCallback callback) {          if (this.gracefulShutdown == null) {               callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);               return;          }          this.gracefulShutdown.shutDownGracefully(callback);     }

此处呈现了优雅敞开的工具类GracefulShutdown,Tomcat容器的GracefulShutdown源码如下所示,能够看到优雅敞开分为以下步骤:

  1. 敞开Tomcat容器的所有的连接器,连接器敞开之后会进行承受新的申请。
  2. 轮询所有的Context容器,期待这些容器中的申请被解决实现。
  3. 如果强行退出,那么就不期待所有容器中的申请解决实现。
  4. 回调优雅敞开的后果,有三种敞开后果:REQUESTS_ACTIVE有沉闷申请的状况下强行敞开,IDLE所有申请实现之后敞开,IMMEDIATE没有任何期待立刻敞开容器。
final class GracefulShutdown {    void shutDownGracefully(GracefulShutdownCallback callback) {        logger.info("Commencing graceful shutdown. Waiting for active requests to complete");        new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();    }    private void doShutdown(GracefulShutdownCallback callback) {        // 敞开Tomcat的所有的连接器,不承受新的申请        List<Connector> connectors = getConnectors();        connectors.forEach(this::close);        try {            for (Container host : this.tomcat.getEngine().findChildren()) {                // 轮询所有的Context容器                for (Container context : host.findChildren()) {                    // 判断容器中的所有申请是不是曾经完结。                    while (isActive(context)) {                        // 强行退出的状况下不期待所有申请解决实现                        if (this.aborted) {                            logger.info("Graceful shutdown aborted with one or more requests still active");                            callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);                            return;                        }                        Thread.sleep(50);                    }                }            }        }        catch (InterruptedException ex) {            Thread.currentThread().interrupt();        }        logger.info("Graceful shutdown complete");        callback.shutdownComplete(GracefulShutdownResult.IDLE);    }}
Spring 容器是怎么晓得敞开并进行回调的呢?本处只介绍kill -15工作原理:Spring容器在启动的时候会向JVM注册销毁回调办法,JVM在收到kill -15之后不会间接退出,而是会一一调用这些回调办法,而后Spring会在这些回调办法中进行优雅敞开,比方从注册核心删除注册信息,优雅敞开Tomcat等等。

我是御狐神,欢送大家关注我的微信公众号:wzm2zsd

本文最先公布至微信公众号,版权所有,禁止转载!