Springboot内嵌的Tomcat,你了解多少呢?

1

Springboot简介

相信大多数开发者对Springboot比较熟悉了,它能够快速地创建一个spring应用,能够完全摒弃XML的配置方式,并且内嵌了Tomcat、Jetty这样的Servlet容器,即无需再将应用打包成war部署。

在Springboot之前,部署一个应用如下:

而现在,由于Springboot内嵌了Servlet容器,于是可以将应用打包成jar,直接运行一个jar包就能启动一个web服务。

Springboot是如何做到的呢?接下来进入今天的正题。

2

Tomcat-embed

Springboot能够将Tomcat内嵌,是因为Tomcat提供了一套JavaAPI,能够通过Tomcat tomcat = new Tomcat()来创建一个Tomcat容器。

只需要引入Maven依赖

<dependency>  <groupId>org.apache.tomcat.embed</groupId>  <artifactId>tomcat-embed-core</artifactId>  <version>${tomcat.version}</version></dependency>

接下来我们通过Springboot源码来看看,spring是如何使用这套API与自身结合的

3

Springboot源码解读

首先,任意一个Springboot应用,都有一个main()函数作为应用的启动方法,里面调用了SpringApplication.run(MyApplication.class, args),我们就从这个run()开始,解密spring容器如何启动Tomcat。

这个run()的实现代码如下,这里去掉了一些与主线逻辑无关的代码

/**

* Run the Spring application, creating and refreshing a new

* {@linkApplicationContext

}.

@param

 args the application arguments (usually passed from a Java main method)

@return a running {@link

 ApplicationContext}

*/
public

 ConfigurableApplicationContext run(String... args) {

  ConfigurableApplicationContext context = null

;

  // 创建spring容器对象 ApplicationContext

  context = createApplicationContext();

  // 做一些初始化容器之前的预备操作

  prepareContext(context, environment, listeners, applicationArguments, printedBanner);

  // 启动spring容器核心方法,包括启动tomcat

  refreshContext(context);

  // 做一些容器启动之后的操作(当前这个方法是空的)

  afterRefresh(context, applicationArguments);

  return

 context;

}

看过Spring源码的同学应该清楚,启动spring容器的核心方法就是refresh(),而这里的方法名叫refreshContext(context),不过是springboot对refresh()的封装罢了,所以内部依然是调用的refresh(),于是我们来到这个核心方法内部。

@Overridepublic void refresh() throws BeansException, IllegalStateException 

{

  synchronized (this

.startupShutdownMonitor) {

    // 在容器启动之前做一些准备操作

    prepareRefresh();

    // 通知子类去刷新实例工厂

    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

    // 初始化当前容器中的实例工厂

    prepareBeanFactory(beanFactory);

    try

 {

      // 允许容器中的基类对实例工厂进行后置处理

      postProcessBeanFactory(beanFactory);

      // 调用实例工厂中的后置处理器

      invokeBeanFactoryPostProcessors(beanFactory);

      // 注册实例的后置处理器,在创建实例时进行拦截

      registerBeanPostProcessors(beanFactory);

      // 初始化消息资源

      initMessageSource();

      // 初始化容器中的事件处理器

      initApplicationEventMulticaster();

      // 初始化一些在特定容器中特殊的实例

      onRefresh();

      // 检查监听器的实例并注册它们

      registerListeners();

      // 实例化所有非懒加载的实例

      finishBeanFactoryInitialization(beanFactory);

      // 最后一步:发布一些响应事件

      finishRefresh();

    }

    catch

 (BeansException ex) {

      ...

    }

    finally

 {

      ...

    }

  }

}

当这个方法调用完,spring容器就基本完成了初始化过程,tomcat当然也是在这个方法内部完成的创建。

创建WebServer容器

Springboot内嵌的各种web容器实例,都是在onRefresh()中进行创建的。

查看方法实现可以发现这个方法是个空方法

protected void onRefresh() throws BeansException 

{

  // For subclasses: do nothing by default.}

但是其子类的都实现了这个方法,子类列表如下:

因为Tomcat是一个Servlet容器,所以我们直接看ServletWebServerApplicationContext这个类中是如何实现的

进入这个类中,该方法的实现代码比较容易看懂,重点在createWebServer(),这里同样只保留了主要代码:

@Overrideprotected void onRefresh() 

{

  super

.onRefresh();

  createWebServer();

}

private void createWebServer() 

{

  WebServer webServer = this

.webServer;

  ServletContext servletContext = getServletContext();

  if (webServer == null && servletContext == null

) {

    // 通过工厂创建WebServer实例

    ServletWebServerFactory factory = getWebServerFactory();

    this

.webServer = factory.getWebServer(getSelfInitializer());

  }

}

既然这里通过工厂方法创建对象,肯定对象类型就不止一种,之前也说到springboot内嵌的web容器不止是tomcat,为了印证这个事情,我们直接看factory.getWebServer()方法的实现

其中,有Jetty、Tomcat还有Undertow的实现;看到这里实际上我们已经找到tomcat实例创建的位置了

创建Tomcat实例

进入TomcatServletWebServerFactory的getWebServer()中,发现这里调用的就是tomcat-embed提供的API了

@Overridepublic WebServer getWebServer(ServletContextInitializer... initializers) 

{

  Tomcat tomcat = new

 Tomcat();

  // 1.创建一个tomcat临时文件路径  File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"

);

  tomcat.setBaseDir(baseDir.getAbsolutePath());

  // 2.创建连接协议,默认使用HTTP1.1协议,NIO网络模型  Connector connector = new Connector(this

.protocol);

  tomcat.getService().addConnector(connector);

  customizeConnector(connector);

  tomcat.setConnector(connector);

  // 3.创建主机,并关闭热部署  tomcat.getHost().setAutoDeploy(false

);

  // 4.配置引擎

  configureEngine(tomcat.getEngine());

  for (Connector additionalConnector : this

.additionalTomcatConnectors) {

    tomcat.getService().addConnector(additionalConnector);

  }

  // 5.初始化TomcatEmbeddedContext

  prepareContext(tomcat.getHost(), initializers);

  // 6.启动tomcat并返回TomcatWebServer对象  return

 getTomcatWebServer(tomcat);

}

前面1-5步都是在配置Tomcat,而完成Tomcat是在第6步getTomcatWebServer(tomcat)完成的,我们接着进去看看

getTomcatWebServer(tomcat)方法返回一个TomcatWebServer对象

TomcatWebServer对象是springboot对Tomcat对象的封装,内部存了tomcat实例的引用,这里执行的是TomcatWebServer的构造方法,

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) 

{

  return new TomcatWebServer(tomcat, getPort() >= 0

);

}

构造方法内部如下

public TomcatWebServer(Tomcat tomcat, boolean autoStart) 

{

  Assert.notNull(tomcat, "Tomcat Server must not be null"

);

  this

.tomcat = tomcat;

  this

.autoStart = autoStart;

  initialize();

}

我们主要来看initialize(),在这个方法内部会调用tomcat.start()

private void initialize() throws WebServerException 

{

  synchronized (this

.monitor) {

    try

 {

      // 给Engine命名

      addInstanceIdToEngineName();

      // 获取Host中的Context

      Context context = findContext();

      // 绑定Context的生命周期监听器      context.addLifecycleListener((event

) -> {

        if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event

.getType())) {

          removeServiceConnectors();

        }

      });

      // 启动tomcat,触发初始化监听器      this

.tomcat.start();

      // 启动过程中子线程的异常从主线程抛出

      rethrowDeferredStartupExceptions();

      // 给当前Context绑定类加载器

      ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());

      // tomcat的所有线程都是守护线程,这里启动一个阻塞的非守护线程来确保tomcat能及时停止

      startDaemonAwaitThread();

    }

    catch

 (Exception ex) {

      ...

    }

  }

}

要注意的是这个方法启动的是tomcat工作需要的一些守护线程,并非是web服务;我们可以来看看start()的定义

