Tomcat8源码笔记(七)组件启动Server Service Engine Host启动
一.Tomcat启动的入口
Tomcat初始化简单流程前面博客介绍了一遍,组件除了StandardHost都有博客,欢迎大家指文中错误。Tomcat启动类是Bootstrap,而启动容器启动入口位于 Catalina 的start方法: 因为反射调用Bootstrap的Catalina实例的start方法.
Catalina启动分为两个大阶段,一是启动Server(start方法),二是 Server启动之后等待(await方法)
一.StandardServer#start
按照之前记录 Tomcat8源码笔记(一)Lifecycle接口 Tomcat的初始化 init 、启动 start都是采用模板设计模式, 共性的地方就是触发监听的事件,个性的地方就是 initInternal、startInternal ,
StandardServer初始化默认是六个监听器,Tomcat8源码笔记(四)Server和Service初始化 这六个监听器暂且就不记录, 直接查看StandardServer的 startInternal方法.
StandardServer#startInternal解读:首先触发configure_start监听事件,主要是完成NamingContextListener的相关JNDI操作,这个暂且忽略. 设置StandardServer状态进入启动中LifecycleState.STARTING,同样六个触发监听器中相关事件; globalNamingResources同样是Tomcat组件,实现Lifecycle接口,所以globalNamingResources#start也会按照模板方法来,具体干嘛的还不清楚,后面补充; 最核心的地方看到了,StandardServer的初始化交给了Service的start,遍历Server的Service,分别start.
二.StandardService#start
StandardService默认是没有监听器的,所以start模板方法直接看startInternal即可. Tomcat8源码笔记(四)Server和Service初始化 这里介绍过StandardService的初始化,可以证明.
StandardService的startInternal方法按顺序分为四个步骤:启动容器container(通常就是StandardEngine)、启动线程池executor、启动mapperListener、启动connector连接器.
那我们下面就从container, 也就是StandardEngine 启动开始记录.
三. StandardEngine#start
Tomcat8源码笔记(五)组件Container分析 记录过StandardEngine的实例化包括初始化过程,StandardEngine有一个监听器,是Tomcat为我们注册的 EngineConfig.
EngineConfig的lifecycleEvent方法如下: 可以看到EngineConfig的start时机是容器启动staring状态,而StandardEngine一开始启动进入的状态是before_start.
StandardEngine#startInternal如下: 可以看到除了打印日志Starting Servlet Engine以外,就是调用父类的startInternal方法;而StandardEngine的父类是ContainerBase,不是LifecycleMBean这个啥,所以StandardEngine启动核心在ContainerBase中.
ContainerBase#startInternal方法
ContainerBase#startInternal方法,Cluster默认的server.xml中没有配置为空,Realm默认的server.xml中配置了一个LockOutRealm,Realm作为一个组件,start方法又是一遍Tomcat的流程,这里重点不是记这个; StandardEngine的children也就是StandardHost,startStopExecutor在之前介绍的Tomcat8源码笔记(五)组件Container分析 中初始化过了,初始化方法位于ContainerBase#initInternal。 这里可以看到使用线程池执行了StartChild实例,StartChild持有当前StandardHost对象,下面程序会阻塞着,直到StartChild的call执行完毕,程序执行完StartChild,还会调用Pipeline的start,启动自身的后台线程。 startInternal执行逻辑有点多,我们先从StartChild看起!
三.一 StartChild#call
因为StartChild是通过线程池来执行的,我们可以通过IDEA很方便的进行多线程DEBUG,这点我以前用Eclipse时候没发现这种优点!
多线程DEBUG方式,在StartChild#call处打断点,上面Suspend(挂起)地方勾选Thread,具体作用对我而言最明显的就是,有时候多线程DEBUG,不选Thread,线程切换时候IDEA界面在那里抽筋一样多个代码地方跳跃,很影响感受,F6单步调试按一下,屏幕狂跳一下.
切换线程的窗口位于DEBUG视图,下面红框的地方可以选择要调试哪个线程。 记住: StartChild执行的那个线程池出来的线程名字叫Catalina-startStop-,而1代表了线程的编号;另外也可以发现executor.submit(new StartChild())方法,得到的Future,在调用get方法之后会一直堵塞知道方法结束,下图中的main线程就在WAIT等待状态.
跑偏了,可以看到上面StartChild,因为StartChild持有StandardHost实例,call方法也是调用StandardHost#start方法! 而StandardHost组件初始化工作都没有做过,组件状态还是NEW的状态,所以按照Tomcat的start模板方法,会先init初始化,然后再start! StandardHost实例持有一个LifecycleListener :HostConfig,不用说肯定是Tomcat替我们注册的,就像StandardEngine有一个EngineConfig的监听器。
那下面的逻辑清楚了,StandardHost#start,先是触发HostConfig初始化事件监听---->StandardHost#initInternal—>HostConfig初始化结束事件监听---->HostConfig启动前事件监听--->
StandardHost#startInternal---->HostConfig启动结束事件监听.
三.二 HostConfig监听触发事件
可以看到只是给HostConfig设置四个 StandardHost的属性:copyXML、unpackWARs、deployXML、contextClass,另外触发事件只在before_start、start等情况下才有逻辑处理,所以before_init只是设置了上面四个属性而已。
按照上面流程,该是 StandardHost#initInternal:
转念一想,StandardHost继承自ContainerBase,initInternal方法应该也是初始化线程池,StandardEngine初始化的线程池产生的线程名字叫Catalina-startStop-,而StandardHost初始化的线程池产生线程名字类似 localhost-startStop-,此外的super.initInternal肯定就是JMX注册功能了.
继承走流程,该是HostConfig触发初始化结束after_init事件,但是上面有HostConfig触发代码,没有after_init的逻辑处理,所以呢?又重复设置了上面四个属性;
继续流程,HostConfig触发启动之前before_start事件,会调用HostConfig#beforeStart:
HostConfig#beforeStart
host#getCreateDirs默认返回true,host#getAppBaseFile获取到catalina-home/webapps目录,并赋值给StandardHost的appBaseFile,
host#getConfigBaseFile获取到catalina-home/conf/Catalina/localhost目录,并赋值给StandardHost的hostConfigBase,
之后校验这两层目录是否存在、是否是目录,属于校验目录是否存在为下面项目作保障。
继承走流程,StandardHost#startInternal, StandardHost在此之前Pipeline有两个Valve阀门,first是AccessLogValve,basic阀门是StandardHostValve,执行之后会添加一个阀门到first的next,这个阀门就是ErrorReportValve; 此外还会调用父类ContainerBase的startInternal方法. 父类ContainerBase#startInternal方法比较冗长,因为StandardHost没有子容器了,所以startInternal主要从Pipeline的start,到设置组件状态为STARTING,到启动自身守护线程;这里我们重点先关注,设置StandardHost状态为STARTING,这就又触发HostConfig的监听事件了!
HostConfig触发start监听事件,执行的方法是start,以下代码JMX忽略,那就是判断之前的appBaseFile是不是目录,deployOnStartup属性默认true,部署项目在TOMCAT启动的时候,那deployApps肯定就是部署项目了呗!先说好,按照上面流程分析,部署完项目之后呢,HostConfig的start就完事了,StandardHost的startInternal也就剩下启动自身守护线程没记录了,然后最后一步呢触发HostConfig启动完成监听,启动完成也没这种监听啊!所以当下只有deployApps和startThread了!
HostConfig#deployApps
deployApps除了前两行,我觉得每一个方法都是部署项目有关的,就是部署各种各样类型的项目呗!再来回忆杀下,appBase是catalina-home/webapps,configBase是catalina-home/conf/Catalina/localhost,至于这几个路径咋拼成的呢? catalina是StandardEngine的name,localhost是StandardHost的name.
filterAppPaths
unfilteredAppPaths就是webapps下所有文件名集合,说来也巧合我们StandardHost的deployIgnore,没设置过就是null,默认啥都不校验忽略;也告诉我们可以在server.xml中的Host元素里配置deployIgnore=“….”,来完成正则表达式匹配达到不部署某些项目的目的,学到了! 果然一个方法就是一个知识点啊!
deployDescriptors
deployDescriptors方法暂时不知道作用,后续补充!
deployWARs
代码过于长,这里不占用篇幅贴图了,主要适用于部署webapps下的 .war项目,后面会尝试部署一个war项目,暂时留作不记录,代码位置位于 HostConfig#deployWARs !
deployDirectories
这个适合我们分析,因为部署的都是文件夹型的项目,实例化了ContextName类,并且部署项目方式是线程池来执行DeployDirectory,并且会一直阻塞着直到webapps下目录项目都部署完毕,我们又可以使用IDEA多线程方式DEBUG
DeployDirectory类定义如下,我们在run方法中打上断点即可进入DEBUG,另外值得考虑的一点是?万一项目多,为啥Tomcatmore不是采用线程池,扩大一点线程数呢?我们可以看到StandardHost的初始化线程和最大线程数都是一致的,1个? 这是为什么呢?多线程可以加快部署项目速度吗?
在DEBUG之前,在补充一个类ContextName,baseName就是项目文件夹名字,path和name一般都是/文件夹名字,比如 /docs 、 /SpringWeb等等,而Root项目的path是“”,TOMCAT也是根据path来将我们的请求转发到对应项目中。
正式进入项目部署的环节,deployDirectory
代码比较冗长,这里只记录下过程,代码位置在:HostConfig#deployDirectory.
webapps下某个项目为例,假设WEB-INF下不存在content.xml,实例化一个StandardContext,代表一个项目,给StandardContext添加ContextConfig,并且给StandardContext设置ContextName的四个属性值,path、name、baseName、version等等,再将StandardContext加入到StandardHost; 部署完成之后就是存储在HostConfig的deployed中,稍后专门写一篇博客记录部署项目。
Tomcat部署项目打印日志如下:
部署完成webapps下的项目,程序会阻塞着直到项目都部署完成,至此HostConfig#start完成,StandardHost#start还剩下 threadStart方法
由于backgroudProcessorDelay默认设置为-1,所以并没有启动这个ContainerBackgroudProcessor!
至此,StandardHost启动完毕,再次回顾下,StandardHost的启动是通过线程池(线程初始化、最大个数都为1个的池子)来启动的,StandardHost可以说完成了webapps下项目的部署,
StandardEngine启动了StandardHost,创建了一堆StandardContext对应每一个项目,而ContainerBase的initInternal方法还没看完,还剩下三步:管道pipeline启动,设置状态为start触发相应监听,启动自身守护线程,会启动一个ContainerBackgroudProcessor线程!
至此StandardServer包括StandardService的启动完毕,也就意味着是Container的启动完毕,而Tomcat还没启动的就是mapperListener、Connector了,大体记录了一遍流程,篇幅原因还有的细节后面补充.