疑惑

SpringBoot中内置了Tomcat容器,那么Tomcat是在什么时间点启动的?是先扫描包加载类再启动Tomcat?,还是先启动Tomcat再扫描包加载类?

我们做一下假设:

  1. 先启动Tomcat,再扫描包加载类
  2. 先扫描包加载类,再启动Tomcat

接下来我们做一下验证。

验证

首先创建一个空的SpringBoot工程,然后启动它,查看一下控制台的输出日志:

springboot使用undertow如何查看堆栈日志 查看springboot内置tomcat_spring boot

从输出的日志中可以看到②中显示Tomcat进行初始化,并且正在启动中,然后③中进行初始化WebApplicationContext,紧接着就是初始化WebApplicationContext完成,然后是在④中报告我们Tomcat启动完成。

从日志上看,好像是先启动Tomcat再去扫描包的,具体是怎么回事,我们看一下源码,暂时先不揭晓。

  • 从启动类中进入到SpringApplication.run()方法中
public ConfigurableApplicationContext run(String... args) {
  // 创建启动监控类,监控启动过程用了多久,但是ms
  StopWatch stopWatch = new StopWatch();
  // 开始计时,设置开始时间
  stopWatch.start();
  // 上下文实例
  ConfigurableApplicationContext context = null;
  // 启动异常错误报告
  Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
  // 启用headless模式(headless模式是在系统缺少部分硬件支持的时候让服务自力更生)
  configureHeadlessProperty();
  // 获取spring.factories文件中配置的org.springframework.boot.SpringApplicationRunListener列表,原理是Spring的事件机制,所有的类都实现自ApplicationEvent,可以监听启动过程中的任意阶段
  SpringApplicationRunListeners listeners = getRunListeners(args);
  // 进入监听启动阶段
  listeners.starting();
  try {
    // 获取JVM运行参数,也就是在使用java -jar xxx.jar命令启动时指定的其他参数
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // 准备运行环境,传入监听器和运行参数,获取环境变量,绑定到环境中
    ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
    // 配置spring.beaninfo.ignore属性,SpringBoot中默认为true
    configureIgnoreBeanInfo(environment);
    // 输出Banner信息,banner的输出方式有三种:none/console/log,默认为console,通过配置参数spring.main.banner-mode指定
    Banner printedBanner = printBanner(environment);
    // ☆创建一个上下文实例,比较重点的地方☆
    context = createApplicationContext();
    // 获取spring.factories文件中声明的SpringBootExceptionReporter
    exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                                                     new Class[] { ConfigurableApplicationContext.class }, context);
    // 创建上下文,设置环境变量和资源加载器,创建bean,加载到所有的listener中
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    // 调用spring的refresh方法,并注册一个ShutdownHook(应用关闭时的动作,可以通过继承AbstractApplicationContext实现自定义)
    refreshContext(context);
    // 空方法
    afterRefresh(context, applicationArguments);
    // 启动完成,停止计时
    stopWatch.stop();
    if (this.logStartupInfo) {
      // 输出启动信息,包括计时
      new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
    }
    // 进入监听器的启动完成事件
    listeners.started(context);
    callRunners(context, applicationArguments);
  }
  catch (Throwable ex) {
    handleRunFailure(context, ex, exceptionReporters, listeners);
    throw new IllegalStateException(ex);
  }

  try {
    // 进入监听器的运行中事件
    listeners.running(context);
  }
  catch (Throwable ex) {
    handleRunFailure(context, ex, exceptionReporters, null);
    throw new IllegalStateException(ex);
  }
  return context;
}

从源码中并未看到是在哪里启动了Tomcat容器,那么我们就看一下创建Spring上下文的方法中是否有关键字

  • createApplicationContext()方法
