Tomcat启动流程源码分析
本文基于Tomcat9.0.55源码分析,源码环境搭建教程。
从脚本开始:启动的入口
在 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) {
// 容器启动,内部子容器host、context都会启动
engine.start();
}
}
synchronized (executors) {
for (Executor executor: executors) {
// 线程池启动
executor.start();
}
}
// 管理url映射,从url准确定位对应的servlet
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
// 启动pipeline,内部会启动所有的valve
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;
}
}
// 如果host内没有配置errorReportValve,这里这里往pipeline中添加一个
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处理 。
(四)ServletWrapper 的启动
当Context容器启动其子容器(Wrapper)时,ServletWrapper的初始化过程正式开始。Wrapper是Servlet的容器封装,而ServletWrapper则是其内部用于管理Servlet生命周期的核心组件。以下是其启动的关键流程:
在StandardContext.startInternal()方法中,完成子容器启动后,会通过loadOnStartup(findChildren())方法处理标记为load-on-startup的Servlet 。
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}findChildren()会获取所有Wrapper子容器(每个对应一个Servlet)。loadOnStartup方法会按load-on-startup值排序Wrapper,确保高优先级Servlet先初始化。
loadOnStartup最终会调用Wrapper的initServlet方法,该方法内部通过ServletWrapper完成Servlet的初始化:
private synchronized void initServlet(Servlet servlet)
throws ServletException {
// 核心调用:Servlet的init方法
servlet.init(facade);
}
ServletWrapper封装了Servlet的生命周期,servlet.init(facade)会调用Servlet的init()方法。对于
DispatcherServlet等框架 Servlet,此步骤会触发框架的初始化(如 Spring MVC 的上下文加载)
总结:Tomcat 启动流程全貌
Tomcat 的启动流程是一个复杂而有序的过程,从启动脚本的执行,到Bootstrap类的初始化,再到Catalina类对核心组件的加载和启动,以及Server、Service和容器组件的协同工作,每一个环节都紧密相连,共同构建起了 Tomcat 服务器的运行基础 。在这个过程中,我们深入了解了各个组件的初始化和启动细节,比如类加载器的创建、XML文件的解析、组件之间的依赖关系等 。理解 Tomcat 的启动流程对于我们优化和维护 Tomcat 服务器具有重要的意义 。通过对启动流程的分析,我们可以找到影响启动速度的瓶颈,从而采取针对性的优化措施,比如合理配置类加载器、优化XML文件的解析等 。在服务器的日常维护中,了解启动流程也有助于我们快速定位和解决启动过程中出现的问题 。希望本文能够帮助 Java 开发者们更深入地理解 Tomcat 的启动原理,为开发和运维高效稳定的 Web 应用提供有力的支持 。