Apache Tomcat源码
Apache Tomcat源码
1 Apache Tomcat源码环境构建
1.1 Apache Tomcat源码下载
https://tomcat.apache.org/download-80.cgi
下载对应的zip包
1.2 Tomcat源码环境配置
1.2.1 增加POM依赖管理文件
解压 apache-tomcat-8.5.63-src压缩包,进src陆慕,创建一个pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>apache-tomcat-8.5.63-src</artifactId>
<name>Tomcat8.5</name>
<version>8.5</version>
<build>
<!--指定源⽬录-->
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<plugins>
<!--引⼊编译插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<encoding>UTF-8</encoding>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
<!--tomcat 依赖的基础包-->
<dependencies>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>javax.xml.soap</groupId>
<artifactId>javax.xml.soap-api</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
</project>
1.2.3 IDEA环境导入与启动
idea导入maven项目,注意环境:
idea: 2020.3
jdk: 11
执行 Bootstrap.java 的main方法即可,非常简单
1)常见错误一
Error:(505, 53) java: 程序包 sun.rmi.registry 不可见 (程序包 sun.rmi.registry 已在模块 java.rmi 中声明, 但该模块未将它导出到未命名模块)
原因:sun的包对ide编译环境不可见造成的,鼠标放在代码中报红的地方,根据idea的提示操作即可。
注意!不要用maven去编译它,这个参数你加入的是idea的环境,所以,用这里:
2)常见错误二
运 行Bootstrap 类的 main 函数,此时就启动了tomcat,启动时候会去加载所配置的 conf 录下 的server.xml等配置 件,所以访问8080端 即可,但此时我们会遇到如下的 个错误
原因是Jsp引擎Jasper没有被初始化,从而无法编译JSP,我们需要在tomcat的源码ContextConfig类中 的configureStart 中增加代码将 Jsp 引擎初始化,如下
org.apache.catalina.startup.ContextConfig#configureStart
webConfig();
//初始化JSP解析引擎
context.addServletContainerInitializer(new JasperInitializer(),null);
if (!context.getIgnoreAnnotations()) {
applicationAnnotationsConfig();
}
if (ok) {
validateSecurityRoles();
}
启动Boostrap文件
http://localhost:8080/
可以看到,tomcat成功启动。
2 Tomcat架构与源码剖析
2.1 Apache Tomcat总体架构
从Tomcat安装目录下的/conf/server.xml 文件里可以看到最顶层的是server。
对照上面的关系图,一个Tomcat实例对应一个server,一个 Server 中有一个或者多个 Service,
一个 Service 中有多个连接器和一个容器,Service组件本身没做其他事
只是把连接器和容器组装起来。连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通 信
Server:Server容器就代表一个Tomcat实例(Catalina实例),其下可以有一个或者多个Service容器;
Service:Service是提供具体对外服务的(默认只有一个),一个Service容器中又可以有多个Connector组件(监听不同端口请求,解析请求)和一个Servlet容器(做具体的业务逻辑处理);
Engine和Host:Engine组件(引擎)是Servlet容器Catalina的核心,它支持在其下定义多个虚拟主机(Host),虚拟主机允许Tomcat引擎在将配置在一台机器上的多个域名,比如www.baidu.com、www.bat.com分割开来互不干扰;
Context:每个虚拟主机又可以支持多个web应用部署在它下边,这就是我们所熟知的上下文对象
Context,上下文是使用由Servlet规范中指定的Web应用程序格式表示,不论是压缩过的war包形式的文件还是未压缩的目录形式;
Wrapper:在上下文中又可以部署多个servlet,并且每个servlet都会被一个包装组件(Wrapper)所包含(一个wrapper对应一个servlet)
去掉注释的server.xml
具体配置如下
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Note: A "Server" is not itself a "Container", so you may not
define subcomponents such as "Valves" at this level.
Documentation at /docs/config/server.html
-->
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!--APR library loader. Documentation at /docs/apr.html -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!-- A "Service" is a collection of one or more "Connectors" that share
a single "Container" Note: A "Service" is not itself a "Container",
so you may not define subcomponents such as "Valves" at this level.
Documentation at /docs/config/service.html
-->
<Service name="Catalina">
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- A "Connector" using the shared thread pool-->
<!--
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
-->
<!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
This connector uses the NIO implementation. The default
SSLImplementation will depend on the presence of the APR/native
library and the useOpenSSL attribute of the
AprLifecycleListener.
Either JSSE or OpenSSL style configuration may be used regardless of
the SSLImplementation selected. JSSE style configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
type="RSA" />
</SSLHostConfig>
</Connector>
-->
<!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
This connector uses the APR/native implementation which always uses
OpenSSL for TLS.
Either JSSE or OpenSSL style configuration may be used. OpenSSL style
configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
maxThreads="150" SSLEnabled="true" >
<UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
<SSLHostConfig>
<Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
certificateFile="conf/localhost-rsa-cert.pem"
certificateChainFile="conf/localhost-rsa-chain.pem"
type="RSA" />
</SSLHostConfig>
</Connector>
-->
<!-- Define an AJP 1.3 Connector on port 8009 -->
<!--
<Connector protocol="AJP/1.3"
address="::1"
port="8009"
redirectPort="8443" />
-->
<!-- An Engine represents the entry point (within Catalina) that processes
every request. The Engine implementation for Tomcat stand alone
analyzes the HTTP headers included with the request, and passes them
on to the appropriate Host (virtual host).
Documentation at /docs/config/engine.html -->
<!-- You should set jvmRoute to support load-balancing via AJP ie :
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
-->
<Engine name="Catalina" defaultHost="localhost">
<!--For clustering, please take a look at documentation at:
/docs/cluster-howto.html (simple how to)
/docs/config/cluster.html (reference documentation) -->
<!--
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
-->
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
via a brute-force attack -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<!-- This Realm uses the UserDatabase configured in the global JNDI
resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately
available for use by the Realm. -->
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
<Host name="localhost2" appBase="webapps2"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
2.2 Apache Tomcat连接器
负责对外交流的连接器(Connector)
连接器主要功能:
1、网络通信
2、用层协议解析读取请求数据
3、将Tomcat 的Request/Response转成标准的Servlet Request/Response
因此Tomcat设计者又设计了三个组件来完成这个三个功能,分别是EndPoint、Processor和Adaptor,其中EndPoint和Processor又一起抽象成ProtocalHandler组件,画图理解下
这里大家先有个印象,下面源码会看到互相之间的调用
下面的源码我们会详细看到处理的转交过程:
Connector 给 handler, handler最终调用 endpoint
Processor 负责提供 Tomcat Request 对象给 Adapter
Adapter 负责提供 ServletRequest 对象给容器
2.3 Apache Tomcat源码剖析
重点分析两个阶段:启动,请求
2.3.1 start.sh如何启动
用过Tomcat的我们都知道,可以通过Tomcat的/bin目录下的脚本startup.sh来启动Tomcat,那么这个脚本肯定就是Tomcat的启动入口了,执行过这个脚本之后发生了什么呢?
1、Tomcat本质上也是一个Java程序,因此startup.sh脚本会启动一个JVM来运行Tomcat的启动类Bootstrap
2、Bootstrap的主要任务是初始化Tomcat的类加载器,并且创建Catalina。
3、Catalina是一个启动类,它通过解析server.xml,创建相应的组件,并调用 Server的start方法
4、Server组件的职责就是管理Service组件,它会负责调用Service的start方法
5、Service组件的职责就是管理连接器和顶层容器Engine,它会调用连接器和 Engine的start方法
6、Engine组建负责启动管理子容器,通过调用Host的start方法,将Tomcat各层容器启动起来(这里是分层级的,上层容器管理下层容器)
2.3.2 生命周期统一管理组件
LifeCycle接口
Tomcat要启动,肯定要把架构中提到的组件进行实例化(实例化创建–>销毁等:生命周期)。
Tomcat中那么多组件,为了统一规范他们的生命周期,Tomcat抽象出了LifeCycle生命周期接口
大家先知道这个内部的类关系,这是一个接口,server.xml 里的哥哥节点都是它的实现类
LifeCycle生命周期接口方法:
源码如下
public interface Lifecycle {
// 添加监听器
public void addLifecycleListener(LifecycleListener listener);
// 获取所有监听器
public LifecycleListener[] findLifecycleListeners();
// 移除某个监听器
public void removeLifecycleListener(LifecycleListener listener);
// 初始化方法
public void init() throws LifecycleException;
......................领略
}
这里我们把LifeCycle接口定义分为两部分
一部分是组件的生命周期方法,比如init()、start()、stop()、destroy()。
另一部分是扩展接口就是状态和监听器。
因为所有的组件都实现了LifeCycle接口,
在父组件的init()方法里创建子组件并调用子组件的init()方法,
在父组件的start()方法里调用子组件的start()方法,
那么调用者就可以无差别的只调用最顶层组件,也就是Server组件的init()和start()方法,整个Tomcat就被启动起来了
2.3.3 Tomcat启动入口在哪里
(1)启动流程图
startup.sh –> catalina.sh start –> java xxxx.jar org.apache.catalina.startup.Bootstrap(main)start(参数)
Bootstrap.init
Catalina.load
Catalina.start
//伪代码:调用关系,我们重点看下面标注的 1 2 3
//startup.bat 或 sh
Bootstrap{
main(){
init(); // 1
load(){ // 2
Catalina.load(){
createServer();
Server.init(){
Service.init(){
Engine.init(){
Host.init(){
Context.init();
}
}
Executor.init();
Connector.init(){ //8080
ProtocolHaldler.init(){
EndPoint.init();
}
}
}
}
}
}
start(){ // 3
//与load方法一致
}
}
}
(2)系统配置与入口
Bootstrap类的main方法
// 知识点【需要debug学习的几个点】
// BootStrap static 块 : 确定Tomcat运行环境的根目录
// main里的init : 入口
// CatalinaProperties: 配置信息加载与获取工具类
// static { loadProperties() }: 加载
2.3.4 Bootstrap的init方法剖析
目标
//1、初始化类加载器 //2、加载catalina类,并且实例化 //3、反射调用Catalina的
setParentClassLoader方法 //4、实例 赋值
//1、初始化类加载器
//2、加载catalina类,并且实例化
//3、反射调用Catalina的setParentClassLoader方法
//4、实例 赋值
public void init() throws Exception {
// 1. 初始化Tomcat类加载器(3个类加载器)
initClassLoaders(); // ===> classloader , 3个
// 用上面生成的catalinaLoader作为启动线程的上下文类加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
// 2. 用loader来实例化Catalina实例【重要节点】
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?>[] paramTypes = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object[] paramValues = new Object[1];
paramValues[0] = sharedLoader;
// 3. 反射调用Catalina的setParentClassLoader方法,
// 将sharedLoader设置为Catalina的parentClassLoader成员变量
// 思考一下,为什么???
// ---- 为了让 Catalina类去加载 shared下的一堆对象,注意看tomcat的classloader继承关系
// 后面的webappLoader,jsploader 都是shared的子加载器。
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
//4、将catalina实例赋值
catalinaDaemon = startupInstance;
// 结论: bootstrap只是个启动点和流程控制,真正干活的,交给了 catalina类,我们下面重点看它的动作
}
2.3.4 Catalina的load方法剖析
org.apache.catalina.startup.Bootstrap#main中的load方法
调用的是catalina中的方法
1)load初始化流程
load(包括下面的start)的调用流程核心技术在于,这些类都实现了 2.3.2 里的 生命周期接口。每个节点自己完成的任务后,会接着调用子节点(如果有的话)的同样的方法,引起链式反应。
反映到流程图如下,下面的debug,包括start我们以图跟代码结合debug:
2)load初始化源码
Catalina的load方法:
// 1. 解析server.xml,实例化各Tomcat组件
// 2. 为Server组件实例设置Catalina相关成员value
// 3. 调用Server组件的init方法,初始化Tomcat各组件
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs(); // 空的,不管他
// Before digester - it may be needed
initNaming(); // 加一堆额外属性值,不管他
// 创建Digester,解析server.xml文件, 这个对象定义了server.xml的结构
Digester digester = createStartDigester(); // ===> server.xml的结构定义
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();// ===> 其实就是server.xml
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}
// 如果没有,找 server-embed.xml
// This should be included in catalina.jar (这货有可能藏在这个jar里)
// Alternative: don't bother with xml, just create it manually.
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}
if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}
try {
inputSource.setByteStream(inputStream);
digester.push(this); // 把当前catalina交给 digester
//1. digester解析server.xml,实例化各Tomcat组件,
// 实例化好后,设置进上面的this,也就是catalina对象中
digester.parse(inputSource);
// parse后,debug变量,仔细看一下 catalina里的值信息,server - service - engine - child(host)
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
// 3. 为Server组件实例设置Catalina相关成员value
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server。
/*
【重要节点】:
回顾上面,解析完的catalina结构,内部完整的包含了子节点的信息,像一棵树
这些所有的节点,都实现了 LifeSycle接口,有统一的方法。
那么,唤起最外层的 server.init() 后,在自己的init完成后,调子节点的 init
引起连锁反应,最终整个体系的 init 完成!
* */
try {
getServer().init();// ===> 3. 调用Server组件的init方法,初始化Tomcat各组件 调用LifecycleBase.init
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
进入到catalina的load方法,即可开启链式反应……
// 1. 解析server.xml,实例化各Tomcat组件
// 2. 为Server组件实例设置Catalina相关成员value
// 3. 调用Server组件的init方法,初始化Tomcat各组件, 开启链式反应的点!
3)关键点
load这里,一堆的节点,其实其他并不重要,我们重点看Connector的init
这涉及到tomcat的一个核心问题: 它到底是如何准备好接受请求的!
try {
// 初始化protocolHandler组件
protocolHandler.init(); // ===> 进!
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
2.3.5 Catalina的start方法剖析
1)start初始化流程
流程图与load过程很相似
2)start启动源码
Catalina的start方法
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
//调用catalina的start方法,启动Tomcat的所有组件
Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
method.invoke(catalinaDaemon, (Object[]) null); // ===> 往catalina追 start方法!
}
/**
* Start a new server instance.
* 启动一个server实例
*/
public void start() {
if (getServer() == null) {
load(); // 检查非空,null的话先load
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
getServer().start();//===> 启动server!!!!!
} 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");
}
// Register shutdown hook
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);
}
}
if (await) {
await(); // ===> 这里留一个尾巴,【关键点!】 2.3.6的秘密就藏在这里
stop(); // 如果 8005 接受到shutdown,上面的await会跳出,到这里 , 一堆停止和释放资源操作,忽略……
}
}
3)关键点
Connector.java 的 start
我们直接把断点打在 Connector.java 的 startInterval()
/**
* 启动一个servier实例(模板方法)
*/
@Override
public final synchronized void start() throws LifecycleException {
// 巴拉巴拉一堆判断和校验操作,不用管它,往下看!
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}
return;
}
if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}
try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
startInternal();//===> 这里!与init同一种模式,触发子类的方法,
// debug into,F7,第一次就是 server ,开始启动!!!!!
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}
2.3.6 请求的处理
启动完就该接受请求了!
那么请求是如何被tomcat接受并响应的???
在调试请求前,必须有个请求的案例,我们先来实现它
1)案例
源码:
DemoServlet.java
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DemoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("-----do get----");
}
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<servlet>
<servlet-name>demoServlet</servlet-name>
<servlet-class>com.itheima.test.DemoServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>demoServlet</servlet-name>
<url-pattern>/test.do</url-pattern>
</servlet-mapping>
</web-app>
debug重启tomcat,访问 http://localhost:8080/demo/test.do
确认控制台打印信息,打断点可以正常进来:
基于请求的环境准备工作完成!
2)url的解析
回顾开篇,server.xml 、 url与对应的容器:
http://localhost:8080/demo/test.do
localhost: Host
8080: Connector
demo: Context
test.do: Url
3)类关系
tomcat靠Mapper来完成对url各个部分的映射
idea追踪MapperElement的继承实现
从MappedHost类打开入口,看拥有的属性和关系
4)接受请求的流程
5)代码追踪
温馨提示:征程开始,下面将是漫长的debug之路。别跟丢了!
代码入口:
@Override
public void startInternal() throws Exception {
// 一堆判断,不去管它,直接整到最下面!
if (!running) {
running = true;
paused = false;
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller threads
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}
startAcceptorThreads(); // ===> 这里!启动线程去接受请求!
}
}
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
// 创建一堆的Acceptor,放入数组,启动它!
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor(); //===> Acceptor其实是EndPoint里的内部类。
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start(); // ===> 查看 NioEndpoint.Acceptor 的run方法! 这里debug是进不去的,它在另一个线程里
// Acceptor实现了Runnable,也就是说,它是一个线程对象,启动它!
}
}
@Override
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// If we are here, means we have other stuff to do
// Do a non blocking select
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error("",x);
continue;
}
// Either we timed out or we woke up, process events first
if (keyCount == 0) {
hasEvents = (hasEvents | events());
}
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// Walk through the collection of ready keys and dispatch
// any active event.
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (socketWrapper != null) {
processKey(sk, socketWrapper); // ===> 【关键节点,请求入口】请求阶段进!
}
}
// Process timeouts
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}
2.3.7 tomcat的关闭
tomcat启动后就一直处于运行状态,那么它是如何保持活动的?又是如何触发退出的?
1)代码追踪
1、标志位全局控制
org.apache.catalina.startup.Bootstrap#main
通过setAwait这个标志位来控制
else if (command.equals("start")) {
daemon.setAwait(true);//用来设置主线程不自动关闭 ===> 开始调用catalina里的方法,都是反射形式
daemon.load(args);//===> 【重点】2、反射调用Catalina#load(args)方法,始化一些资源,优先加载conf/server.xml
daemon.start();//===> 【重点】3、反射调用Catalina.start() 开始启动
//总结
//Tomcat是通过调用Catalina的load()和start()方法,完成了Tomcat的启动
//load()方法负责组件的初始化
//start()方法负责组件的启动
if (null == daemon.getServer()) {
System.exit(1);
}
2、进入到Catalina#start方法
org.apache.catalina.startup.Catalina#start
if (await) {
await(); // ===> 这里留一个尾巴,【关键点!】 2.3.6的秘密就藏在这里
stop(); // 如果 8005 接受到shutdown,上面的await会跳出,到这里 , 一堆停止和释放资源操作,忽略……
}
3、进入到await方法
org.apache.catalina.core.StandardServer#await
重点关注
awaitSocket = new ServerSocket…
@Override
public void await() {
// 监听 8005 socket
// 阻塞等待指令,10s超时,继续循环
// 收到SHUTDOWN ,退出循环
}
结论:通过阻塞来实现主线程存活!
2)操作演练
xml定义的端口 8005
将断点打在 org.apache.catalina.startup.Catalina#start, 下面的 stop() 一行
在命令行键入:telnet ip port 后,然后键入大写的SHUTDOWN。其中port默认为8005
然后输入大写【SHUTDOWN】,会被断点捕获到。
结论:通过使用telnet关闭8005端口也正好印证了上面的 结论。
shutdown.bat和上面的原理也是一样的