Tomcat启动流程源码分析
从脚本开始:启动的入口
在 Tomcat 的世界里,启动之旅始于启动脚本。对于 Windows 系统,我们使用startup.bat
;而在 Linux 系统中,则是startup.sh
。这些脚本就像是一把钥匙,开启了 Tomcat 服务器的大门。
以startup.sh
为例,它首先检测操作系统,然后获取脚本路径,确保catalina.sh
文件存在且可执行,最后执行catalina.sh start
命令来启动 Tomcat 。其关键代码如下:
eval exec "\"$_RUNJDB\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
-D$ENDORSED_PROP="$JAVA_ENDORSED_DIRS" \
-classpath "$CLASSPATH" \
-sourcepath "$CATALINA_HOME"/../../java \
-Dcatalina.base="$CATALINA_BASE" \
-Dcatalina.home="$CATALINA_HOME" \
-Djava.io.tmpdir="$CATALINA_TMPDIR" \
org.apache.catalina.startup.Bootstrap "$@" start
可以看到,catalina.sh
脚本最终调用 Java 命令来执行 Tomcat 的启动类org.apache.catalina.startup.Bootstrap
。
Bootstrap 类:初始化的基石
在 Tomcat 启动的漫漫长路中,org.apache.catalina.startup.Bootstrap
类堪称是一座坚实的基石。这个类拥有一个public static void main(String args[])
方法,它就像是 Tomcat 启动之旅的指南针,引导着整个启动过程的走向 。当我们启动 Tomcat 时,main
方法所传入的参数通常是start
,这就如同给 Tomcat 下达了出发的指令 。对main
方法进行简化后,我们可以清晰地看到其主要完成了三件大事:
public static void main(String args[]) {
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
// 内部会 创建 commonLoader、catalinaLoader、sharedLoader 三层类加载器
// 然后通过反射实例化 org.apache.catalina.startup.Catalina
bootstrap.init();
}
// 命令行参数解析
String command = "start";
// 根据命令执行不同操作 startd 后台运行,start 阻塞当前线程
if (command.equals("start")) {
daemon.setAwait(true);
// load和start方法内部都是通过反射获取到catalina对象,然后调用catalina的load和start方法
// load方法内会解析Server.xml文件,
// 然后调用org.apache.catalina.core.StandardServer#initInternal,构建 Server、Service、Connector、Engine 等组件树
// org.apache.tomcat.util.digester.Digester#startElement 方法中会创建server对象
daemon.load(args);
// 逐层启动所有组件
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
}
}
初始化:在这一阶段,init
方法被调用,会初始化类加载器、实例化Catalina类。
加载:完成初始化后,load
方法开始发挥作用,它负责解析Server.xml,创建Server对象
响应启动或停止命令:根据main
方法传入的参数,Bootstrap
类会调用相应的方法,启动或者停止tomcat服务。
(一)初始化类加载器
在init
方法中,初始化类加载器是至关重要的一环,它就像是为 Tomcat 打造了一把把独特的 “钥匙”,用于打开不同类和资源的大门 。Tomcat 默认会初始化三种特有的类加载器,它们分别是commonLoader
、catalinaLoader
和sharedLoader
。这三种类加载器各司其职,共同构建起了 Tomcat 的类加载体系。
/**
* 调用入口 org.apache.catalina.startup.Bootstrap#main
* 该方法内会调用initClassLoaders方法初始化类加载器
* 然后通过反射实例化Catalina
*/
public void org.apache.catalina.startup.Bootstrap#init() throws Exception {
// 初始化类加载器
initClassLoaders();
// 通过catalinaLoader类加载器加载catalina类
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
...
// 反射创建catalina实例
method.invoke(startupInstance, paramValues);
}
// 初始化类加载器
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
commonLoader
:它是 Tomcat 所有类加载器的父类类加载器,其配置在catalina.properties
文件中,默认加载路径为{catalina.base}/lib/*.jar,{catalina.home}/lib/*.jar
。这意味着它能够加载 Tomcat 基础目录和安装目录下lib
文件夹中的所有jar
文件,为 Tomcat 的核心功能提供了基础的类库支持 。可以说,commonLoader
就像是一个大型的资源仓库,存放着 Tomcat 运行所必需的各种 “物资”。
catalinaLoader
和sharedLoader
:在默认配置下,catalinaLoader
和sharedLoader
并没有单独设置加载路径,它们实际上都指向了commonLoader
。这就好比两个助手,在没有特殊任务安排时,都协助commonLoader
完成类加载的工作 。不过,在一些特定的场景下,我们也可以对它们进行个性化的配置,让它们承担起更具针对性的类加载任务 。例如,在需要隔离不同 Web 应用所使用的类时,就可以通过配置让catalinaLoader
和sharedLoader
分别加载不同的类库,从而实现类的隔离和共享 。
(二)创建 Catalina 实例
初始化类加载器之后,catalinaLoader
就像是一位技艺精湛的工匠,开始着手加载并创建Catalina
实例 。这一过程通过反射机制来实现,就像是在一个巨大的知识宝库中,通过特定的索引精准地找到并取出所需的知识 。
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
加载类:catalinaLoader
首先使用loadClass
方法加载org.apache.catalina.startup.Catalina
类,这个过程就像是在图书馆中根据书名找到对应的书籍 。
创建实例:然后,通过反射调用该类的无参构造函数,使用newInstance
方法创建Catalina
类的实例,就像是根据书籍中的蓝图制造出相应的产品 。
创建好Catalina
实例后,还需要为其设置一些重要的属性,其中设置父类加载器是关键的一步 。通过反射调用Catalina
的setParentClassLoader
方法,将sharedLoader
实例设置为Catalina
实例的父级类加载器 。这就好比为一个新出生的婴儿找到了一位可靠的监护人,确保其在成长过程中能够获取到正确的 “营养”(类和资源) 。
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
最后,将创建好的Catalina
实例赋值给catalinaDaemon
,这个catalinaDaemon
就像是一个承载着 Tomcat 启动使命的核心载体,后续的加载和启动操作都将围绕它来展开 。可以说,Catalina
实例的创建是 Tomcat 启动过程中的一个重要里程碑,它为后续的各种功能实现奠定了坚实的基础 。
Catalina 类:加载与启动的核心
在 Tomcat 的启动之旅中,Catalina
类无疑占据着核心地位,它就像是一场盛大演出的导演,掌控着整个启动过程的节奏和流程 。当Bootstrap
类完成了初始化和一些前期准备工作后,便将启动的重任交给了Catalina
类 。Catalina
类主要通过load
和start
这两个关键方法,有条不紊地完成 Tomcat 的加载和启动工作,就如同搭建一座高楼,先精心打好地基(加载),再一层一层地向上建造(启动) 。
(一)load 方法:解析与初始化
load
方法堪称是Catalina
类中的一位 “知识渊博的学者”,它的主要职责是解析Server.xml
文件,并对 Tomcat 的核心组件进行初始化,为后续的启动工作筑牢根基 。
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Parse main server.xml
parseServerXml(true);
Server s = getServer();
if (s == null) {
return;
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error(sm.getString("catalina.initError"), e);
}
}
if(log.isInfoEnabled()) {
log.info(sm.getString("catalina.init", Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
}
}
创建 Digester 对象:parseServerXml
方法首先创建了一个Digester
对象,这个Digester
对象就像是一位精通各种语言的翻译官,专门负责解析XML
文件 。Digester
底层采用SAX
(Simple API for XML)解析XML
文件,这种解析方式由 “事件” 驱动,在识别出特定XML
元素时,会执行特定的动作 。例如,当遇到Server
标签时,Digester
会根据预先设定的规则,创建一个StandardServer
对象 。
Digester digester = start ? createStartDigester() : createStopDigester();
在createStartDigester
方法中,会对Digester
进行一系列的配置,比如设置是否进行DTD
(Document Type Definition)规则校验、是否进行节点设置规则校验等 。同时,还会为Digester
添加各种规则,这些规则定义了如何将XML
元素转换为 Java 对象 。例如,添加addObjectCreate
规则,用于在遇到特定的XML
标签时创建相应的 Java 对象;添加addSetProperties
规则,用于设置对象的属性;添加addSetNext
规则,用于建立对象之间的关系 。
protected Digester createStartDigester() {
// Initialize the digester
Digester digester = new Digester();
digester.setValidating(false);
digester.setRulesValidation(true);
Map<Class<?>, List<String>> fakeAttributes = new HashMap<>();
// Ignore className on all elements
List<String> objectAttrs = new ArrayList<>();
objectAttrs.add("className");
fakeAttributes.put(Object.class, objectAttrs);
// Ignore attribute added by Eclipse for its internal tracking
List<String> contextAttrs = new ArrayList<>();
contextAttrs.add("source");
fakeAttributes.put(StandardContext.class, contextAttrs);
// Ignore Connector attribute used internally but set on Server
List<String> connectorAttrs = new ArrayList<>();
connectorAttrs.add("portOffset");
fakeAttributes.put(Connector.class, connectorAttrs);
digester.setFakeAttributes(fakeAttributes);
digester.setUseContextClassLoader(true);
// Configure the actions we will be using
digester.addObjectCreate("Server",
"org.apache.catalina.core.StandardServer",
"className");
digester.addSetProperties("Server");
digester.addSetNext("Server",
"setServer",
"org.apache.catalina.Server");
// 省略其他规则添加
return digester;
}
解析 Server.xml 文件:创建好Digester
对象后,会获取Server.xml
文件的输入流,并将其转换为InputSource
对象 。然后,将当前的Catalina
实例压入Digester
的对象栈顶,这一步就像是给Digester
一个 “指引”,让它在解析XML
文件时知道如何将创建的对象与Catalina
实例关联起来 。接着,调用digester.parse(inputSource)
方法开始解析Server.xml
文件 。在解析过程中,Digester
会根据之前设置的规则,依次创建Server
、Service
、Connector
、Engine
、Host
等核心组件,并为这些组件设置相应的属性 。例如,当解析到Server
标签时,会创建一个StandardServer
对象,并设置其port
、shutdown
等属性;当解析到Service
标签时,会创建一个StandardService
对象,并将其添加到StandardServer
的services
列表中 。
InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
if (generateCode) {
digester.startGeneratingCode();
generateClassHeader(digester, start);
}
digester.parse(inputSource);
初始化核心组件:完成Server.xml
文件的解析后,load
方法会获取解析得到的Server
组件,并为其设置Catalina
实例、Catalina Home
和Catalina Base
等属性 。然后,调用Server
组件的init
方法,对Server
及其包含的Service
、Connector
、Engine
、Host
等组件进行初始化 。在初始化过程中,各个组件会完成一些必要的准备工作,比如创建线程池、初始化网络连接等 。例如,Connector
组件会初始化其对应的ProtocolHandler
,ProtocolHandler
负责处理网络通信,它会创建ServerSocket
来监听指定的端口,为后续接收客户端请求做好准备 。
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server
try {
// 初始化Service、Connector、Engine
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error(sm.getString("catalina.initError"), e);
}
}
(二)start 方法:启动服务
当load
方法完成了组件的加载和初始化后,start
方法就像是一位充满激情的指挥官,下达启动的命令,让 Tomcat 的各个组件开始协同工作,对外提供服务 。
public void start() {
if (getServer() == null) {
load();
}
// Start the new server
try {
// 依次启动下层组件 Server → Service → Connector/Engine → Host → Context
getServer().start();
} catch (LifecycleException e) {
getServer().destroy();
return;
}
// 注册关闭钩子,当 JVM 接收到关闭信号(如 Ctrl+C)时,执行资源释放
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
if (await) {
await();
stop();
}
}
启动 Server 及相关服务:start
方法首先会检查Server
组件是否已经存在,如果不存在,则调用load
方法进行加载和初始化 。然后,调用getServer().start()
方法启动Server
组件 。在Server
组件的start
方法中,会依次启动其包含的Service
组件 。每个Service
组件又会启动其内部的Connector
组件和Container
组件(如Engine
、Host
、Context
、Wrapper
等) 。以StandardService
的startInternal
方法为例,它会先启动Engine
,Engine
的子容器都会被启动,Web 应用的部署也会在这个步骤完成;然后启动Executor
线程池;接着启动MapperListener
;最后启动Connector
组件 。当Connector
组件启动完成后,意味着 Tomcat 可以对外提供请求服务了 。
// 依次启动下层组件 Server → Service → Connector/Engine → Host → Context
getServer().start();
注册关闭钩子:在启动过程中,start
方法还会注册一个关闭钩子(shutdown hook) 。关闭钩子是一个在 JVM 关闭时会被执行的线程,它的作用是在 Tomcat 服务器关闭时,执行一些清理工作,比如释放资源、关闭连接等,以确保服务器能够安全地关闭 。在 Tomcat 中,关闭钩子是通过CatalinaShutdownHook
类实现的,它继承自Thread
类 。当 JVM 接收到关闭信号时,会启动所有注册的关闭钩子,CatalinaShutdownHook
的run
方法会被调用,在这个方法中会调用Server
的stop
方法,停止 Tomcat 服务器的运行 。
// 注册关闭钩子,当 JVM 接收到关闭信号(如 Ctrl+C)时,执行资源释放
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
阻塞监听关闭命令:如果在启动时设置了await
为true
,start
方法会调用await
方法,让 Tomcat 在shutdown
端口阻塞监听关闭命令 。当接收到关闭命令时,会调用stop
方法停止 Tomcat 服务器 。这样可以确保 Tomcat 在接收到关闭信号时,能够有序地停止运行,避免数据丢失或资源泄漏等问题 。
if (await) {
await();
stop();
}
Server 与 Service 组件:启动流程的关键环节
(一)Server 组件的启动
在 Tomcat 的架构中,Server
组件堪称是整个服务器的 “总指挥”,它就像是一个庞大军队的最高统帅,掌控着 Tomcat 服务器的整体生命周期 。Server
组件的启动是 Tomcat 启动流程中的关键步骤,它就像是打响了一场战役的第一枪,为后续的服务启动奠定了基础 。
Server
组件实现了Lifecycle
接口,这就赋予了它生命周期管理的能力 。当Server
组件启动时,会触发一系列的生命周期事件 。在StandardServer
类(Server
组件的标准实现类)的startInternal
方法中,首先会发出CONFIGURE_START_EVENT
事件,这个事件就像是一个信号弹,通知相关的监听器开始进行启动前的配置准备工作 。例如,NamingContextListener
监听器在接收到这个事件后,会实例化 Tomcat 相关的上下文以及ContextResource
资源,为后续的命名服务和资源管理做好准备 。
protected void startInternal() throws LifecycleException {
// 触发start事件
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
setState(LifecycleState.STARTING);
globalNamingResources.start();
// Start our defined Services
synchronized (servicesLock) {
for (Service service : services) {
service.start();
}
}
}
接着,Server
组件会启动globalNamingResources
,这一步就像是为整个服务器搭建了一个资源索引库,主要负责管理全局命名资源,比如 JNDI(Java Naming and Directory Interface)资源 。这些资源可以被 Tomcat 服务器中的其他组件共享和访问,为各种服务的正常运行提供了必要的支持 。例如,Web 应用可以通过 JNDI 获取数据库连接池等资源,从而实现与数据库的交互 。
在启动globalNamingResources
之后,Server
组件会进入一个关键的环节,即启动它所包含的Service
组件 。Server
组件可以包含多个Service
组件,每个Service
组件都像是一个独立的作战单元,负责提供特定的服务 。Server
组件通过遍历services
数组,依次调用每个Service
组件的start
方法,来启动这些 “作战单元” 。这就好比最高统帅命令各个部队依次进入战斗状态,协同作战,共同完成服务器的启动任务 。在启动每个Service
组件时,Server
组件会对servicesLock
进行同步操作,以确保在多线程环境下,Service
组件的启动过程是安全有序的 。
(二)Service 组件的启动
Service
组件在 Tomcat 的体系中扮演着 “桥梁” 的角色,它巧妙地将Connector
组件和Container
组件(如Engine
)连接在一起,使得它们能够协同工作,共同为客户端提供服务 。可以说,Service
组件就像是一个协调各方的项目经理,确保各个团队(组件)之间的合作顺畅 。
Service
组件的启动过程也是围绕着Lifecycle
接口展开的 。在StandardService
类(Service
组件的标准实现类)的startInternal
方法中,首先会设置自身的状态为STARTING
,这就像是在告诉外界,自己即将开始启动服务 。
protected void startInternal() throws LifecycleException {
if(log.isInfoEnabled()) {
log.info(sm.getString("standardService.start.name", this.name));
}
setState(LifecycleState.STARTING);
// Start our defined Container first
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}
mapperListener.start();
// Start our defined Connectors second
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
容器 。Engine
容器是Service
组件中的核心容器,它就像是一个精密的大脑,负责处理客户端的请求,并将请求分发给相应的Host
和Context
。在启动Engine
容器时,Service
组件会对engine
进行同步操作,确保Engine
容器的启动过程是线程安全的 。Engine
容器启动后,它的子容器(如Host
、Context
等)也会被递归启动,这个过程就像是一颗大树从树干开始,逐渐向树枝和树叶蔓延生长,将整个服务体系搭建起来 。在这个过程中,Web 应用的部署也会同步完成,就像是在搭建好的舞台上摆放好各种道具和布景,为演出做好准备 。
接着,Service
组件会启动Executor
线程池 。Executor
线程池就像是一群勤劳的工人,负责为Connector
处理请求提供必要的线程资源 。通过使用线程池,可以有效地提高服务器的并发处理能力,避免在高并发情况下频繁创建和销毁线程带来的性能开销 。Service
组件会遍历executors
列表,依次启动每个Executor
线程池,为后续的请求处理工作做好准备 。
之后,Service
组件会启动MapperListener
。MapperListener
就像是一个智能的导航仪,负责将请求的 URL 映射到对应的容器(如Host
、Context
、Wrapper
等) 。它会监听服务器的状态变化,当有新的请求到来时,能够快速准确地找到处理该请求的容器,确保请求能够被正确地处理 。启动MapperListener
就像是为服务器安装了一个精准的导航系统,使得服务器在处理请求时能够更加高效和准确 。
最后,Service
组件会启动Connector
组件 。Connector
组件是 Tomcat 与客户端进行通信的 “大门”,它负责接收客户端的请求,并将请求转换为 Tomcat 内部能够处理的Request
对象 。Service
组件会遍历connectors
数组,依次启动每个Connector
组件 。在启动Connector
组件时,会先检查其状态,如果状态不是FAILED
,才会启动它 。当所有的Connector
组件启动完成后,Tomcat 服务器就像是一座灯火通明的城堡,准备好迎接来自客户端的各种请求,正式对外提供服务 。
容器组件的启动:Engine、Host 与 Context
(一)Engine 的启动
Engine
容器在 Tomcat 的请求处理流程中扮演着 “导航仪” 的关键角色,它负责将请求精准地路由到对应的Host
容器 。当Engine
容器启动时,在StandardEngine
类的startInternal
方法中,首先会记录服务器的标识信息,这就像是给 “导航仪” 贴上了自己的独特标签,方便识别和管理 。
protected synchronized void startInternal() throws LifecycleException {
// Log our server identification information
if (log.isInfoEnabled()) {
log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
}
// Standard container startup
super.startInternal();
}
然后,Engine
容器会调用父类ContainerBase
的startInternal
方法 。在这个方法中,Engine
容器会启动集群(如果存在),集群就像是一个紧密协作的团队,通过集群可以实现负载均衡和高可用性,提升 Tomcat 的整体性能 。同时,还会启动Realm
,Realm
就像是一个安全卫士,负责进行身份验证和授权,确保只有合法的用户才能访问 Tomcat 服务器 。
protected synchronized void startInternal() throws LifecycleException {
// Start our subordinate components, if any
logger = null;
getLogger();
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}
// Start our child containers, if any
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}
MultiThrowable multiThrowable = null;
for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}
}
if (multiThrowable != null) {
throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
multiThrowable.getThrowable());
}
// Start the Valves in our pipeline (including the basic), if any
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
setState(LifecycleState.STARTING);
// Start our thread
if (backgroundProcessorDelay > 0) {
monitorFuture = Container.getService(ContainerBase.this).getServer()
.getUtilityExecutor().scheduleWithFixedDelay(
new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
}
}
接着,Engine
容器会启动它的子容器,也就是Host
容器 。在这个过程中,Engine
容器会将子容器的启动任务委托给startStopExecutor
线程池 。startStopExecutor
线程池就像是一群勤劳的小蜜蜂,它们会通过调用LifecycleBase#start()
方法去启动子容器 。在LifecycleBase#start()
方法中,会调用StandardHost
的startInternal()
方法,从而正式开始启动Host
容器 。这个过程就像是一场接力赛,Engine
容器将启动的接力棒传递给startStopExecutor
线程池,再由线程池传递给StandardHost
,确保每个环节都紧密相连,顺利完成Host
容器的启动 。
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}
除了启动子容器,Engine
容器还会启动pipeline
。pipeline
就像是一个管道系统,它负责将请求按照一定的顺序传递给各个Valve
进行处理 。在pipeline
中,每个Valve
都像是管道中的一个关卡,它们依次对请求进行处理,完成各种功能,如日志记录、身份验证等 。当pipeline
启动完成后,Engine
容器会将自身的状态设置为STARTING
,并启动线程,这个线程会在后台执行一些周期性的任务,比如检查类的时间戳、Session 对象的超时时间等,确保Engine
容器的稳定运行 。
(二)Host 的启动
Host
容器在 Tomcat 中就像是一个 “虚拟站点管理员”,它代表着一个虚拟主机,负责管理多个Context
容器,就如同管理员管理着多个不同的区域 。当Host
容器启动时,在StandardHost
类的startInternal
方法中,首先会设置错误报告阀门 。错误报告阀门就像是一个 “问题报告员”,当Host
容器出现错误时,它会负责生成错误报告,帮助开发人员快速定位和解决问题 。
protected synchronized void startInternal() throws LifecycleException {
// Set error report valve
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
found = true;
break;
}
}
if(!found) {
Valve valve = ErrorReportValve.class.getName().equals(errorValve) ?
new ErrorReportValve() :
(Valve) Class.forName(errorValve).getConstructor().newInstance();
getPipeline().addValve(valve);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardHost.invalidErrorReportValveClass",
errorValve), t);
}
}
super.startInternal();
}
然后,Host
容器会调用父类ContainerBase
的startInternal
方法 。在这个方法中,Host
容器会执行与Engine
容器启动时类似的操作,比如启动集群(如果存在)、启动Realm
(如果存在) 。之后,Host
容器会启动它的子容器,也就是Context
容器 。同样,Host
容器会将子容器的启动任务委托给startStopExecutor
线程池,由线程池中的线程调用LifecycleBase#start()
方法来启动子容器 。在启动子容器的过程中,Host
容器会遍历children
数组,为每个Context
容器创建一个StartChild
任务,并将这些任务提交到startStopExecutor
线程池 。StartChild
任务会调用Context
容器的start
方法,从而启动Context
容器 。
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (Container child : children) {
results.add(startStopExecutor.submit(new StartChild(child)));
}
在启动过程中,Host
容器还会安装errorReportValue
阀门 。这个阀门是专门用于处理错误报告的,它会在Host
容器的pipeline
中占据一席之地 。当Host
容器处理请求时,如果发生错误,errorReportValue
阀门就会被触发,它会生成详细的错误报告,包括错误的类型、发生的位置等信息,这些信息对于开发人员诊断和修复问题非常有帮助 。同时,Host
容器还会添加一个ErrorDispatcherValve
阀门,这个阀门主要负责处理错误的分发,将错误请求转发到合适的处理程序,确保错误能够得到妥善的处理 。
(三)Context 的启动
Context
容器在 Tomcat 中可以看作是一个 “Web 应用的舞台”,它代表着一个 Web 应用程序,负责管理 Web 应用的各种资源和组件,就如同舞台上的各种道具和演员 。当Context
容器启动时,在StandardContext
类的startInternal
方法中,会执行一系列复杂而关键的步骤 。
protected synchronized void startInternal() throws LifecycleException {
// 创建工作目录
File workDir = new File(System.getProperty("catalina.base"), "work/Catalina/" + getName());
if (!workDir.exists()) {
workDir.mkdirs();
}
// 实例化WebResourceRoot
WebResourceRoot resources = new StandardRoot(this);
setResources(resources);
// 实例化Loader
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
// 启动Loader
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}
// 发出CONFIGURE\_START\_EVENT事件
fireLifecycleEvent(Lifecycle.CONFIGURE\_START\_EVENT, null);
// 启动子容器
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}
// 启动pipeline
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}
}
首先,Context
容器会设置 Web 应用的工作目录 。这个工作目录就像是 Web 应用的 “临时仓库”,用于存储一些临时文件和缓存数据 。例如,JSP 文件在编译后生成的类文件就会存储在这个工作目录中 。Context
容器会根据catalina.base
系统属性和自身的名称来构建工作目录的路径 。如果这个工作目录不存在,Context
容器会自动创建它,确保 Web 应用有一个合适的 “临时仓库” 来存放数据 。
其次,Context
容器会指定一个Loader
。Loader
就像是一个 “知识搬运工”,负责加载 Web 应用的类和资源 。在默认情况下,Context
容器会使用WebappLoader
作为Loader
。WebappLoader
会根据 Web 应用的结构,从WEB-INF/classes
目录和WEB-INF/lib
目录中加载类文件和资源文件 。例如,当 Web 应用需要使用某个类时,WebappLoader
会从相应的目录中找到并加载这个类,为 Web 应用的正常运行提供必要的支持 。
然后,Context
容器会获取字符编码格式 。字符编码格式就像是一种 “语言规范”,确保 Web 应用在处理文本数据时能够正确地解析和显示字符 。Context
容器会从配置文件或者环境变量中获取字符编码格式的设置 。如果没有显式设置,Context
容器会使用默认的字符编码格式 。例如,在处理 HTTP 请求中的参数时,Context
容器会根据设置的字符编码格式来解析参数值,避免出现乱码问题 。
接着,Context
容器会创建临时文件目录 。临时文件目录就像是一个 “临时文件存放区”,用于存储 Web 应用在运行过程中产生的临时文件 。Context
容器会在工作目录下创建一个专门的临时文件目录 。例如,当 Web 应用需要上传文件时,上传的文件会先存储在这个临时文件目录中,等待进一步的处理 。
此外,Context
容器还会发出CONFIGURE_START_EVENT
事件 。这个事件就像是一个 “集结号”,通知相关的监听器开始进行配置和启动工作 。ContextConfig
监听器会处理这个事件,它会从 Web 应用的web.xml
文件或者 Servlet 3.0 的注解配置中读取 Servlet、Filter、Listener 等相关的配置信息 。例如,ContextConfig
监听器会解析web.xml
文件,创建Wrapper
对象,并将其与当前的Context
容器建立关联 。同时,Context
容器还会启动它的子容器,也就是Wrapper
容器 。每个Wrapper
容器代表一个 Servlet,Context
容器会遍历children
数组,启动那些状态不可用的Wrapper
容器 。最后,Context
容器会启动pipeline
,确保请求能够在pipeline
中按照预定的顺序被各个Valve
处理 。
总结:Tomcat 启动流程全貌
Tomcat 的启动流程是一个复杂而有序的过程,从启动脚本的执行,到Bootstrap
类的初始化,再到Catalina
类对核心组件的加载和启动,以及Server
、Service
和容器组件的协同工作,每一个环节都紧密相连,共同构建起了 Tomcat 服务器的运行基础 。在这个过程中,我们深入了解了各个组件的初始化和启动细节,比如类加载器的创建、XML
文件的解析、组件之间的依赖关系等 。理解 Tomcat 的启动流程对于我们优化和维护 Tomcat 服务器具有重要的意义 。通过对启动流程的分析,我们可以找到影响启动速度的瓶颈,从而采取针对性的优化措施,比如合理配置类加载器、优化XML
文件的解析等 。在服务器的日常维护中,了解启动流程也有助于我们快速定位和解决启动过程中出现的问题 。希望本文能够帮助 Java 开发者们更深入地理解 Tomcat 的启动原理,为开发和运维高效稳定的 Web 应用提供有力的支持 。