一、启动流程
在我们初学Tomcat的时候,会去官网下载,解压之后运行bin目录下的startup.sh/startup.bat。
1、Tomcat是用Java编写的一个程序,所以是需要在JVM上运行,通过启动脚本来运行Tomcat的启动类Bootstrap。
2、Bootstrap是一个启动类,主要任务就是初始化Tomcat的类加载器,并且创建Catalina。
3、Catalina是一个启动类,它回去解析server.xml,创建对应的组件,并调用Server的start方法。
4、Server组件负责管理Service组件,调用Service的start方法。
5、Service组件的职责就是管理连接器和顶层容器Engine,所以它会调用Engine和连接器的start方法。
由此可见,Tomcat的顶层们各有各自负责的事情,它们并不处理具体的请求,它们做的事"管理"。
二、Catalina的start方法
Catalina负责创建Server,解析server.xml中的<server></server>
创建Server,并将Server里面的组件都创建出来,紧接着调用Server组件的init方法和start方案,这样整个Tomcat就启动起来了。Tomcat还会监听停止操作,例如使用者主动停止,异常停止等,当发生这些情况是,需要优雅停止并且清理资源,此时JVM钩子就可以派上用场了,注册一个JVM关闭钩子,可以在JVM停止的时候进行一些处理,例如资源的回收。像我们在使用线程池的时候也可以注册一个关闭钩子,优雅关闭线程池回收资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public void start() {
// 如果Server实列为空,那么就解析server.xml创建出来
if (this.getServer() == null) {
this.load();
}
// 如果创建失败,报错退出
if (this.getServer() == null) {
log.fatal(sm.getString("catalina.noServer"));
} else {
long t1 = System.nanoTime();
try {
// 启动Server
this.getServer().start();
} catch (LifecycleException var6) {
log.fatal(sm.getString("catalina.serverStartFail"), var6);
try {
this.getServer().destroy();
} catch (LifecycleException var5) {
log.debug("destroy() failed for failed Server ", var5);
}
return;
}
if (log.isInfoEnabled()) {
log.info(sm.getString("catalina.startup", new Object[]{Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))}));
}
// 创建注册钩子
if (this.useShutdownHook) {
if (this.shutdownHook == null) {
this.shutdownHook = new Catalina.CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager)logManager).setUseShutdownHook(false);
}
}
// 监听停止请求
if (this.await) {
this.await();
this.stop();
}
}
}
|
三、Server组件
Server组件的实现类是StandardServer。Server继承了LifeCycleBase,因此生命周期被统一管理,并且它的子组件是Service,因此需要管理Service的生命周期,也就是在Server组件需要调用Service的启动和停止的方法。因此在Server内部维护者若干个Service组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public void addService(Service service) {
service.setServer(this);
synchronized(this.servicesLock) {
// 创建一个新的长度+1的数组
Service[] results = new Service[this.services.length + 1];
// 把旧数组复制到新的数组上
System.arraycopy(this.services, 0, results, 0, this.services.length);
results[this.services.length] = service;
this.services = results;
// 启动service
if (this.getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException var6) {
}
}
// 出发监听事件
this.support.firePropertyChange("service", (Object)null, service);
}
}
|
可见当添加一个新的Service,会创建一个新的数组长度是原数组的长度+1,并把原数组的内容复制过去,然后再把新的Service添加进新数组,这样做的目的是为了节省内容空间。
Server组件还有一个重要的任务就是启动一个Socket来监听停止端口,所以我们可以通过Tomcat提供的停止脚本来停止Tomcat。在上面的Catalina的start方法最后调用了await方法,Catalina的await方法只是再调用Server的await方法:
1
2
3
4
|
// Catalina.java
public void await() {
this.getServer().await();
}
|
在 await 方法里会创建一个 Socket 监听 8005 端口(server.xml),并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。
四、Service组件
Tomcat中Service是个接口,具体的实现类是StandardService。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class StandardService extends LifecycleMBeanBase implements Service {
private static final Log log = LogFactory.getLog(StandardService.class);
// Service的name
private String name = null;
private static final StringManager sm = StringManager.getManager("org.apache.catalina.core");
// Server实例
private Server server = null;
protected final PropertyChangeSupport support = new PropertyChangeSupport(this);
// 连接器数组
protected Connector[] connectors = new Connector[0];
private final Object connectorsLock = new Object();
protected final ArrayList<Executor> executors = new ArrayList();
// 对应的Engine容器
private Engine engine = null;
private ClassLoader parentClassLoader = null;
// 映射器以及其监听器
protected final Mapper mapper = new Mapper();
protected final MapperListener mapperListener = new MapperListener(this);
//....
}
|
StandardService 继承了 LifecycleBase 抽象类,此外 StandardService 中还有一些我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。
因为Tomcat支持热部署,当Web应用的部署发生变化时,Mapper中的映射信息也要跟着变化,MapperListener就是一个监听器,监听容器的变化,并把信息更新到Mapper中,这是典型的观察者模式。
又到我们常说的Tomcat的组件时上层控制下层的启动,那么来看一下Service的启动方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
protected void startInternal() throws LifecycleException {
//1. 触发启动监听器
setState(LifecycleState.STARTING);
//2. 先启动 Engine,Engine 会启动它子容器
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
//3. 再启动 Mapper 监听器
mapperListener.start();
//4. 最后启动连接器,连接器会启动它子组件,比如 Endpoint
synchronized (connectorsLock) {
for (Connector connector: connectors) {
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}
|
Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。
五、Engine组件
Engine 本质是一个容器,在Tomcat中Engine是个接口实现类是StandardEngine,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口。
Engine 的子容器是 Host,所以它持有了一个 Host 容器的数组,这些功能都被抽象到了 ContainerBase 中,在ContainerBase中有个一个Map来存储
1
|
protected final HashMap<String, Container> children = new HashMap<>();
|
ContainerBase 用 HashMap 保存了它的子容器,并且 ContainerBase 还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,
1
2
3
4
5
6
7
8
|
// StandardEngine.java
protected synchronized void startInternal() throws LifecycleException {
if (log.isInfoEnabled()) {
log.info(sm.getString("standardEngine.start", new Object[]{ServerInfo.getServerInfo()}));
}
// 调用父类(ContainerBase)的启动方法
super.startInternal();
}
|
在ContainerBase中会用专门的线程池来启动子容器。如下:
1
2
3
4
|
for(int var7 = 0; var7 < var6; ++var7) {
Container child = var5[var7];
results.add(this.startStopExecutor.submit(new ContainerBase.StartChild(child)));
}
|
Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
final class StandardEngineValve extends ValveBase {
public StandardEngineValve() {
super(true);
}
public final void invoke(Request request, Response response) throws IOException, ServletException {
Host host = request.getHost();
if (host != null) {
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
host.getPipeline().getFirst().invoke(request, response);
}
}
}
|
这个基础阀实现非常简单,就是把请求转发到 Host 容器。从代码中可以看到,处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。
六、总结
Tomcat的启动过程是启动类和顶层组件来完成,他们承担着管理的角色,负责将子组件创建出来,并拼装在一起,同时掌控着子组件的“生老病死”。
在我们设计组件的时候应该思考用那些合适的数据接口来存储,像用数组来存储Service组件,每次都新建数组,复制,添加新Service组件。数组的结构简单,占用的内存小。使用HashMap来保存子容器,虽然Map占用的内存多一点,但是一次性哈希就能快速找到子容器,这就是空间换时间。