kphx
25. 嵌入式容器:创建过程回顾和嵌入式Tomcat的核心启动

25. 嵌入式容器:创建过程回顾和嵌入式Tomcat的核心启动

 
前几篇咱解析了WebMvc中的一些原理,以及 DispatcherServlet 的完整工作原理,接下来的两篇咱来深入 SpringBoot 中的嵌入式容器。
我们都知道,默认情况下 SpringBoot 应用可以打jar包运行,在启动IOC容器时引导启动嵌入式Web容器(Tomcat),而且根据之前的IOC原理,我们知道在 ServletWebServerApplicationContext 的 onRefresh 方法中创建了嵌入式 Tomcat,这一篇咱来展开研究嵌入式 Tomcat 创建的过程。
(为统一概念,本篇和下一篇中出现的嵌入式Web容器统一为 Tomcat

0. 前置知识

Tomcat 的内部核心结构包含如下组件:
  • Service:一个 Tomcat-Server 可以有多个 Service , Service 中包含下面的所有组件
  • Connector:用于与客户端交互,接收客户端的请求,并将结果响应给客户端
  • Engine:负责处理来自 Service 中的 Connector 的所有请求
  • Host:可理解为主机,一个主机绑定一个端口号
  • Context:可理解为应用,一个主机下有多个应用,一个应用中有多个 Servlet (可以简单理解为 webapps 中一个文件夹代表一个 Context )

1. ServletWebServerApplicationContext#onRefresh

protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start web server", ex); } }
在 ServletWebServerApplicationContext 的 onRefresh 方法会调用 createWebServer 方法创建嵌入式 Tomcat。
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); // 如果WebServer和ServletContext都为null时,证明需要创建嵌入式Tomcat if (webServer == null && servletContext == null) { // 创建了嵌入式Tomcat的工厂 ServletWebServerFactory factory = getWebServerFactory(); // 创建嵌入式Tomcat this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); }
这部分源码我们之前也都看过,重点来看 getWebServer 中的创建嵌入式 Tomcat 的部分:

2. TomcatServletWebServerFactory#getWebServer

(不需要展开的步骤均已在源码中标好注释)
public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol"; private String protocol = DEFAULT_PROTOCOL; public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); // 给嵌入式Tomcat创建一个临时文件夹,用于存放Tomcat运行中需要的文件 File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); // Tomcat核心概念:Connector,默认放入的protocol为NIO模式 Connector connector = new Connector(this.protocol); // 给Service添加Connector tomcat.getService().addConnector(connector); // 执行定制器,修改即将设置到Tomcat中的Connector customizeConnector(connector); tomcat.setConnector(connector); // 关闭热部署(嵌入式Tomcat不存在修改web.xml、war包等情况) tomcat.getHost().setAutoDeploy(false); // 设置backgroundProcessorDelay机制 configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } // 2.1 生成TomcatEmbeddedContext prepareContext(tomcat.getHost(), initializers); // 3. 创建TomcatWebServer return getTomcatWebServer(tomcat); }
咱来看这个方法,首先它new了一个 Tomcat 对象(它还不是真正的嵌入式 Tomcat),之后下面的一大堆都是对 Tomcat 的配置,这里面着重看一眼这个最复杂的 prepareContext 方法:

2.1 prepareContext

