目录
1.服务器的启动
在Tomcat学习(一):启动过程(1)中简单介绍了Tomcat的启动类、加载server.xml和初始化部分组件的过程。
在使用load方法初始化组件后,Catalina又调用了Server的start方法,开始了各组件的启动。和init一样,LifecycleBase也在startInternal前后增加了事件发布:
public final synchronized void start() throws LifecycleException {
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
return;
}
if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
startInternal();
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
对于正在准备启动、正在启动、已启动三个状态,不做重复启动,日志记录后返回;如果还未初始化,则进行初始化;如果初始化失败,则停止启动;正常状态下,应该是已经初始化或者已停止才能启动,否则会抛异常。
然后发布准备启动事件,并执行startInternal方法,最后根据启动状态决定是停机、抛出异常还是发布已启动事件。
进入Server#startInternal,会立即发出CONFIGURE_START_EVENT事件,然后启动JNDI和所有的Service。JNDI的启动只是发布一个事件,没有额外操作,所以主要看Service的。
Service#startInternal和initInternal基本一致,只是调用方法变成了start而已。
1)Engine
StandardEngine的startInternal方法是委托父类ContainerBase完成的,主要工作就是:
- 启动集群
- 启动安全组件Realm
- 启动子容器(即Host,对于Host来说就是Context,以此类推)
- 启动Pipeline
- 发布正在启动相关事件
在StandardHost中,首先向Pipeline添加一个ErrorReportValve,然后委托Container继续向下一级启动。这里有一个隐藏的Context加载方式。在Host启动完根据server.xml解析到的Context后,会发出生命周期时间,此时有一个LifecycleListener,名为HostConfig,就会处理该事件,从而到webapps(根据appBase属性配置)目录下扫描Web应用。该逻辑下面单独列出。
在StandardContext中,首先发出一个j2ee.state.starting广播,然后启动所属的JNDI服务。然后尝试加载Web应用,这一段也单独列出。由于加载Context时,会把Servlet一并解析、加载好,所以这次就不需要委托ContainerBase向下一级启动了。与Host类似,也会触发ContextConfig加载Web应用。该逻辑下面单独列出。
Pipeline的启动就简单很多,就是启动所有实现了Lifecycle接口的Valve,默认的几个Valve启动过程都是发布事件。
2)Executor
其实Tomcat的Executor线程池,也是基于JDK的线程池实现的,所以这里创建了任务队列TaskQueue(基于LinkedBlockingQueue)和ThreadPoolExecutor(基于JDK的ThreadPoolExecutor)
protected void startInternal() throws LifecycleException {
taskqueue = new TaskQueue(maxQueueSize);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
executor.setThreadRenewalDelay(threadRenewalDelay);
if (prestartminSpareThreads) {
executor.prestartAllCoreThreads();
}
taskqueue.setParent(executor);
setState(LifecycleState.STARTING);
}
3)MapperListener
MapperListener会尝试注册默认Host名,且将自身注册为Engine和所有Host的监听器。然后对于每个Host,将其注册到Mapper。
public void startInternal() throws LifecycleException {
setState(LifecycleState.STARTING);
Engine engine = service.getContainer();
if (engine == null) {
return;
}
findDefaultHost();
addListeners(engine);
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
registerHost(host);
}
}
}
Mapper和MapperListener是完成从请求URL到容器映射的关键类,具体作用会在请求处理中体现。
4)Connector启动了内部的ProtocolHandler,最后又启动了内部的Endpoint。以NioEndpoint为例,它对startInternal进行了实现:
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
if (socketProperties.getProcessorCache() != 0) {
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
}
if (socketProperties.getEventCache() != 0) {
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
}
if (socketProperties.getBufferPool() != 0) {
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());
}
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
}
}
首先是初始化了Processor、Event和Socket的缓存,也就是说Tomcat为了提高处理效率,是设计了缓存机制的。
接下来对线程池、LimitLatch(限制最大连接数)进行初始化。
下面一段的Poller和初始化时提前启动的Poller实现不同,但也还是根据SelectionKey做处理。
最后启动一个Acceptor线程,负责接收请求,然后包装请求。
Poller和Acceptor线程具体工作在请求处理环节介绍。
2.Web应用的加载
一个典型的Context(Web应用)配置如下:
<Context docBase="testapp" path="/test"/>
docBase代表其在webapps(Host的appBase属性配置的)下的文件夹名,path代表其URL的context部分,例如上面配置的应用,根请求地址是:http://localhost:8080/test。
1)首先是创建工作目录,位于 webapps/work/Engine名称/Host名称/Context名称 。
2)然后尝试加载WebResourceRoot,实现类为StandardRoot,包括的资源有五种:
private final List<List<WebResourceSet>> allResources =
new ArrayList<>();
{
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
其中,mainResources指的是 /WEB-INF/classes/META-INF/resources 目录下的资源文件。pre、jar、post,都是context.xml(是Tomcat的配置文件)中指定的。class指的就是所有class文件了。这一步如果执行成功,就会调用StandardRoot的start方法,对加载到的资源进行初始化。
3)然后创建WebappLoader(就是每个应用私有的ClassLoader)。
4)然后创建Cookie处理器(默认实现为Rfc6265CookieProcessor)、初始化字符集映射、依赖检测、初始化JNDI服务。
5)接下来启动WebappLoader(是的,它也是一个Lifecycle实现类)。启动过程的主要工作有:
- 设置classpath
- 设置权限
- 加载/WEB-INF/classes和/WEB-INF/lib下的资源
- 发布事件
启动完成后,为它设置一些属性。
6)然后启动安全组件Realm。
7)发布CONFIGURE_START_EVENT事件,从而激活ContextConfig。
8)启动子节点(即StandardWrapper,靠ContextConfig创建)、Pipeline、会话管理器等组件。
9)将加载到的资源,全部传给ServletContext。
10)创建实例管理器,用于实例化Servlet、Filter。
11)创建Jar包扫描器并设置到ServletContext。
12)合并ServletContext和Context组件的参数。
13)调起ServletContainerInitializer对象(Servlet规范3.0版本引入,用于编程式配置Servlet、Filter)。
14)实例化应用监听器,包括SevletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionIdListener、HttpSessionAttributeListener,HttpSessionListener、ServletContextListener。Spring MVC就是靠ServletContextListener启动DispatcherServlet的。
15)检测未覆盖的HTTP方法的安全约束
16)启动会话管理器
17)实例化FilterConfig、Filter,并调用Filter#init()初始化
18)如果web.xml中,load-on-startup值大于0,此处调用Wrapper的load方法开始实例化
19)启动后台定时处理线程,用来监控文件变更,如果有变化,就需要重新加载or部署
20)发布j2ee.state.running通知
21)释放WebResourceRoot资源
22)发布启动事件
3.自动扫描机制
在Engine启动部分,介绍了基于Tomcat生命周期事件机制设计的HostConfig、ContextConfig存在自动扫描的能力,在这里进行介绍。
3.1 HostConfig
首先看它的事件处理逻辑:
public void lifecycleEvent(LifecycleEvent event) {
// Identify the host we are associated with
try {
host = (Host) event.getLifecycle();
if (host instanceof StandardHost) {
setCopyXML(((StandardHost) host).isCopyXML());
setDeployXML(((StandardHost) host).isDeployXML());
setUnpackWARs(((StandardHost) host).isUnpackWARs());
setContextClass(((StandardHost) host).getContextClass());
}
} catch (ClassCastException e) {
log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
return;
}
// Process the event that has occurred
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.START_EVENT)) {
start();
} else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
stop();
}
}
如果是StandardHost发出的事件,就更新一下信息,然后处理四种事件:
1)PERIODIC_EVENT
该事件代表Web应用资源发生变化,需要重新加载or部署。check方法如下:
protected void check() {
if (host.getAutoDeploy()) {
// Check for resources modification to trigger redeployment
DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);
for (int i = 0; i < apps.length; i++) {
if (!isServiced(apps[i].name))
checkResources(apps[i], false);
}
// Check for old versions of applications that can now be undeployed
if (host.getUndeployOldVersions()) {
checkUndeploy();
}
// Hotdeploy applications
deployApps();
}
}
checkResource方法会检查每个已部署的应用的资源,从代码中可以看到,每个应用都维护了两个列表:redeployResources和reloadResources,记录了资源及其最后修改时间,前一个列表的资源如果发生变更,会导致重新部署,后一个则是重新加载。重新加载和重新部署的区别是,重新加载是Context的重启,重新部署则是重新创建Context。
对于一个资源,有三种情况:
- 是目录,更新时间即可
- 是WAR包,且Context的docBase属性结尾不是“.war”,即需要解压,那么就先删除旧目录,再重新加载,否则直接重新加载即可
- 其他情况,直接重新加载即可
2)BEFORE_START_EVENT
此时Host刚初始化完毕,尚未启动,这一步主要是用来创建需要的目录
3)START_EVENT
该事件会在Host启动时触发,主要工作是完成Web应用的部署。
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
可以看到,这里对三种不同形式的应用进行了部署。
- 根据context描述文件部署
描述文件其实就是把server.xml中,Context元素独立出来,通过Host元素的xmlBase属性指定描述文件所在目录,未配置则默认是 $CATALINA_BASE/conf/Engine名称/Host名称。部署时,扫描配置文件目录,通过线程池提交一个解析任务。
解析任务中又使用了Digester对xml文件进行解析,然后构造一个Context对象,并利用Host的addChild方法,将Context添加到Host。在ContainerBase中,addChildInternal方法会判断当前容器是否已经启动,是的话就将新加入的子容器也一起启动了。最后还会添加对描述文件、Web应用目录、web.xml等的监视,一旦文件发生变更,就会触发重新加载或部署。
- 部署WAR包
对于appBase目录下所有的WAR包(文件名不能是META-INF或WEB-INF),类似根据context描述文件部署,也是利用线程池提交任务来加载的。在提交前,如果配置了需要解压,会把WAR包展开成目录再操作。
如果deployXML属性为true且其内部包含META-INF/context.xml,则使用该文件创建Context对象,如果deployXML为false,但包含META-INF/context.xml,则创建FailedContext,表示部署失败。其他情况下,都是根据contextClass属性来反射创建Context对象,默认是StandardContext。
如果使用WAR包包含的描述文件,且copyXML属性为true,还会将描述文件复制到 $CATALINA_BASE/conf/Engine名称/Host名称 下,文件名和WAR包名称相同。
最后对Context对象设置一些属性,比如生命周期监听器、名称、路径、版本、docBase等,然后通过addChild方法添加到Host。
- Web应用目录部署
基本上和WAR包的一样,可以理解成WAR包多了一步解压操作
4)STOP_EVENT
解除Host作为MBean的注册
3.2 ContextConfig
ContextConfig也是一个生命周期事件监听器,会处理6类事件,不过只有3个和Web应用加载关系比较大,所以只看这三个。
1)CONFIGURE_START_EVENT
负责解析web.xml,将Servlet包装为Wrapper,并创建Filter、ServletContextListener等Web容器相关的对象。
- 初始化Web容器
- 如果配置了ignoreAnnotations属性为false,则解析注解配置,并添加JNDI资源引用
- 验证安全角色名称
- 设置安全认证
第一步中,会加载 默认配置(即 Tomcat目录下conf/web.xml)、Web应用的 WEB-INF/web.xml、JAR包中的 META-INF/web-fragment.xml 和 META-INF/services/javax.servlet.ServletContainerInitializer。然后将从web-fragment.xml解析得到的WebXml对象进行排序,并将排序后各WebXml对应的来源JAR包名存入ServletContext的javax.servlet.context.orderedLibs属性。
对于这个orderedLibs,如果不为空,那么就会检查这些JAR包包含的ServletContainerInitializer实现,封装为initializerClassMap。再解析其HandlesTypes注解指定的Class列表,封装成typeInitializerMap(key为Class,值为ServletContainerInitializer集合)。
如果typeInitializerMap不为空,就会据此来处理/WEB-INF/classes下的类,查找@WebServlet、@WebFilter、@WebListener的等注解的内容,合并进WebXml对象。
然后,将所有的WebXml对象合并。
接下来就是配置JspServlet,以及用合并后的WebXml配置StandardContext,包括Servlet、Filter等Servlet规范规定的组件,还有欢迎文件(welcome-file-list配置)、postConstructMethod和preDestroyMethod等配置。在for (ServletDef servlet : webxml.getServlets().values()) {...}循环中,完成了Servlet封装为Wrapper的过程,并通过addChild添加到Context中。
接下来还会查找 WEB-INF/classes/META-INF/resources 下的静态资源,静态资源和上面解析出的ServletContainerInitializer信息,最后都会被设置到StandardContext中。
2)BEFORE_START_EVENT
该事件用于处理docBase配置,并且解决目录锁问题。
先看docBase的处理。首先获取了Host的appBase属性和Context的docBase属性,从而计算出docBase的绝对路径。如果docBase配置的是个WAR包,且需要解压,则解压之,并且将docBase属性更新为解包后的目录。否则就不用更新。
如果docBase是目录,但是有一个同名WAR包,也需要解压部署,则重新解压。
如果docBase配置的目录不存在,但存在同名WAR包,且需要解压部署,则解压并更新docBase属性。
然后看目录锁的处理。首先也是获取了Host的appBase属性和Context的docBase属性,计算出docBase的绝对路径。然后计算出工作目录下Web应用的目录名或WAR包名,并将源文件复制到工作目录,最后更新docBase属性为工作目录下的目录名 or WAR包名。
这一步的好处是不会对源文件加锁,这样就可以边运行边做修改。
3)AFTER_INIT_EVENT
这一步首先初始化了Digester,用来后续解析conf/context.xml、web.xml用。
然后解析了conf/context.xml,用来将Tomcat提供的默认配置更新到StandardContext对象中。并解析了conf/Engine名称/Host名称/context.xml.default文件(Host级的默认配置),同样进行更新。最后解析Context的configFile配置的context.xml文件,再次更新属性。
实际上在逻辑中,上面三个发生的顺序,应该是 3 - 2 - 1 才对。
至此,Tomcat就完成了所有组件的启动,并且开启了Socket监听,后面就是等待请求到达并处理了。