1 Apache Tomcat源码环境构建
1.1 Apache Tomcat源码下载
https://tomcat.apache.org/dow...
环境:jdk11
下载对应的zip包
下载到本地任意磁盘下
1.2 Tomcat源码环境配置
1.2.1 增加POM依赖管理文件
解压 apache-tomcat-8.5.63-src压缩包,
得到⽬录 apache-tomcat-8.5.63-src 进⼊ apache-tomcat-8.5.63src ⽬录,创建⼀个pom.xml⽂件,
⽂件内容如下
4.0.0
org.apache.tomcat
apache-tomcat-8.5.63-src
Tomcat8.5
8.5
Tomcat8.5
java
java
org.apache.maven.plugins
maven-compiler-plugin
3.1
UTF-8
11
org.easymock
easymock
3.4
ant
ant
1.7.0
wsdl4j
wsdl4j
1.6.2
javax.xml
jaxrpc
1.1
org.eclipse.jdt.core.compiler
ecj
4.5.1
javax.xml.soap
javax.xml.soap-api
1.4.0
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的环境,所以,用idea编译和启动
2)常见错误二
原因:jdk版本的事,选jdk11
file - project structure
3)常见错误三
运⾏ 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();
}
...................略
启动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
虚拟主机
把webapps复制一份,叫webapps2,然后修改里面ROOT的index.jsp , 随便改一下
修改web.xml添加虚拟主机,参考下面:(记得把 localhost2 加入到 hosts文件中)
重启访问 http://localhost2/ 试试,和localhost对比一下
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()。
另一部分是扩展接口就是状态和监听器。
tips: (画图便于理解)
因为所有的组件都实现了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(参数)
tips:
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();
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. 实例化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成员变量
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
//4、将catalina实例赋值
catalinaDaemon = startupInstance;
}
2.3.4 Catalina的load方法剖析
tips
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各组件, 开启链式反应的点!
3)关键点
load这里,一堆的节点,其实其他并不重要,我们重点看Connector的init
这涉及到tomcat的一个核心问题: 它到底是如何准备好接受请求的!
// Connector.java:
initInternal(){
//断点到这里!
protocolHandler.init(); // ===> 开启秘密的地方
}
2.3.5 Catalina的start方法剖析
1)start初始化流程
流程图
与load过程很相似
2)start启动源码
Catalina的start方法
/**
* 反射调用Catalina的start方法
*
* @throws Exception Fatal start error
*/
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(){
getServer.start(); // ===> 核心点
}
3)关键点
Connector.java 的 start
我们直接把断点打在 Connector.java 的 startInterval()
Connector(){
startInterval() {
//断点打到这里!
protocolHandler.start();
}
}
//最终目的:发现在 NioEndpoint.Acceptor.run() 里, socket.accept来等待和接受请求。
//至此启动阶段结束!
2.3.6 请求的处理
启动完就该接受请求了!
那么请求是如何被tomcat接受并响应的???
在调试请求前,必须有个请求的案例,我们先来实现它
1)案例
源码:
DemoServlet.java
package com.itheima.test;
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
demoServlet
com.itheima.test.DemoServlet
demoServlet
/test.do
debug重启tomcat,访问 http://localhost:8080/demo/te...
确认控制台打印信息,打断点可以正常进来:
基于请求的环境准备工作完成!
2)url的解析
回顾开篇,server.xml 、 url与对应的容器:
http://localhost:8080/demo/te...
localhost: Host
8080: Connector
demo: Context
test.do: Url
3)类关系
tomcat靠Mapper来完成对url各个部分的映射
- idea追踪MapElement的继承实现
- 从MappedHost类打开入口,看拥有的属性和关系
4)接受请求的流程
5)代码追踪
温馨提示:征程开始,下面将是漫长的debug之路。别跟丢了!
代码入口:
NioEndpoint:
// 真正的入口:
NioEndPoint.Poller{
run(){
//断点打在这里!!!
processKey(sk, socketWrapper);
}
}
2.3.7 tomcat的关闭
tomcat启动后就一直处于运行状态,那么它是如何保持活动的?又是如何触发退出的?
1)代码追踪
1、标志位全局控制
org.apache.catalina.startup.Bootstrap#main
通过setAwait这个标志位来控制
else if (command.equals("start")) {
daemon.setAwait(true);//主线程是否退出全局控制阈值
daemon.load(args);//2、调用Catalina#load(args)方法,始化一些资源,优先加载conf/server.xml
daemon.start();//3、调用Catalina.start()开始启动
2、进入到Catalina#start方法
org.apache.catalina.startup.Catalina#start
.................................略
if (await) {
await();
stop();
}
}
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和上面的原理也是一样的
如果本文对您有帮助,欢迎
关注
和点赞
`,您的支持是我坚持创作的动力。转载请注明出处!