关于java:学习Tomcat七之Spring内嵌Tomcat

47次阅读

共计 10242 个字符,预计需要花费 26 分钟才能阅读完成。

后面的文章中,咱们介绍了 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
@SpringBootApplication
public 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,源码如下:

@Override
protected 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 等信息。

@FunctionalInterface
public 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

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

正文完
 0