/**

 * Prepare for the beginning of active use of the public methods other than

 * property getters/setters and life cycle methods of this component. This

 * method should be called before any of the public methods other than

 * property getters/setters and life cycle methods of this component are

 * utilized. The following {@link

 LifecycleEvent}s will be fired in the

 * following order:

 *

 *

  • BEFORE_START_EVENT: At the beginning of the method. It is as this
  •  * point the state transitions to

     * {@link

     LifecycleState#STARTING_PREP}.

     *

  • START_EVENT: During the method once it is safe to call start() for
  •  * any child components. It is at this point that the

     * state transitions to {@link

     LifecycleState#STARTING}

     * and that the public methods other than property

     * getters/setters and life cycle methods may be

     * used.

     *

  • AFTER_START_EVENT: At the end of the method, immediately before it
  •  * returns. It is at this point that the state

     * transitions to {@link

     LifecycleState#STARTED}.

     *

     *

     *

     * @exception

     LifecycleException if this component detects a fatal error

     * that prevents this component from being used

     */
    public void start() throws LifecycleException;

    启动Web服务

    真正完成springboot启动的方法,依然是由TomcatWebServer这个类完成的,这个类封装了控制整个web服务声明周期的方法,比如initialize(), start(), stop()等等,而这个start()显然就是web服务启动的方法了。

    @Overridepublic void start() throws WebServerException 

    {

      synchronized (this

    .monitor) {

        Connector connector = this

    .tomcat.getConnector();

        if (connector != null && this

    .autoStart) {

          performDeferredLoadOnStartup();

        }

        logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path "        + getContextPath() + ""

    );

      }

    }

    看到这个方法里的这行log,大家应该很熟悉了,说明最终启动web容器的函数就是performDeferredLoadOnStartup(),

    performDeferredLoadOnStartup()方法内部逻辑如下

    private void performDeferredLoadOnStartup() 

    {

      for (Container child : this

    .tomcat.getHost().findChildren()) {

        if (child instanceof

     TomcatEmbeddedContext) {

          ((TomcatEmbeddedContext) child).deferredLoadOnStartup();

        }

      }

    }

    public void deferredLoadOnStartup() throws LifecycleException 

    {

      doWithThreadContextClassLoader(getLoader().getClassLoader(),

          () -> getLoadOnStartupWrappers(findChildren()).forEach(this

    ::load));

    }

    getLoadOnStartupWrappers()负责将Wrapper按照loadOnStartup的优先级进行重排序,这里的主要逻辑就是按照Wrapper的优先级,依次调用wrapper.load(),然后打印"Tomcat started on port …"完成web容器启动。

    最后,还有一个问题未解答,就是start()是何时调用的?

    start()何时调用

    要解答这个问题,可以通过观察springboot的启动日志,看"Tomcat started on port …"这行日志是何时打印的。

    按照这个方法,我们不难发现,这行日志仍然是在spring容器的refresh()里打印出来的,继续深入最终是锁定了refresh()里的最后一步,也就是finishRefresh()

    ServletWebServerApplicationContext重写了finishRefresh()

    @Overrideprotected void finishRefresh() 

    {

      super

    .finishRefresh();

      WebServer webServer = startWebServer();

      if (webServer != null

    ) {

        publishEvent(new ServletWebServerInitializedEvent(webServer, this

    ));

      }

    }

    private WebServer startWebServer() 

    {

      WebServer webServer = this

    .webServer;

      if (webServer != null

    ) {

        webServer.start();

      }

      return

     webServer;

    }

    上面方法中的WebServer是一个接口,里面有三个方法start(), stop(), getPort() TomcatWebServer对其进行了实现,其中的start()就是在这里调用的,这里也就是最终启动web服务的地方。

    至此,Springboot内嵌web服务器的秘密解开了。

    4

    总结

    Spring框架基于接口的设计模式,使得整个框架具有良好的扩展性。我们再来回顾下spring是如何整合tomcat的。

    首先,通过继承AbstractApplicationContext,重写onRefresh()对web容器进行初始化,重写finishRefresh()启动web服务。

    其次,spring抽象了WebServer接口,提供了“启动”和“停止”两个基本方法,具体方法由不同的web容器各自实现,其中tomcat的实现类叫TomcatWebServer。

    最后,TomcatWebServer在构造方法中调用initialize()初始化tomcat,调用start()方法启动web服务,在spring容器销毁之前调用stop()完成tomcat生命周期。

    来源:blog.csdn.net/yasin_huang/article/details/106946751

    文章来源于公众号终码一生

    转载请联系授权