(注释均已在源码中标注好,小伙伴们对哪一步感兴趣可以借助IDE自己动手Debug体会一下实现)
protected void prepareContext(Host host, ServletContextInitializer[] initializers) { File documentRoot = getValidDocumentRoot(); // 创建TomcatEmbeddedContext TomcatEmbeddedContext context = new TomcatEmbeddedContext(); if (documentRoot != null) { context.setResources(new LoaderHidingResourceRoot(context)); } context.setName(getContextPath()); context.setDisplayName(getDisplayName()); // 设置contextPath,很熟悉了 context.setPath(getContextPath()); // 给嵌入式Tomcat创建docbase的临时文件夹 File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase"); context.setDocBase(docBase.getAbsolutePath()); // 注册监听器 context.addLifecycleListener(new FixContextListener()); context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader() : ClassUtils.getDefaultClassLoader()); // 设置默认编码映射 resetDefaultLocaleMapping(context); addLocaleMappings(context); context.setUseRelativeRedirects(false); try { context.setCreateUploadTargets(true); } catch (NoSuchMethodError ex) { // Tomcat is < 8.5.39. Continue. } configureTldSkipPatterns(context); // 自定义的类加载器,可以加载web应用的jar包 WebappLoader loader = new WebappLoader(context.getParentClassLoader()); loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName()); // 指定类加载器遵循双亲委派机制 loader.setDelegate(true); context.setLoader(loader); // 注册默认的Servlet if (isRegisterDefaultServlet()) { addDefaultServlet(context); } // 如果需要jsp支持,注册jsp的Servlet和Initializer if (shouldRegisterJspServlet()) { addJspServlet(context); addJasperInitializer(context); } // 注册监听器 context.addLifecycleListener(new StaticResourceConfigurer(context)); ServletContextInitializer[] initializersToUse = mergeInitializers(initializers); host.addChild(context); configureContext(context, initializersToUse); postProcessContext(context); }
回到 getWebServer 中,最下面执行最核心的方法: getTomcatWebServer

3. getTomcatWebServer

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { return new TomcatWebServer(tomcat, getPort() >= 0); }
它只是new了一个 TomcatWebServer 而已,进入构造方法中:
public TomcatWebServer(Tomcat tomcat, boolean autoStart) { Assert.notNull(tomcat, "Tomcat Server must not be null"); this.tomcat = tomcat; this.autoStart = autoStart; initialize(); }
构造方法中竟然还暗藏玄机,它在属性赋值后执行了 initialize 方法。

4. initialize

private void initialize() throws WebServerException { logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); synchronized (this.monitor) { try { // 4.1 设置Engine的id addInstanceIdToEngineName(); // 4.2 获取第一个Context Context context = findContext(); // 4.3 添加监听器 context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { // Remove service connectors so that protocol binding doesn't // happen when the service is started. removeServiceConnectors(); } }); // Start the server to trigger initialization listeners // 4.4 启动Tomcat this.tomcat.start(); // We can re-throw failure exception directly in the main thread rethrowDeferredStartupExceptions(); // ...... } }
把整个启动过程分为几步来看:

4.1 addInstanceIdToEngineName:设置Engine的id

private void addInstanceIdToEngineName() { int instanceId = containerCounter.incrementAndGet(); if (instanceId > 0) { Engine engine = this.tomcat.getEngine(); engine.setName(engine.getName() + "-" + instanceId); } }
这部分在初始化时,containerCounter 的值是-1,调用 incrementAndGet 方法后返回0。因为是0,下面的if也就不进入了。
notion image

4.2 findContext:获取第一个Context

private Context findContext() { for (Container child : this.tomcat.getHost().findChildren()) { if (child instanceof Context) { return (Context) child; } } throw new IllegalStateException("The host does not contain a Context"); }
这一步它要拿到Tomcat中的 host,来获取它的 children 。通过Debug发现已经存在一组 Container 了,当然也只有一个:
notion image
拿到之后,返回去。

4.3 addLifecycleListener:添加监听器

context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { // Remove service connectors so that protocol binding doesn't // happen when the service is started. // 删除ServiceConnectors,以便在启动服务时不会发生协议绑定。 removeServiceConnectors(); } });
这一步的单行注释是解释不让 Connector 初始化,可为什么人家组件都初始化了,就单单不让它初始化呢?这要回归到IOC容器的启动原理中。
创建嵌入式 Tomcat 的时机是 onRefresh 方法,此时还有很多单实例Bean没有被创建,此时如果直接初始化所有组件后,Connector 也被初始化,此时客户端就可以与 Tomcat 进行交互,但这个时候单实例Bean还没有初始化完毕(尤其是 DispatcherServlet),就会导致传入的请求 Tomcat 无法处理,出现异常。
所以 SpringBoot 为了避免这个问题,会在嵌入式 Tomcat 发布事件时检测此时的 Context 状态是否为 "START_EVENT" ,如果是则将这些 Connector 先移除掉

4.4 this.tomcat.start:启动Tomcat

public void start() throws LifecycleException { getServer(); // 4.5 server.start(); }
第一步的操作跟前面是一样的,下面是 server.start() ,它来真正启动嵌入式 Tomcat 。

4.5 server.start

来到 LifecycleBase :
public final synchronized void start() throws LifecycleException { if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) || LifecycleState.STARTED.equals(state)) { if (log.isDebugEnabled()) { Exception e = new LifecycleException(); log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e); } else if (log.isInfoEnabled()) { log.info(sm.getString("lifecycleBase.alreadyStarted", toString())); } return; } if (state.equals(LifecycleState.NEW)) { init(); } else if (state.equals(LifecycleState.FAILED)) { // ...... }
这里启动时会根据当前的状态来走不同的分支,而刚启动的 Tomcat 状态为 "NEW" ,进入 init 方法:
notion image

4.5.1 StandardServer#init

来到父类 LifecycleBase 中(StandardServer 没有重写):
public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { handleSubClassException(t, "lifecycleBase.initFail", toString()); } }
上面对于不为NEW的状态,会额外执行方法,当前状态为NEW,不进入,走下面的try块。try块在 initInternal 的前后设置了两次生命周期的状态(初始化中、初始化完成),说明 initInternal 方法中一定是真正的 Tomcat 组件初始化的过程。

4.5.2 StandardServer#initInternal

来到 StandardServer :
protected void initInternal() throws LifecycleException { super.initInternal(); // ...... }
先来到父类 LifecycleBase 的 initInternal 方法:
protected void initInternal() throws LifecycleException { // If oname is not null then registration has already happened via preRegister(). // 如果oname不为null,则已经通过preRegister()进行了注册 if (oname == null) { mserver = Registry.getRegistry(null, null).getMBeanServer(); oname = register(this, getObjectNameKeyProperties()); } }
默认情况下 oname 为null,走下面的初始化过程。初始化完成后的效果:
notion image
回到子类 StandardServer :
protected void initInternal() throws LifecycleException { super.initInternal(); // ...... // Initialize our defined Services for (int i = 0; i < services.length; i++) { services[i].init(); } }
中间的一大段我们暂且不关心,主要来看最后一步:它要初始化这些 Server 中的 Service 。

4.5.3 又回到LifecycleBase

public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { handleSubClassException(t, "lifecycleBase.initFail", toString()); } }
再走一遍流程,进到 initInternal 方法,这次初始化的是 StandardService

4.5.4 StandardService#initInternal

protected void initInternal() throws LifecycleException { super.initInternal(); if (engine != null) { engine.init(); } // Initialize any Executors for (Executor executor : findExecutors()) { if (executor instanceof JmxEnabled) { ((JmxEnabled) executor).setDomain(getDomain()); } executor.init(); } // Initialize mapper listener mapperListener.init(); // Initialize our defined Connectors synchronized (connectorsLock) { for (Connector connector : connectors) { connector.init(); } } }
发现了这里面要依次初始化几个组件:EngineExecutorLifecycleMBeanBase(mapperListener)Connector 。
到这里咱应该有种意识:这几个家伙的初始化不会又回到 LifecycleBase 中了吧?可以很确定的回答:基本是的。。。所以接下来咱就不贴 LifecycleBase 的源码了,直接看这几个组件的实现吧:

4.5.5 StandardEngine#initInternal

protected void initInternal() throws LifecycleException { // Ensure that a Realm is present before any attempt is made to start // one. This will create the default NullRealm if necessary. // 在尝试启动一个Realm之前,请确保存在一个Realm。如有必要,这将创建默认的NullRealm getRealm(); super.initInternal(); } public Realm getRealm() { Realm configured = super.getRealm(); // If no set realm has been called - default to NullRealm // This can be overridden at engine, context and host level if (configured == null) { configured = new NullRealm(); this.setRealm(configured); } return configured; }
很明显这里是初始化Realm的,且实现很简单,不再展开。

4.5.6 Executor#initInternal

protected void initInternal() throws LifecycleException { super.initInternal(); }
它还是调的父类 LifecycleMBeanBase 的方法,不再赘述。

4.5.7 MapperListener#initInternal

MapperListener 没有重写 initInternal 方法,相当于也跟上面一样,不再赘述。
protected void initInternal() throws LifecycleException { // If oname is not null then registration has already happened via preRegister(). // 如果oname不为null,则已经通过preRegister()进行了注册 if (oname == null) { mserver = Registry.getRegistry(null, null).getMBeanServer(); oname = register(this, getObjectNameKeyProperties()); } }
跟上面一样,不再赘述。

4.5.8 Connector#initInternal

protected void initInternal() throws LifecycleException { super.initInternal(); // ...... // Initialize adapter // CoyoteAdapter负责连接Coyote和Servlet容器(web应用) adapter = new CoyoteAdapter(this); protocolHandler.setAdapter(adapter); // ...... try { protocolHandler.init(); } catch (Exception e) { throw new LifecycleException( sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e); } }
中间部分它初始化了一个 CoyoteAdapter ,它负责连接 Connector 和 Container ,也就是 Coyote 和 Servlet容器
最下面的try-catch中,它又调用了 protocolHandler.init :

4.5.9 protocolHandler.init

来到 AbstractHttp11Protocol :
public void init() throws Exception { // Upgrade protocols have to be configured first since the endpoint // init (triggered via super.init() below) uses this list to configure // the list of ALPN protocols to advertise // 必须先配置升级协议,因为端点初始化(通过下面的super.init()触发)使用此列表来配置要发布的ALPN协议列表 for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { configureUpgradeProtocol(upgradeProtocol); } super.init(); }
Debug发现这个 upgradeProtocols 为空,直接走下面父类(AbstractProtocol)的 init 方法:
public void init() throws Exception { if (getLog().isInfoEnabled()) { getLog().info(sm.getString("abstractProtocolHandler.init", getName())); logPortOffset(); } if (oname == null) { // Component not pre-registered so register it oname = createObjectName(); if (oname != null) { Registry.getRegistry(null, null).registerComponent(this, oname, null); } } if (this.domain != null) { rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName()); Registry.getRegistry(null, null).registerComponent( getHandler().getGlobal(), rgOname, null); } String endpointName = getName(); endpoint.setName(endpointName.substring(1, endpointName.length()-1)); endpoint.setDomain(domain); endpoint.init(); }
上面又是一堆初始化,这个咱暂且不关注,注意最底下有一个 endpoint.init :

4.5.10 endpoint.init

来到 AbstractEndPoint :
public final void init() throws Exception { // Debug为false if (bindOnInit) { bindWithCleanup(); bindState = BindState.BOUND_ON_INIT; } if (this.domain != null) { // Register endpoint (as ThreadPool - historical name) oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\""); Registry.getRegistry(null, null).registerComponent(this, oname, null); ObjectName socketPropertiesOname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties"); socketProperties.setObjectName(socketPropertiesOname); Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null); for (SSLHostConfig sslHostConfig : findSslHostConfigs()) { registerJmx(sslHostConfig); } } }
这里面又是初始化 oname ,又是配置 socketProperties 的,但这里面再也没见到 init 方法,证明这部分初始化过程已经结束了。
值得注意的是,Debug发现 bindOnInit 变量为false,说明嵌入式 Tomcat 不在初始化期间绑定端口号

4.5.11 初始化小结

嵌入式 Tomcat 的组件初始化步骤顺序如下:
  1. Server
  1. Service
  1. Engine
  1. Executor
  1. MapperListener
  1. Connector
  1. Protocol
  1. EndPoint

至此,初始化过程完毕,回到 start 方法中:
public final synchronized void start() throws LifecycleException { // ...... if (state.equals(LifecycleState.NEW)) { init(); } // else if ...... try { setStateInternal(LifecycleState.STARTING_PREP, null, false); startInternal(); // ...... }
接下来到了真正启动的部分了:startInternal

4.6 StandardServer#startInternal

protected void startInternal() throws LifecycleException { // 发布启动事件 fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); // 4.6.1 NamingResources启动 globalNamingResources.start(); // Start our defined Services synchronized (servicesLock) { for (int i = 0; i < services.length; i++) { // 4.6.2 Service启动 services[i].start(); } } if (periodicEventDelay > 0) { monitorFuture = getUtilityExecutor().scheduleWithFixedDelay( new Runnable() { @Overridepublic void run() { startPeriodicLifecycleEvent(); } }, 0, 60, TimeUnit.SECONDS); } }
startInternal 方法中有两部分启动:globalNamingResources 启动,services 启动。分别来看:

4.6.1 globalNamingResources.start

来到 NamingResourcesImpl ,因为它也实现了 LifecycleBase ,还是会来到上面的 startInternal 方法中:
protected void startInternal() throws LifecycleException { fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); }
这部分只是发布事件和设置状态而已,与之前一致,不再赘述。

4.6.2 StandardService#start

最终又来到 startInternal 方法了:
protected void startInternal() throws LifecycleException { if(log.isInfoEnabled()) log.info(sm.getString("standardService.start.name", this.name)); setState(LifecycleState.STARTING); // Start our defined Container first if (engine != null) { synchronized (engine) { // 4.6.3 engine.start(); } } synchronized (executors) { for (Executor executor: executors) { // 4.6.6 executor.start(); } } // 4.6.7 mapperListener.start(); // Start our defined Connectors second synchronized (connectorsLock) { for (Connector connector: connectors) { // If it has already failed, don't try and start it if (connector.getState() != LifecycleState.FAILED) { // 4.6.8 connector.start(); } } } }
发现这部分与之前的初始化几乎一致!也是依次启动 Engine 、Executor 、MapperListener 、Connector 。

4.6.3 Engine#start

protected synchronized void startInternal() throws LifecycleException { // Log our server identification information if (log.isInfoEnabled()) { log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo())); } // Standard container startup super.startInternal(); }
它直接调的父类 ContainerBase 的 startInternal 方法:(重要部分注释已标注在源码中)
protected synchronized void startInternal() throws LifecycleException { // Start our subordinate components, if any logger = null; getLogger(); // Cluster与集群相关,SpringBoot项目中使用嵌入式Tomcat,不存在集群 Cluster cluster = getClusterInternal(); if (cluster instanceof Lifecycle) { ((Lifecycle) cluster).start(); } // Realm与授权相关 Realm realm = getRealmInternal(); if (realm instanceof Lifecycle) { ((Lifecycle) realm).start(); } // Start our child containers, if any // Container的类型是StandardHost Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<>(); for (int i = 0; i < children.length; i++) { // 4.6.4 异步初始化Host results.add(startStopExecutor.submit(new StartChild(children[i]))); } MultiThrowable multiThrowable = null; for (Future<Void> result : results) { try { result.get(); } catch (Throwable e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); if (multiThrowable == null) { multiThrowable = new MultiThrowable(); } multiThrowable.add(e); } } if (multiThrowable != null) { throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"), multiThrowable.getThrowable()); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) { ((Lifecycle) pipeline).start(); } setState(LifecycleState.STARTING); // Start our thread if (backgroundProcessorDelay > 0) { monitorFuture = Container.getService(ContainerBase.this).getServer() .getUtilityExecutor().scheduleWithFixedDelay( new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS); } }
这里面又是嵌套的初始化了其他组件,一一来看:

4.6.4 new StartChild(children[i])

这部分比较有意思,它用了异步初始化,先来看看 StartChild 的定义:
private static class StartChild implements Callable<Void>
它实现了带返回值的异步多线程接口 Callable !那里面的核心方法就是 call :
public Void call() throws LifecycleException { child.start(); return null; }
它在这里初始化 child,而通过Debug得知 child 的类型是 StandardHost,故来到 StandardHost 的 start 方法:
protected synchronized void startInternal() throws LifecycleException { // Set error report valve String errorValve = getErrorReportValveClass(); if ((errorValve != null) && (!errorValve.equals(""))) { try { boolean found = false; Valve[] valves = getPipeline().getValves(); for (Valve valve : valves) { if (errorValve.equals(valve.getClass().getName())) { found = true; break; } } if(!found) { Valve valve = (Valve) Class.forName(errorValve).getConstructor().newInstance(); getPipeline().addValve(valve); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString( "standardHost.invalidErrorReportValveClass", errorValve), t); } } super.startInternal(); }
上面的一个大if结构是设置错误提示页面的,下面又调父类的 startInternal :
protected synchronized void startInternal() throws LifecycleException { // ...... // Start our child containers, if any Container children[] = findChildren(); List<Future<Void>> results = new ArrayList<>(); for (int i = 0; i < children.length; i++) { results.add(startStopExecutor.submit(new StartChild(children[i]))); }
又回来了。。。因为一个 Host 包含一个 Context 。
Host 搜索children就会搜到它下面的 Context ,之后又是下面的初始化过程,进入 Context 的初始化:

4.6.5 TomcatEmbeddedContext#start

这个方法非常非常长(300行+),小册就不全部贴出来了,咱现在是启动阶段,那我们只关心里面的 start 相关代码。
借由前面的Debug过程,我们先总结一个规律:所有带生命周期性质的组件,都会在启动时走到 startInternal 方法
那我们接下来Debug这个300行+的代码时,只需要在方法头和方法尾打上断点,中间部分只要走 startInternal 方法的,就是组件的初始化。通过Debug,发现有如下组件被调用了 start 方法:
  • StandardRoot
  • DirResourceSet
  • WebappLoader
  • JarResourceSet
  • StandardWrapper
  • StandardPineline
  • StandardWrapperValve
  • NonLoginAuthenticator
  • StandardContextValve
  • StandardManager
  • LazySessionIdGenerator
组件的 startInternal 方法就不一一列举了,小册把这些组件的核心功能列举一下,有兴趣的小伙伴可以深入研究一下,小册也只是引导小伙伴们对嵌入式 Tomcat 的底层原理有一个大概的认识和了解。

4.6.6 Executor#start

synchronized (executors) { for (Executor executor: executors) { // 4.6.6 executor.start(); } }
Engine 启动完成后,下一步到了 Executor 的启动。但由于 Executor 没有实现 startInternal 方法,故这一步不再展开。

4.6.7 MapperListener#start

mapperListener.start();
Executor 启动完成后,接下来启动 MapperListener :
public void startInternal() throws LifecycleException { setState(LifecycleState.STARTING); Engine engine = service.getContainer(); if (engine == null) { return; } // 获取当前部署的主机名(本地调试为localhost) findDefaultHost(); // 4.6.7.1 把当前自身注册到Engine、Host、Context、Wrapper中 addListeners(engine); // 取出的Container的类型为Host Container[] conHosts = engine.findChildren(); for (Container conHost : conHosts) { Host host = (Host) conHost; if (!LifecycleState.NEW.equals(host.getState())) { // Registering the host will register the context and wrappers // 4.6.7.2 将Host、Context、Wrapper注册到当前监听器中 registerHost(host); } } }
这里面它干了三件事请:获取主机名,将监听器注册到各组件中,将各组件注册到监听器(实现双向)。咱主要看一眼两方互相注册的动作:

4.6.7.1 addListeners

private void addListeners(Container container) { container.addContainerListener(this); container.addLifecycleListener(this); for (Container child : container.findChildren()) { addListeners(child); } }
很明显这是递归调用,而且从 Engine 开始一层一层往下执行,都把当前监听器注册进去。

4.6.7.2 registerHost

private void registerHost(Host host) { String[] aliases = host.findAliases(); mapper.addHost(host.getName(), aliases, host); for (Container container : host.findChildren()) { if (container.getState().isAvailable()) { registerContext((Context) container); } } // Default host may have changed findDefaultHost(); // log ...... }
注意这里面的for循环,又调了 registerContext ,可见这个思路也跟上面差不多,类似于递归,不过这是手动一步一步往里设置(毕竟类型不一样)。

4.6.8 Connector#start

// Start our defined Connectors second synchronized (connectorsLock) { for (Connector connector: connectors) { // If it has already failed, don't try and start it if (connector.getState() != LifecycleState.FAILED) { connector.start(); } } }
最后一步是启动 Connector 。但通过Debug发现根本没有 Connector !
notion image
为什么之前还看到一个 Connector ,现在就没了呢?还记得在 this.tomcat.start(); 之前有一个监听器吗?
context.addLifecycleListener((event) -> { if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { // Remove service connectors so that protocol binding doesn't // happen when the service is started. removeServiceConnectors(); } });
很明显就是这一步把 Connector 删了嘛!
Remove service connectors so that protocol binding doesn't happen when the service is started.删除 Service 的 Connector ,以便在启动服务时不会发生协议绑定。
那大概率就是这一步让这个家伙给删了。注意这个监听器是在 context 中注册的,那我们猜想,应该是 Context 启动时触发的这个监听效果。
回过头来重新Debug一次,发现在 Engine 启动之后,Connector 就已经没了。
notion image
notion image
将断点打在上面 context 的监听器上,放行,发现果然 在 TomcatEmbeddedContext 的启动期间触发了这个监听器
notion image
notion image
至于为什么要删除的原因,上面4.3章节也描述了,是为了防止 SpringBoot 应用还没有初始化完成时就已经可以接收客户端的请求

至此,tomcat.start(); 方法彻底执行完成。

4.6.9 启动小结

启动过程依次启动了如下组件:
  1. NamingResources
  1. Service
  1. Engine
  1. Host
  1. Context
  1. Wrapper
  1. MapperListener

4.7 回到initialize

private void initialize() throws WebServerException { // ...... this.tomcat.start(); // We can re-throw failure exception directly in the main thread // 如果上面的启动出现问题,则抛出异常 rethrowDeferredStartupExceptions(); try { // 将当前Context与当前ClassLoader绑定 ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } catch (NamingException ex) { // Naming is not enabled. Continue } // Unlike Jetty, all Tomcat threads are daemon threads. We create a // blocking non-daemon to stop immediate shutdown // 4.8 阻止Tomcat结束 startDaemonAwaitThread(); } catch (Exception ex) { stopSilently(); destroySilently(); throw new WebServerException("Unable to start embedded Tomcat", ex); } } }
嵌入式 Tomcat 启动完成后,在try块的最底下会起一个新的线程,阻止 Tomcat 结束。

4.8 startDaemonAwaitThread

private void startDaemonAwaitThread() { Thread awaitThread = new Thread("container-" + (containerCounter.get())) { @Overridepublic void run() { TomcatWebServer.this.tomcat.getServer().await(); } }; awaitThread.setContextClassLoader(getClass().getClassLoader()); awaitThread.setDaemon(false); awaitThread.start(); }
这里面它会起一个新的 awaitThread 线程,并回调 Tomcat 中 Server 的 await 方法,并且它还设置 Daemon 为false。
先解释一下为什么设置 Daemon :Tomcat 中所有的进程都是 Daemon 线程,在Java应用中,只要有一个非 Daemon 线程还在运行,则 Daemon 线程就不会停止,整个应用也不会终止。既然要让 Tomcat 一直运行以监听客户端请求,就必须需要让 Tomcat 内部的 Daemon 线程都存活,根据前面的描述,就必须制造一个能卡住停止的非 Daemon 线程。于是上面新起的 awaitThread 线程就被设置为非 Daemon 线程。
下面看看线程中执行的 await 方法:(源码很长,只截取出跟 SpringBoot 嵌入式 Tomcat 有关的部分)
public void await() { // Negative values - don't wait on port - tomcat is embedded or we just don't like ports // 如果关闭Tomcat的端口是-2,则直接返回,不卡线程 if (getPortWithOffset() == -2) { // undocumented yet - for embedding apps that are around, alive. return; } // 如果关闭Tomcat的端口是-1,代表是嵌入式Tomcat if (getPortWithOffset() == -1) { try { awaitThread = Thread.currentThread(); while(!stopAwait) { try { Thread.sleep( 10000 ); } catch( InterruptedException ex ) { // continue and check the flag } } } finally { awaitThread = null; } return; } // Set up a server socket to wait on // 退出端口正常的处理(属于外部Tomcat逻辑)...... }
可以发现,如果设置的 Tomcat 的退出端口是 -1,则代表是嵌入式 Tomcat,它会每10秒会检查一次stopAwait的值,如果为true则停止卡线程,让 Tomcat 停止。
默认请款下 Tomcat 的退出端口是8005,为什么这里会变成 -1 呢?追踪源码,发现在 Tomcat 的 getServer 方法中有设置:
public Server getServer() { if (server != null) { return server; } System.setProperty("catalina.useNaming", "false"); server = new StandardServer(); initBaseDir(); // Set configuration source ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null)); // 设置端口号为 -1,代表嵌入式 server.setPort( -1 ); Service service = new StandardService(); service.setName("Tomcat"); server.addService(service); return server; }
至此,嵌入式 Tomcat 已经成功创建好,但 Connector 还没有归还,还在被删除中。

5. ServletWebServerApplicationContext#startWebServer

当IOC容器的 onRefresh 方法执行完,单实例Bean初始化完成后,来到 finishRefresh 方法:
protected void finishRefresh() { super.finishRefresh(); WebServer webServer = startWebServer(); if (webServer != null) { publishEvent(new ServletWebServerInitializedEvent(webServer, this)); } }
在这里它会真正启动嵌入式 Tomcat 容器:
private WebServer startWebServer() { WebServer webServer = this.webServer; if (webServer != null) { webServer.start(); } return webServer; }
可以看到它在这里调用了 TomcatWebServer 的 start 方法。

6. TomcatWebServer#start

(关键步骤注释已标注在源码中)
public void start() throws WebServerException { synchronized (this.monitor) { if (this.started) { return; } try { // 6.1 还原、启动Connector addPreviouslyRemovedConnectors(); // 只拿一个Connector Connector connector = this.tomcat.getConnector(); if (connector != null && this.autoStart) { // 6.2 延迟启动 performDeferredLoadOnStartup(); } // 检查Connector是否正常启动 checkThatConnectorsHaveStarted(); this.started = true; logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '" + getContextPath() + "'"); } // catch ...... finally { // 解除ClassLoader与TomcatEmbeddedContext的绑定关系 Context context = findContext(); ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } } }
源码中的注释已解释的比较清楚,下面分述源码中两个重要的环节:还原 Connector 和启动 Connector :

6.1 addPreviouslyRemovedConnectors:还原Connector

private void addPreviouslyRemovedConnectors() { Service[] services = this.tomcat.getServer().findServices(); for (Service service : services) { Connector[] connectors = this.serviceConnectors.get(service); if (connectors != null) { for (Connector connector : connectors) { // 6.1.1 添加并启动 service.addConnector(connector); if (!this.autoStart) { stopProtocolHandler(connector); } } this.serviceConnectors.remove(service); } } }
可以发现它将一个缓存区的 Connector 一个一个取出放入 Service 中。注意在 service.addConnector 中有顺便启动的部分:

6.1.1 service.addConnector

public void addConnector(Connector connector) { synchronized (connectorsLock) { connector.setService(this); Connector results[] = new Connector[connectors.length + 1]; System.arraycopy(connectors, 0, results, 0, connectors.length); results[connectors.length] = connector; connectors = results; } try { if (getState().isAvailable()) { // 6.1.2 启动Connector connector.start(); } } catch (LifecycleException e) { throw new IllegalArgumentException( sm.getString("standardService.connector.startFailed", connector), e); } // Report this property change to interested listeners support.firePropertyChange("connector", null, connector); }
前面的部分是取出 Connector ,并与 Service 绑定,之后中间部分的try块,会启动 Connector :

6.1.2 connector.start

protected void startInternal() throws LifecycleException { // Validate settings before starting if (getPortWithOffset() < 0) { throw new LifecycleException(sm.getString( "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset()))); } setState(LifecycleState.STARTING); try { // 启动ProtocolHandler protocolHandler.start(); } catch (Exception e) { throw new LifecycleException( sm.getString("coyoteConnector.protocolHandlerStartFailed"), e); } }
Connector 的启动会引发 ProtocolHandler 的启动:

6.1.3 protocolHandler.start

public void start() throws Exception { if (getLog().isInfoEnabled()) { getLog().info(sm.getString("abstractProtocolHandler.start", getName())); logPortOffset(); } // 启动EndPoint endpoint.start(); monitorFuture = getUtilityExecutor().scheduleWithFixedDelay( new Runnable() { @Overridepublic void run() { if (!isPaused()) { startAsyncTimeout(); } } }, 0, 60, TimeUnit.SECONDS); }
ProtocolHandler 的启动会引发 EndPoint 的启动,至此所有组件均已启动完毕。

6.2 performDeferredLoadOnStartup:延迟启动

private void performDeferredLoadOnStartup() { try { for (Container child : this.tomcat.getHost().findChildren()) { if (child instanceof TomcatEmbeddedContext) { // 延迟启动Context ((TomcatEmbeddedContext) child).deferredLoadOnStartup(); } } } catch (Exception ex) { if (ex instanceof WebServerException) { throw (WebServerException) ex; } throw new WebServerException("Unable to start embedded Tomcat connectors", ex); } }
发现这里面会延迟启动 TomcatEmbeddedContext ,此处它对比较老的表现层框架(如Struts)做了一些兼容支持,主要是替换类加载器,由于 SpringBoot 默认使用WebMvc或WebFlux,已不采用过老的表现层框架,故此处不再展开讨论。
至此,嵌入式 Tomcat 完整启动。

小结

  1. 嵌入式 Tomcat 与外置的 Tomcat 在核心组件上都是一样的,主要包括 Service 、Connector 、Engine 、Host 、Context 。
  1. Tomcat 的启动过程分为初始化和启动两个步骤,分别按照核心组件的顺序启动。
  1. 嵌入式 Tomcat 在启动时要先移除掉 Connector ,防止IOC容器还没有全部启动完成后就能接收客户端的请求。
【至此,嵌入式 Tomcat 成功启动,核心启动步骤解析完毕。下一篇咱看看嵌入式 Tomcat 有哪些调整的接口,好给我们做性能调优】
badge