Spring Boot启动过程(六)之内嵌Tomcat中StandardHost、StandardContext和StandardWrapper的启动教程详解
StandardEngine[Tomcat].StandardHost[localhost]的启动与StandardEngine不在同一个线程中,它的start:
// 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]))); } boolean fail = false; for (Future<Void> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString("containerBase.threadedStartFailed"), e); fail = true; } } if (fail) { throw new LifecycleException( sm.getString("containerBase.threadedStartFailed")); }</div>
private static class StartChild implements Callable<Void> { private Container child; public StartChild(Container child) { this.child = child; } @Override public Void call() throws LifecycleException { child.start(); return null; } }</div>
这个start流程中,initInternal方法是ContainerBase的代码,还是那个初始化startStopExecutor的,线程名例如Thread[localhost-startStop-1,5,main],这次是用来初始化host的子容器的,然后是StandardHost中的startInternal方法,主要是注册了一个errorValue,如果现有的pipeline中没有errorvalue,则反射创建org.apache.catalina.valves.ErrorReportValve实例,并加入pipeline中,容器pipeline加入Value时会发布一个Container.ADD_VALVE_EVENT事件,与engine一样,之后进入ContainerBase的startInternal,但是这次Realm是null不需要启动,然后findChildren出StandardEngine[Tomcat]. StandardHost [localhost].StandardContext[],然后同样新开个线程new StartChild,start同样是上面的代码,需要特别说明的是,这次before_init的事件有监听的了,FixContextListener,DisablePersistSessionListener,MemoryLeakTrackingListener;FixContextListener监听的处理,会加入一个用于不做用户身份认证的安全检查的Value:
Context context = (Context) event.getLifecycle(); if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { context.setConfigured(true); } // LoginConfig is required to process @ServletSecurity // annotations if (context.getLoginConfig() == null) { context.setLoginConfig( new LoginConfig("NONE", null, null, null)); context.getPipeline().addValve(new NonLoginAuthenticator()); }</div>
DisablePersistSessionListener监听只处理start事件,所以这里只判断了一下发现不是就出去了,其实这里可以思考下,有没有更好的办法,让监听不只是广播方式,能不能用订阅方式,先不细想了,接着看代码,MemoryLeakTrackingListener只监听了after_start事件,这步同样什么都没做。
于是来到了StandardContext的initInternal,它的super.initInternal又是一个startStopExecutor,ContainerBase的super.initInternal就不再说了,发送j2ee.object.created消息:
Notification notification = new Notification("j2ee.object.created", this.getObjectName(), sequenceNumber.getAndIncrement()); broadcaster.sendNotification(notification);</div>
Notification是EventObject的子类,代表由MBean发出的通知,MBean server发出的通知会包含发出的MBean的引用,如果MBean注册了监听,可以通过object name或引用获取消息发出者,官方建议使用object name;sendNotification方法:
/** * Sends a notification. * * If an {@code Executor} was specified in the constructor, it will be given one * task per selected listener to deliver the notification to that listener. * * @param notification The notification to send. */ public void sendNotification(Notification notification) { if (notification == null) { return; } boolean enabled; for (ListenerInfo li : listenerList) { try { enabled = li.filter == null || li.filter.isNotificationEnabled(notification); } catch (Exception e) { if (logger.debugOn()) { logger.debug("sendNotification", e); } continue; } if (enabled) { executor.execute(new SendNotifJob(notification, li)); } } }</div>
发完消息就转变状态为初始化完成,因为监听器是注册在context容器上的,于是after_init事件又触发了那三个监听器,这一阶段监听器什么都没处理走了下过场而已;before_start同走过场;然后StandardContext的startInternal方法,发布了个j2ee.state.starting消息object name为Tomcat:j2eeType=WebModule,name=//localhost/,J2EEApplication=none, J2EEServer=none;setConfigured(false)还没有正确的配置;设置WebResourceRoot,WebResourceRoot提供整个应用资源处理类的各种方法,内嵌用的实现类是StandardRoot,set的过程中加了写锁:
try { setResources(new StandardRoot(this)); } catch (IllegalArgumentException e) { log.error(sm.getString("standardContext.resourcesInit"), e); ok = false; }</div>
StandardRoot的属性allResources:
private final List<List<WebResourceSet>> allResources = new ArrayList<>(); { allResources.add(preResources); allResources.add(mainResources); allResources.add(classResources); allResources.add(jarResources); allResources.add(postResources); }</div>
http://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/WebResourceRoot.html有相关说明,我就不翻译了。
set之后就是启动resourcesStart,initInternal执行的是StandardRoot的initInternal方法,super.initInternal中依然是那两行代码,register(cache, getObjectNameKeyProperties() + ",name=Cache")会发送MBeanServerNotification. REGISTRATION_NOTIFICATION通知,生成ObjectName这里cacheJmxName是Tomcat:type=WebResourceRoot,host=localhost,context=/,name=Cache;registerURLStreamHandlerFactory里面的代码是TomcatURLStreamHandlerFactory.register()这行代码的注释说这是为了支持war包内的jar资源的。之后是循环上面的allResources,init里面加入的webResourceSet,但是由于全都是空的,所以等于没执行,就不说了,回头再仔细看看什么情况下回不为空,还是内嵌的就是空的。createMainResourceSet主要是设置个主目录,例如/tmp/tomcat-docbase.3031819619941848514.80,然后是各种资源该放在哪个子目录的一些设置代码;这次资源有一个了,所以可以有一个start了,DirResourceSet的;super.initInternal()的super是AbstractFileResourceSet:
//-------------------------------------------------------- Lifecycle methods @Override protected void initInternal() throws LifecycleException { super.initInternal(); // Is this an exploded web application? if (getWebAppMount().equals("")) { // Look for a manifest File mf = file("META-INF/MANIFEST.MF", true); if (mf != null && mf.isFile()) { try (FileInputStream fis = new FileInputStream(mf)) { setManifest(new Manifest(fis)); } catch (IOException e) { log.warn(sm.getString("dirResourceSet.manifestFail", mf.getAbsolutePath()), e); } } } }</div>
super.initInternal主要是对base目录进行了一些规范化处理,规范的方法主要是UnixFileSystem中的canonicalize其中还使用ExpiringCache对路径做了缓存,另外还有在normalize方法中对路径中类似"\.."的部分做了处理。WebAppMount是Web应用发布资源的位置,必须以‘/'开头,这里应该是通过它来判断不是war包部署的模式,然后由于manifest没找到,所以方法返回初始化完成,这个资源一路状态变化就启动完了。
回到StandardRoot,接下来是processWebInfLib方法,代