protected ConfigurableApplicationContext createApplicationContext() {
  Class<?> contextClass = this.applicationContextClass;
  if (contextClass == null) {
    try {
      switch (this.webApplicationType) {
        case SERVLET:
          // org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
          contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
          break;
        case REACTIVE:
          // org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext
          contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
          break;
        default:
          // org.springframework.context.annotation.AnnotationConfigApplicationContext
          contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
      }
    }
    catch (ClassNotFoundException ex) {
      throw new IllegalStateException(
        "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
    }
  }
  return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

这是一个创建ApplicationContext实例的方法,我们只看Tomcat的AnnotationConfigServletWebServerApplicationContext,他的继承关系如下

springboot使用undertow如何查看堆栈日志 查看springboot内置tomcat_tomcat_02

以上类中并没有在静态代码块中启动Tomcat,则说明不是在这里启动的,这里只是创建一个ApplicationContext上下文实例

  • prepareContext()方法
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
                            SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
  // 设置环境变量
  context.setEnvironment(environment);
  // 处理上下文
  postProcessApplicationContext(context);
  // 做refresh前的初始化准备
  applyInitializers(context);
  // 进入到监听器的上下文准备阶段
  listeners.contextPrepared(context);
  if (this.logStartupInfo) {
    logStartupInfo(context.getParent() == null);
    logStartupProfileInfo(context);
  }
  // 创建特殊的引导类实例,单例的
  ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
  beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
  if (printedBanner != null) {
    // 创建Banner类实例,单例的
    beanFactory.registerSingleton("springBootBanner", printedBanner);
  }
  if (beanFactory instanceof DefaultListableBeanFactory) {
    ((DefaultListableBeanFactory) beanFactory)
    .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
  }
  if (this.lazyInitialization) {
    context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
  }
  // Load the sources
  Set<Object> sources = getAllSources();
  Assert.notEmpty(sources, "Sources must not be empty");
  load(context, sources.toArray(new Object[0]));
  listeners.contextLoaded(context);
}

方法里整个过程都在创建各种类的实例,并未出现对Tomcat的启动操作,看来也不在这个方法中,那么就继续往下看

  • refreshContext()方法
    这个方法最终是调用到AbstractApplicationContext#refresh()中,这就到了Spring基础框架中。我们在分析createApplicationContext()方法的时候,知道Tomcat使用的是org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext类,这个类继承自org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext,在这个类中,重写了父类AbstractApplicationContext中的refresh()、onRefresh()和finishRefresh()方法,而在AbstractApplicationContext#refresh()方法中,调用了onRefresh()和finishRefresh()这两个方法
public void refresh() throws BeansException, IllegalStateException {
  synchronized (this.startupShutdownMonitor) {
    // Prepare this context for refreshing.
    // Tell the subclass to refresh the internal bean factory.
    // Prepare the bean factory for use in this context.
    try {
      // Allows post-processing of the bean factory in context subclasses.
      // Invoke factory processors registered as beans in the context.
      // Register bean processors that intercept bean creation.
      // Initialize message source for this context.
      // Initialize event multicaster for this context.

      // Initialize other special beans in specific context subclasses.
      onRefresh();

      // Check for listener beans and register them.
      // Instantiate all remaining (non-lazy-init) singletons.

      // Last step: publish corresponding event.
      finishRefresh();
    } catch (BeansException ex) {
      throw ex;
    } finally {
    }
  }
}

此时调用的onRefresh()和finishRefresh()实际上是调用了ServletWebServerApplicationContext中的方法,那么我们就来看下这个类中的这两个方法分别做了什么

  • onRefresh()方法
protected void onRefresh() {
  // 调用父类的onRefresh()方法
  super.onRefresh();
  try {
    // 创建WebServer
    createWebServer();
  }
  catch (Throwable ex) {
    throw new ApplicationContextException("Unable to start web server", ex);
  }
}

private void createWebServer() {
  WebServer webServer = this.webServer;
  ServletContext servletContext = getServletContext();
  if (webServer == null && servletContext == null) {
    // 获取ServletWebServerFactory的实例,使用Tomcat的话会获取到tomcatServletWebServerFactory
    ServletWebServerFactory factory = getWebServerFactory();
    // 获取WebServer实例,方法内创建Tomcat并准备Tomcat启动所需环境变量
    this.webServer = factory.getWebServer(getSelfInitializer());
  } else if (servletContext != null) {
    try {
      getSelfInitializer().onStartup(servletContext);
    } catch (ServletException ex) {
    }
  }
  // 初始化自定义的环境变量,涉及到servletContextInitParams和servletConfigInitParams
  initPropertySources();
}

protected ServletWebServerFactory getWebServerFactory() {
  // 使用bean名称,这样我们就不会考虑层次结构
  String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
  if (beanNames.length == 0) {
    throw new ApplicationContextException("");
  }
  if (beanNames.length > 1) {
    throw new ApplicationContextException("");
  }
  // 获取Bean实例,这里获取到的就是TomcatServletWebServerFactory类的实例
  return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

通过代码我们知道在onRefresh()方法中主要是创建Tomcat实例,准备Tomcat启动所需参数和配置信息,并未启动Tomcat

  • finishRefresh()方法
protected void finishRefresh() {
  super.finishRefresh();
  // 启动WebServer,难道是这里启动的Tomcat?
  WebServer webServer = startWebServer();
  if (webServer != null) {
    publishEvent(new ServletWebServerInitializedEvent(webServer, this));
  }
}

private WebServer startWebServer() {
  WebServer webServer = this.webServer;
  if (webServer != null) {
    // 启动WebServer,这里调用的是TomcatWebServer#start()方法
    webServer.start();
  }
  return webServer;
}

// TomcatWebServer#start()方法
public void start() throws WebServerException {
  // 加锁防并发
  synchronized (this.monitor) {
    if (this.started) {
      return;
    }
    try {
      addPreviouslyRemovedConnectors();
      // 获取Tomcat的Connector
      Connector connector = this.tomcat.getConnector();
      if (connector != null && this.autoStart) {
        // 如果connector不为null,并且自动启动,则启动时执行延迟加载
        // 当端口大于0的时候,autoStart就为true
        performDeferredLoadOnStartup();
      }
      // 检查Connector是否已启动,若有未启动的Connector则抛异常
      checkThatConnectorsHaveStarted();
      // 修改运行标识属性
      this.started = true;
      // 输出Tomcat启动成功日志
      logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
                  + getContextPath() + "'");
    } catch (ConnectorStartFailedException ex) {
    } catch (Exception ex) {
    } finally {
    }
  }
}

这里好像也仅仅就是检测Tomcat服务是否启动成功了,并没有执行启动这个操作,看来也不在这里。

这就奇怪了,我们几乎翻遍了整个启动类的代码,都没有Tomcat启动的代码,那么Tomcat是怎么启动的呢?是不是我们遗漏了什么?我们来回顾一下上面的分析,TomcatWebServer实例的获取的地方ServletWebServerFactory#getWebServer()方法内我们好像没有看,是不是在创建TomcatWebServer实例的时候直接就启动了,然后在TomcatWebServer#start()方法中只是去检测启动的状态?我们来看下ServletWebServerFactory#getWebServer()的源码

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);
  // 获取TomcatWebServer实例
  return getTomcatWebServer(tomcat);
}

// 不重要
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
  // autoStart = getPort() >= 0
  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()方法源码

private void initialize() throws WebServerException {
  synchronized (this.monitor) {
    try {

      // 启动服务器以触发初始化侦听器
      this.tomcat.start();

      // 所有Tomcat线程都是守护进程线程。我们创建一个阻塞非守护进程来停止立即关闭
      startDaemonAwaitThread();
    }
    catch (Exception ex) {
      stopSilently();
      destroySilently();
      throw new WebServerException("Unable to start embedded Tomcat", ex);
    }
  }
}

终于找到了,语句this.tomcat.start()就是启动Tomcat容器的关监语句。

总结

通过对SpringApplication.run()方法执行顺序的分析来看,先构建Spring上下文加载Bean,也就是扫描包,然后再在refreshContext()方法中创建Tomcat容器并启动容器。

由此我们可以验证出假设2是正确的