我们可以通过Tomcat的/bin目录下的脚本startup.sh来启动Tomcat,执行了这个脚本会发生什么呢? 通过下面这张流程图了解一下。
Tomcat启动流程图.jpg- Tomcat本质上是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类Bootstrap。
- Bootstrap的主要任务是初始化Tomcat的类加载器并创建Catalina。
- Catalina是一个启动类,它通过解析server.xml、创建相应的组件,并调用Server的start方法。
- Server组件的职责就是管理Service组件,它会负责调用Service的start方法。
- Service组件的职责就是管理连接器和顶层容器组件Engine,因此它会调用连接器和Engine的start方法。
Catalina
Catalina的主要任务就是创建Server,需要解析出server.xml,把在server.xml里配置的各种组件一一创建出来,接着调用Server组件的init方法和start方法,这样整个Tomcat就启动起来了。作为”管理者“,Catalina还需要处理各种异常情况,比如我们通过”Ctrl + C“关闭Tomcat时,Tomcat将如何优雅的停止并且清理资源呢?因此Catalina在JVM中注册了一个”关闭钩子“。
public void start() {
// 如果持有的Server实例为空,就解析server.xml创建一个
if (getServer() == null) {
load();
}
// 如果创建失败 报错退出
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// 启动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("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}
// 创建并注册JVM关闭钩子
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
// 用await方法监听停止请求
if (await) {
await();
stop();
}
}
那什么是”关闭钩子“,它又是做什么的呢?如果我们需要在JVM关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向JVM注册一个”关闭钩子“,”关闭钩子“其实就是一个线程,JVM在停止之前会尝试执行这个线程的run方法。下面是Tomcat的”关闭钩子“CatalinaShutdownHook:
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
Catalina.this.stop();
}
} catch (Throwable ex) {
ExceptionUtils.handleThrowable(ex);
log.error(sm.getString("catalina.shutdownHookFail"), ex);
} finally {
// If JULI is used, shut JULI down *after* the server shuts down
// so log messages aren't lost
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).shutdown();
}
}
}
}
可以看出,Tomcat的“关闭钩子”实际上就是执行了Server的stop方法,Server组件的stop方法会释放和清理所有的资源。
Server组件
Server组件的具体实现类是StandardServer,Server继承了LifecycleBase,它的生命周期被统一管理,并且它的子组件是Service,因此它还要管理Service的生命周期,也就是说在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,下面是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;
// 启动 Service 组件
if (getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException e) {
// Ignore
}
}
// 触发监听事件
// Report this property change to interested listeners
support.firePropertyChange("service", null, service);
}
}
除此之外,Server组件还有一个重要的任务是启动一个Socket类监听停止端口,这就是为什么你能通过shutdown命令来关闭Tomcat。上面Caralina的启动方法的最后一行代码就是调用了Server的await方法。在await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就新建连接,然后从Socket中读取数据;如果读到的数据是停止命令”SUTDOWN“,就退出循环,进入stop流程。
Service组件
Service组件的具体实现类是StandardService,我们西拿来看看它的定义以及关键的成员变量。
public class StandardService extends LifecycleMBeanBase implements Service {
/**
* The name of this service.
* Service的名字
*/
private String name = null;
/**
* The <code>Server</code> that owns this Service, if any.
* Server实例
*/
private Server server = null;
/**
* The set of Connectors associated with this Service.
* 连接器数组
*/
protected Connector connectors[] = new Connector[0];
private final Object connectorsLock = new Object();
// 对应的Engine容器
private Engine engine = null;
/**
* Mapper.
* 映射器
*/
protected final Mapper mapper = new Mapper();
/**
* Mapper listener.
* 映射器的监听器
*/
protected final MapperListener mapperListener = new MapperListener(this);
}
为什么要有一个MapperListener?这是因为Tomcat支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,它监听容器的变化,并把信息更新到Mapper中,这是典型的观察者模式。
作为”管理“角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序,Service的启动方法:
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
// 触发启动监听器
setState(LifecycleState.STARTING);
// 先启动engine, Engine会启动它的子容器
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
// 启动Mapper容器
mapperListener.start();
// 启动连接器,连接器会启动它的子组件 比如Endpoint
// Start our defined Connectors second
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}
从启动方法可以看到,Service先启动了Engine组件,再启动Mapper监听器,最后才是启动连接器,内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而Mapper也依赖容器组件,容器组件启动好了才能监听它们的变化,因此Mapper和MapperListener在容器组件之后启动。组件停止的顺序和启动的顺序正好相反的,也是基于它们的依赖关系。
Engine组件
再来看看顶层容器组件Engine是如何实现的,Engine本质是一个容器,因此它继承了ContainerBase基类,并且实现了Engine接口。
public class StandardEngine extends ContainerBase implements Engine {
...
}
Engine的子容器是Host,所以它持有了一个Host容器的数组,在抽象类ContainerBase中,ContainerBase中有这样一个数据结构:
protected final HashMap<String, Container> children = new HashMap<>();
ContainerBase用HashMap保存了它的子容器,并且ContainerBase还实现了子容器的”增删改查“,甚至连子容器的启动和停止都提供了默认实现,比如ContainerBase会用专门的线程池来启动子容器。
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
所以Engine在启动Host子容器时就直接重用了这个方法。
我们知道容器最重要的功能是处理请求,而Engine容器对请求的”处理“,其实就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的。
我们知道每一个容器组件都有一个Pipeline,而Pipeline中有一个基础阀(Basic Valve),而Engine容器的基础阀定义如下:
final class StandardEngineValve extends ValveBase {
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
// 拿到请求中的Host容器
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST,
sm.getString("standardEngine.noHost",
request.getServerName()));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
// 调用Host容器中的Pipeline中的第一个Valve
host.getPipeline().getFirst().invoke(request, response);
}
}
这个基础阀实现非常简单,就是把请求转发到Host容器。我们可以看到处理请求的Host容器对象是从请求中拿到的,请求对象中怎么会有Host容器呢?这是因为请求到达Engine容器之前,Mapper组件已经对请求进行了路由处理,Mapper组件通过请求的URL定位了相应的容器,并且把容器对象保存到了请求对象中。
Tomcat的启动过程,具体是由启动类和”高层“组件来完成的,它们都承担着”管理“的角色,负责将子组件创建出来,并把它们拼装在一起,同时也掌握子组件的”生杀大权“。
网友评论