tomcat是做java web开发的开发者经常所使用的支持并运行servlet的一个中间件,不管项目是基于spring、springboot或者是纯servlet的,只要用到servlet,多多少少都离不开tomcat。经常使用tomcat而不了解其自身结构,会让开发者在处理tomcat抛出自身异常的时候变得束手无策。我也如此,在18年还在实习的时候,面对catalina.out抛出的众多的org.apache.catalina.LifecycleException
变得束手无策,明明端口没有占用,为什么tomcat就无法启动呢?明明tomcat已经启动成功了,但为什么就是无法访问,甚至返回http 502呢?抱着好奇的态度,我去github上找到了tomcat的源码,下载并抱着专(gou)心(pi)的态度研究。直到今年疫情隔离的时候,我才有幸看懂tomcat,以此来记录我在tomcat中所获得的学习经验(拖了整整半年才开始写第一章..)。
Github : https://github.com/apache/tomcat
tips : master分支是持续进行更新的,也就是develop版本,稳定的版本在分支上有对应的tag。
1.Tomcat结构描述
要开始进行源码阅读,首先必须要熟悉tomcat的结构目录
注意:是release的结构目录,而不是工程目录
tomcat 目录描述
- bin目录存放了可执行脚本及程序入口的jar包
- conf目录存放了tomcat的配置文件,可以配置tomcat的安全权限策略、catalian的属性配置、context的路径、server的端口等(最常用的)、web.xml的全局配置等配置项
- lib目录存放的则是tomcat分模块生成的jar包以及tomcat对其他库的依赖包等
- logs目录存放的是记录tomcat运行的日志文件
- temp目录存放的是tomcat的缓存文件,如java.io.tempdir配置路径等
- webapps目录存放的是servlet工程,也就是context
tomcat 源码构建并导入IDE
- 确保安装了ant,并且在cmd里ant是可用的
- 确保安装了jdk8(废话太多…)
使用git,clone源码后,得到以下工程目录:
得到源码后,我们首先要做的事是:
- 阅读BUILDING.txt,你想要的各种构建姿势文档里都有描述
- 阅读README.md(我觉得这是混迹github上的最必不可少的操作,你要去学习(piao)各种资源时,作者写的readme文档都会让你事半功倍)
- copy build.properties.default一份到当前目录,修改名称为去掉default就行,该配置文件用与构建tomcat时,下载的第三方构建包存放的目录
准备好以上三步后,开始进行操作。拷贝build.properties.default重名为build.properties,找到base.path
属性项,原属性路径指向为${user.home}/tomcat-build-libs
,我不想让他放在我的C盘中,所以我写死了路径到tomcat工程目录下
执行ant
进行编译生成,执行时ant会去下载bnd、cglib、commons-daemon、ecj、jaxrpc、junit、objenesis、tomcat-native、wsdl4j
等依赖包,由于节点是在国外,所以这一步的操作非常慢,有条件可以挂一个vpn,build成功后,执行ant ide-eclipse
,生成eclipse项目的配置文件,接下来就可以导入eclipse或者IntelliJ里面了
导入进eclipse或者IntelliJ后,会显示缺失很多包依赖,这个时候就需要手动的去解决包路径的问题,如在eclipse中build-path找到tocmat-build-libs对应的jar包的路径,在idea中也是如此,解决了包依赖的问题,找到org.apache.catalina.startup.Bootstrap
的main函数运行就可以启动tomcat了,eclipse中会弹出command选择项,如start、stop等,在idea中就需要指定VM options : start
tomcat 启动流程
在涉及到启动流程时,要先清楚tomcat的组件有那些,这一点可以通过conf目录下的server.xml得到
- Server 代表tomcat本身
- Listener 监听器
- GlobalNamingResources JNDI资源
- Service 服务
- Executor 线程池
- Connector 连接器
- Engine 虚拟出来的主机
- Cluster 用于tomcat集群的配置
- Host 虚拟出来的Host
- Valve 阀
tomcat的组件其实不止这些,但这些是经常用的,在后续学习进度中再继续记录其他容器吧,目前提到的这些是要涉及到启动流程时使用的
一般外置tomcat启动是执行bin目录下的startup.sh | bat脚本(windows安装包安装的除外),startup脚本会先进行操作系统的判断,用于解析不同平台下的soft-link的问题,解析完soft-link后,再判断catalina.sh是否是存在的,如果存在则执行catalina.sh start 的操作并将所有参数传给catalina.sh |
执行catalina脚本会先判断操作系统平台,并tomcat目录路径赋值到CATALINA_HOME
,在启动的时候会将该值注入到tomcat中。并判断catalina.out文件是否存在,如果不存在则创建。
将log配置文件路径、JAVA_OPTS(java的配置项)、CATALINA_OPTS(catalina的配置项)、以及java.io.tempdir路径和刚刚从startup.sh脚本中传过来的所有参数带入到org.apache.catalina.startup.Bootstrap
中,并传入一个start的字符串参数,这个会在main方法中用到,并以后台的方式启动,这个时候tomcat进入启动状态中了
Bootstrap在jvm初始化类中就开始执行tomcat上下文路径的配置了
static {
// Will always be non-null
// 获取tomcat路径
String userDir = System.getProperty("user.dir");
// Home first
// CATALINA_HOME_PROP=catalina.home
String home = System.getProperty(Globals.CATALINA_HOME_PROP);
File homeFile = null;
if (home != null) {
File f = new File(home);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
// 第一次失败 先检查当前目录是否是在bin目录下
if (homeFile == null) {
// First fall-back. See if current directory is a bin directory
// in a normal Tomcat install
File bootstrapJar = new File(userDir, "bootstrap.jar");
// 如果是在bin目录下,则去到上一级,并获取他的绝对路径
if (bootstrapJar.exists()) {
File f = new File(userDir, "..");
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
}
// 第二次失败 直接使用当前路径
if (homeFile == null) {
// Second fall-back. Use current directory
File f = new File(userDir);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
// 将tomcat路径赋值到System中,供其他类调用
catalinaHomeFile = homeFile;
System.setProperty(
Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
// catalina.base路径,如果从脚本中获取为null
// 则直接将catalina.home属性赋值给catalina.base
String base = System.getProperty(Globals.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
// 并保存到System属性中
System.setProperty(
Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}
配置好catalina的路径后,进入到main方法,在catalina脚本中传入的start字符串参数在下面就会被用到
public static void main(String args[]) {
// 锁住当前实例 daemonLock是一个final static的Object对象
synchronized (daemonLock) {
// 线程安全的Bootstrap所持有的实例
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
// 初始化ClassLoader
// 使用反射机制调用Catalina类中的setParentClassLoader方法将ClassLoader注入
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// 设置Catalina的ClassLoader到当前线程的上线文类加载器中
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
try {
// 默认command为start,以防止没有传入action的参数
String command = "start";
if (args.length > 0) {
// 从脚本中获取传来的action
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
// 以下的三个方法全是反射调用的catalina
// 调用catalina的setAwait方法,用于阻塞Server
daemon.setAwait(true);
// 使用Digester初始化容器实例,并执行容器的init
daemon.load(args);
// 启动容器
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
篇幅有点长,今天暂时不想写了…. 先mark住,等明天再来分析Server到Wrapper的启动加载