回顾一下Tomcat的启动步骤
- 1.安装JDK,配置环境变量
- 2.下载Tomcat并解压
- 3.执行tomcat/bin目录下的start.sh
执行脚本后的流程
- 1.Tomcat本质上还是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类BootStrap
其实Tomcat和我们自己平时写的代码并没有本质上的区别,只是Tomcat的启动时通过脚本.我们常用的SpringBoot或简单的Java类可以通过java命令启动.
- 2.BootStrap主要任务是初始化Tomcat的
类加载器
,创建Catalina
. - 3.Catalina会解析server.xml,创建响应的组件,并调用Server.start
- 4.Server负责管理Service,调用Service.start
- 5.Service会管理顶层容器Engine,调用Engine.start
经过这几步Tomcat启动就算完成了.
Tomcat比作公司
- Catalina:公司创始人,负责组件团队,创建Server以及它的子组件
- Server:公司的CEO,管理多个事业群,每个事业群是一个Service
- Service:事业群总经理,管理两个职能部门,
对外市场部:连接器
,对内的研发部:容器
- Engine:研发部总经理,作为最顶层的容器组件
Catalina
Catalina主要任务就是创建Server,解析server.xml
,将server.xml定义的各个组件创建出来,然后调用Server的init和start方法
public void start() {
//1.获取Server,如果为空进行创建
if (getServer() == null) {
load();
}
//2.创建失败直接报错退出
if (getServer() == null) {
log.fatal(sm.getString("catalina.noServer"));
return;
}
long t1 = System.nanoTime();
// 3.启动Server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info(sm.getString("catalina.startup", Long.valueOf((t2 - t1) / 1000000)));
}
// 创建Tomcat关闭的钩子
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
//监听Tomcat停止请求
if (await) {
await();
stop();
}
}
Hook
:钩子,Tomcat中的关闭钩子是用于在JVM关闭时做一些清理工作,比如将缓存数据刷到磁盘,或者清理临时文件呢.Hook
本质上是一个线程,JVM在停止之前会尝试执行这个线程的run方法.
Catalina的关闭钩子
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
//其实只是调用了stop方法
Catalina.this.stop();
}
} catch (Throwable ex) {
ExceptionUtils.handleThrowable(ex);
log.error(sm.getString("catalina.shutdownHookFail"), ex);
} finally {
//略...
}
}
}
}
Catalina的关闭Hook中,只是调用了内部的stop
方法,最终也是通过Server的stop和destory方法进行资源释放和清理.
Server
Server组件的实现类是StandardServer
,继承自LifeCycleBase
,生命周期被统一管理,它的子组件是Service,因此需要对Service的生命周期进行管理.
- 在启动时调用Service组件的start方法
- 停止是调用Service组件的stop方法
Server添加Service组件
public void addService(Service service) {
service.setServer(this);
synchronized (servicesLock) {
Service results[] = new Service[services.length + 1];
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;
if (getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException e) {
// Ignore
}
}
// Report this property change to interested listeners
support.firePropertyChange("service", null, service);
}
}
可以看到Server通过一个数组持有所有Service的引用,同时这个数组默认长度是0,只有在每次新增Service组件时候会创建新的数组,长度为原数组长度+1,然后将原数组的数据复制到新数组中,并且使用的是System.arraycopy()
的Native方法,避免数组的自动1.5倍扩容浪费内存空间.
除了管理Service组件外,Server还有一个重要功能,就是启动一个Socket监听停止端口,就是平常使用shutdown.sh脚本就能停止的原因.
其实在Catalina启动的最后,有一个await
方法,这个方法就是调用了Server#await,在Server#await方法中会创建一个Socket对关闭进行监听,在一个死循环中监听来自8005端口的数据(关闭端口模式就是8005),收到SHUTDOWN
指令后就会退出循环,进入stop的流程.
Service
Service的具体实现是StandardService
.StandardService继承LifeCycleBase,并且会持有Server,Connector,Engine,Mapper等组件.
public class StandardService extends LifecycleBase implements Service {
//Server实例
private Server server = null;
//连接器数组
protected Connector connectors[] = new Connector[0];
private final Object connectorsLock = new Object();
//对应的Engine容器
private Engine engine = null;
//映射器及其监听器
protected final Mapper mapper = new Mapper();
protected final MapperListener mapperListener = new MapperListener(this);
其中MapperListener的作用是支持动态部署,监听容器变化将信息更新到Mapper中.
在Service的启动方法中,维护了子组件的生命周期,在各种组件启动的时候,组件有个字的启动顺序
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
//1.触发启动监听
setState(LifecycleState.STARTING);
//2.启动Engine,由Engine启动其子容器
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
//略...
//启动Mapper监听
mapperListener.start();
//最后启动连接器
synchronized (connectorsLock) {
for (Connector connector: connectors) {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}
由组件启动顺序可以看出,Service先启动了Engine,然后是Mapper监听器,最后才启动连接器.
因为只有对内的组件都启动好了,才能启动对外服务的组件,这样才能保证连接后不会因为内部组件未初始化完成导致的问题.所以停止的顺序就会和启动时刚好相反
Engine
Engine具体实现类是StandardEngine
本质是一个顶层容器,所以会继承自ContainerBase
,实现Engine接口.
但是由于Engine是顶层的容器,所以很多功能都抽象到ContainerBase
中实现.
通过HashMap持有所有子容器Host的引用.
protected final HashMap children = new HashMap<>();
当Engine在启动的时候,会通过专门的线程池启动子容器
Engine容器最重要的功能其实就是将请求转发给Host进行处理,具体是通过pipline-Valve实现的.
在Engine的构造函数中,就已经将Pipline-Valve的第一个基础阀设置好了
/**
* Create a new StandardEngine component with the default basic Valve.
*/
public StandardEngine() {
super();
//设置第一个基础阀
pipeline.setBasic(new StandardEngineValve());
//..略
}
StandardEngineValve
在创建Engine时,就会默认创建一个StandardEngineValve
,用于连接Host的Pipline,并且在Mapper
组件中已经对请求进行了路
由处理,通过URL定位了相应的容器,然后把容器对象保存在Request
对象中,所以StandardEngineValve就能开始整个调用链路.
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
Server在启动连接器和容器都进行了加锁
为了保证多线程操作共享资源的正确性.
Server内部使用了线程不安全的数组进行Service的引用,Engine对Host的持有使用了hashMap也是线程不安全的.但是又因为存在资源的动态添加和删除,所以需要加锁.