为什么关心 Tomcat 中一个 web 应用的加载过程?在前面的文章中看过多次 Tomcat 的组件结构图,这里再贴出来回顾一下:
看过前面的Tomcat 7 的一次请求分析系列文章的人应该知道,浏览器一次请求送到 Tomcat 服务器之后,最终会根据浏览器中的 url 路径找到相应的实际要访问的 web 应用的 context 对象(默认即org.apache.catalina.core.StandardContext
类的实例)。比如访问的 url 为http://localhost:8080/
,那么将会送到上图 ROOT 文件夹表示的 web 应用中,访问的 url 为http://localhost:8080/docs
,那么将访问 docs 文件夹表示的 web 应用。所以能够猜到的是在 Tomcat 启动完成后,必定容器内部已经构造好了表示相应web应用的各个 context 对象。
本文就对这个问题一探究竟。在Tomcat 7 服务器关闭原理的开头提到,默认的配置下 Tomcat 启动完之后会看到后台实际上总共有 6 个线程在运行:
main
、http-bio-8080-Acceptor-0
、http-bio-8080-AsyncTimeout
、ajp-bio-8009-Acceptor-0
、ajp-bio-8009-AsyncTimeout
,已经谈到了这些线程的作用,它们是如何产生并响应请求的。但有一个线程没有说,即ContainerBackgroundProcessor[StandardEngine[Catalina]]
,而本文要解答的问题奥秘就在这个线程之中。
先看看这个线程是如何产生的,其实从命名就可以看出一些端倪,它叫做容器后台处理器,并且跟 StandardEngine 关联起来,它的产生于作用也同样如此。
Tomcat 7 中所有的默认容器组件( StandardEngine、StandardHost、StandardContext、StandardWrapper )都会继承父类org.apache.catalina.core.ContainerBase
,在这些容器组件启动时将会调用自己内部的 startInternal 方法,在该方法内部一般会调用父类的 startInternal 方法( StandardContext 类的实现除外),比如org.apache.catalina.core.StandardEngine
类中的 startInternal 方法:
super.startInternal()
即调用父类org.apache.catalina.core.ContainerBase的startInternal
方法,在该方法最后:
LifecycleState.STARTING
状态(这样将向容器发布一个Lifecycle.START_EVENT
事件),这一行的作用本文后面会提到,暂且按下不表。第 9 行调用 threadStart 方法,看看 threadStart 方法的代码:
ContainerBackgroundProcessor[
开头,线程名字后面取的是对象的 toString 方法,以 StandardEngine 为例,看看org.apache.catalina.core.StandardEngine
的 toString 方法实现:
但这里有一个问题,既然 StandardEngine、StandardHost 都会调用super.startInternal()
方法,按默认配置,后台理应产生两个后台线程,实际为什么只有一个?
回到org.apache.catalina.core.ContainerBase
的 threadStart 方法,在启动线程代码之前有两个校验条件:
null
,backgroundProcessorDelay 是-1
org.apache.catalina.core.StandardEngine
在其自身构造函数中做了一点修改:
-1
改成了10
,所以 Tomcat 启动解析 xml 时碰到一个 Engine 节点就会对应产生一个后台处理线程。
讲完了这个后台处理线程的产生,看看这个线程所作的事情,再看下这个线程的启动代码:
而这个 backgroundProcess 方法在 ContainerBase 内部已经给出了实现:
Lifecycle.PERIODIC_EVENT
事件。
上面就是 Tomcat 7 的后台处理线程所作的事情的概述,在 Tomcat 的早期版本中有一些后台处理的事情原来是在各个组件内部分别自定义一个线程并启动,在 Tomcat 5 中改成了所有后台处理共享同一线程的方式。
回到本文要解答的问题,web 应用如何加载到容器中的?在 ContainerBase 类的 backgroundProcess 方法的最后:
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
复制代码
向容器注册了一个PERIODIC_EVENT
事件。前面说道默认的ContainerBackgroundProcessor[StandardEngine[Catalina]]
线程会定期(默认为 10 秒)执行 Engine、Host、Context、Wrapper 各容器组件及与它们相关的其它组件的 backgroundProcess 方法,所以也会定期向 Host 组件发布一个PERIODIC_EVENT
事件,这里看下 StandardHost 都会关联的一个监听器org.apache.catalina.startup.HostConfig
:
在 Tomcat 启动解析 xml 时
org.apache.catalina.startup.Catalina
类的 386 行:digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"))
在 HostRuleSet 类的 addRuleInstances 方法中:
第 9 到 12 行看到,所有 Host 节点都会添加一个
org.apache.catalina.startup.HostConfig
对象作为org.apache.catalina.core.StandardHost
对象的监听器
在 HostConfig 的 lifecycleEvent 方法中可以看到如果 Host 组件收到了 Lifecycle.PERIODIC_EVENT 事件的发布所作出的响应(如果对 Tomcat 7 的 Lifecycle 机制不清楚可以看下Tomcat 7 启动分析(五)Lifecycle 机制和实现原理:
PERIODIC_EVENT
将会执行 check 方法。第 19 行,如果发布的事件是START_EVENT
则执行 start 方法。check 方法和 start 方法最后都会调用 deployApps() 方法,看下这方法的实现:
本文前面提到默认情况下组件启动的时候会发布一个Lifecycle.START_EVENT
事件(在org.apache.catalina.core.ContainerBase
类的 startInternal 方法倒数第二行),回到 HostConfig 的 lifecycleEvent 方法中,所以默认启动时将会执行 HostConfig 的 start 方法,在该方法的最后:
if (host.getDeployOnStartup())
deployApps();
复制代码
因为默认配置 host.getDeployOnStartup() 返回 true ,这样容器就会在启动的时候直接加载相应的 web 应用。
当然,如果在 server.xml 中 Host 节点的 deployOnStartup 属性设置为 false ,则容器启动时不会加载应用,启动完之后不能立即提供 web 应用的服务。但因为有上面提到的后台处理线程在运行,会定期执行 HostConfig 的 check 方法: