Tomcat源码笔记 - Tomcat启动过程分析

背景

我们通常启动Tomcat是从catalina.sh脚本或startup.sh(其实也就是catalina.sh)脚本开始的。
从catalina.sh开始分析Tomcat源码,会发现其启动类是org.apache.catalina.startup.Bootstrap类。
首先,作为一个启动类,解析一些启动参数,读取一些配置,这些都很正常。但再仔细看,有个问题非常奇怪:为什么很多功能只是间接调用org.apache.catalina.startup.Catalina类的类似方法,而且还要用反射的形式?

简述启动流程

启动catalina.sh脚本

虽然直接通过Bootstrap类启动也是可以的,但是catalina.sh脚本提供了很多默认配置和参数,省去我们很多工作。
catalina.sh脚本的主要工作:

  1. 确认环境
  2. 在CLASSPATH中添加bin/bootstrap.jar,这个jar包带有Tomcat的Java main方法
  3. 在CLASSPATH中添加bin/tomcat-juli.jar,这个jar包含有一些启动时会用到的工具类
  4. 通过jre执行bootstrap.jar中的org.apache.catalina.startup.Bootstrap类,至此启动脚本工作结束

Bootstrap类main方法被调用

正如前边所说,其实Bootstrap类也只是一个包装Catalina类的“启动器”。
Bootstrap类的主要工作:

  1. 读catalina.properties配置,生成三个ClassLoader——commonLoadercatalinaLoadersharedLoader,用于加载Tomcat和Web应用需要的类
  2. 通过反射获取org.apache.catalina.startup.Catalina类的实例
  3. 通过反射间接调用Catalina实例的方法,启动或关闭Tomcat

commonLoader
父级ClassLoader是system class loader,属于双亲委派模型中自定义的ClassLoader。
用于加载CATALINA_HOME/lib/*.jar

catalinaLoader
父级ClassLoader是commonLoader。
加载org.apache.catalina.core.*等Tomcat核心类。
在catalinaLoader初始化结束后,Bootstrap会将其parentClassLoader设置为sharedLoader。这一步是为什么?

sharedLoader
父级ClassLoader是commonLoader。

为什么Bootstrap要用反射调用Catalina

回到原问题,这个回答在网上翻了很久也没找到答案。但是Bootstrap类的作者显然也认为这很纠结,并且说明了这么纠结的原因:

The purpose of this roundabout approach is to keep the Catalina internal classes (and any other classes they depend on, such as an XML parser) out of the system class path and therefore not visible to application level classes.
这种迂回方法的目的是将Catalina内部类(以及它们依赖的任何其他类,例如XML解析器)保留在系统类路径之外,因此对应用程序级类不可见。
—— Google翻译

好吧,这个理由我能接受,但是为什么要用那么多反射?明明源码中Bootstrap和Catalina两个类在同一个包下,为何搞得这么费劲?

让我们回到一切的开始,分析使用反射的原因

在catalina.sh脚本中,CLASSPATH添加了一个脚本bin/bootstrap.jar。我们看看这个包里到底都有些什么:

Archive:  bin/bootstrap.jar
Zip file size: 28680 bytes, number of entries: 21
drwxr-xr-x  2.0 unx        0 bx stor 16-Jun-09 14:56 META-INF/
-rw-r--r--  2.0 unx      494 b- defN 16-Jun-09 14:56 META-INF/MANIFEST.MF
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/catalina/
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/catalina/security/
-rw-r--r--  2.0 unx     7901 b- defN 16-Jun-09 14:56 org/apache/catalina/security/SecurityClassLoad.class
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/catalina/startup/
-rw-r--r--  2.0 unx    11868 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/Bootstrap.class
-rw-r--r--  2.0 unx     3385 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/CatalinaProperties.class
-rw-r--r--  2.0 unx     1224 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory$1.class
-rw-r--r--  2.0 unx     1210 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory$2.class
-rw-r--r--  2.0 unx     1052 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory$Repository.class
-rw-r--r--  2.0 unx     1395 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory$RepositoryType.class
-rw-r--r--  2.0 unx     7456 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory.class
-rw-r--r--  2.0 unx     5022 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/Tool.class
-rw-r--r--  2.0 unx     7272 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/catalina.properties
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/naming/
-rw-r--r--  2.0 unx      590 b- defN 16-Jun-09 14:56 org/apache/naming/JndiPermission.class
-rw-r--r--  2.0 unx      172 b- defN 16-Jun-09 14:56 META-INF/NOTICE
-rw-r--r--  2.0 unx    11560 b- defN 16-Jun-09 14:56 META-INF/LICENSE
21 files, 60601 bytes uncompressed, 25594 bytes compressed:  57.8%

和预期一样,这个jar中包含了很多启动Tomcat必须的类,更确切地说,都是Bootstrap类依赖的类。
但是,为什么没有Catalina.class?这个类在哪里?

# 查找一下Catalina.class

[lee@iZ28j1t57giZ tomcat]$ grep "org/apache/catalina/startup/Catalina.class" bin/*
[lee@iZ28j1t57giZ tomcat]$ grep "org/apache/catalina/startup/Catalina.class" lib/*
Binary file lib/catalina.jar matches

这个类在lib/catalina.jar?我们打开这个jar看一下:

Archive:  lib/catalina.jar
Zip file size: 1562369 bytes, number of entries: 744
drwxr-xr-x  2.0 unx        0 bx stor 16-Jun-09 14:56 META-INF/
-rw-r--r--  2.0 unx      391 b- defN 16-Jun-09 14:56 META-INF/MANIFEST.MF
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/
drwxr-xr-x  2.0 unx        0 b- stor 16-Jun-09 14:56 org/apache/catalina/
......
-rw-r--r--  2.0 unx    11868 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/Bootstrap.class    # 甚至这里还有个Bootstrap.class
-rw-r--r--  2.0 unx     1695 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/Catalina$CatalinaShutdownHook.class
-rw-r--r--  2.0 unx    18477 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/Catalina.class
-rw-r--r--  2.0 unx     3385 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/CatalinaProperties.class
-rw-r--r--  2.0 unx     1224 b- defN 16-Jun-09 14:56 org/apache/catalina/startup/ClassLoaderFactory$1.class
......

的确,在lib/catalina.jar
回想一下启动流程,lib/*.jar并没有在一开始的catalina.sh脚本中添加到CLASSPATH中,也就是说这个lib目录中的jar包在启动Bootstrap类之前是不会被加载的。
再回头看一下,lib目录下的jar是由commonLoader加载的。
因此,在Bootstrap类生成commonLoader前,Catalina.class并没有被加载,因此无论是初始化或是调用其方法,都需要使用反射,至此已经解决了最初的问题——为什么要使用反射。

但是另外一个问题又出现了——为什么要将启动必须的类拆分成两个jar?或者说为什么启动还要分Bootstrap和Catalina两步?

回到作者的注释中,看看Bootstrap都做了什么

作者表示他自己也知道这个启动流程很纠结,并提出了一个合理的原因:为了隔离应用的类资源。
说到类加载,很容易联想到Java的双亲委派类加载机制,也许隔离应用资源与这个机制有关,我们看一下类加载的过程。

回顾类加载器的工作机制:

  • 委托:自己的类加载器首先委托父级加载器加载,父级找不到才自己加载
  • 可见性:子类可见父类加载的类,但对父类来说子类加载的类不可见
  • 单一性:父类加载过的类子类不可再加载

加载Bootstrap
由于Bootstrap是jre启动的,而且属于Java应用程序中的类,很明显其类加载器是system class loader。

commonLoader、catalinaLoader和sharedLoader
与Bootstrap类似,这两个类的类加载器是system class loader。
其中:

  • commonLoader加载了Tomcat的基础资源
  • catalinaLoader加载了Catalina.class,并且后续catalinaLoader的父级类加载器被设置为sharedLoader
  • sharedLoader在Catalina启动过程中没有加载内容

类加载器层次
在commonLoader、catalinaLoader、sharedLoader生成并加载前文所说的内容后,类加载器的层次结构包括Catalina在内,是这样:

Tomcat源码笔记 - Tomcat启动过程分析_第1张图片

其中:

  • Bootstrap加载器和System加载器 加载的类对Tomcat和Web应用都可见
  • Common加载器 作为Catalina加载器和Shared加载器的父加载器,加载的类对Tomcat和Web应用也都可见
  • Catalina加载器 加载的类均为Tomcat核心类,对Web应用不可见
  • Shared加载器 加载的类对Tomcat就不可见了,当然,Tomcat也不需要可见

当然,还有高级配置,多个Web应用共享Shared加载器:


Tomcat源码笔记 - Tomcat启动过程分析_第2张图片

这个不是本篇讨论的重点。

总结:正如作者所说,Bootstrap除了作为启动入口以外,做了一件很重要的事情——装载并隔离资源

  1. Tomcat与Web应用需要共用的类资源已由Bootstrap、System、Common三种加载器加载
  2. Tomcat单独需要的类资源由Catalina加载器加载
  3. CatalinaClassLoader在加载完Catalina类后,修改其父级加载器为Shared加载器,并通过反射来调用,完成了Bootstrap类和Web应用(即Catalina类)的解耦
  4. 之后Web应用中新的类加载器加载过程将与CatalinaClassLoader隔离
  5. 有必要的话可以多次实例化Web应用,且可以实现与Web应用/WEB-INF/classes//WEB-INF/lib/目录绑定,并相互隔离
  6. 如果之后有Web应用中需要共享的类,只需要在catalina.properties中配置shared.loader即可
  7. Catalina加载器加载的类对于Web应用这个分支是不可见的

继续看看,Catalina又做了什么

Catalina类的工作比起Bootstrap要简单不少,核心只有两点:生成Server启动Server
没错,其实还是在间接调用其他类。

生成Server
Catalina可以通过读取CATALINA_HOME/conf/server.xml文件,并通过这个配置的结构和内容生成一个Server。
另外,通过Catalina类提供给配置解析类的getter和setter方法,我们实际上已经可以在自己的应用中创建并使用Tomcat了。

启动Server
类似Bootstrap,Catalina的start、stop等方法实际上还是在调用Server的start、stop和destroy等方法。
但是这一次并没有使用反射,而是直接调用。

到此为止,我们基本可以推断启动分为Bootstrap和Catalina两步(甚至加上catalina.sh脚本、Server可以分四步)的原因了。

分析分多步启动的原因

结合Bootstrap的工作和Catalina的工作,我们可以合理推测为什么分步启动。


启动过程
  • 理由1:其实每个类都只提供了一些非必要的功能
    • Tomcat的启动并非必须依赖Bootstrap,甚至不一定需要Catalina
    • 我们完全可以通过自己的配置,直接启动Server即可
    • Catalina为Server启动提供了xml文件解析、校验和shutdownHook等功能,当然我们也可以在自己的应用中调用Catalina的方法启动Server
    • Bootstrap提供了整个程序的启动入口,并进行了资源隔离
    • catalina.sh、Bootstrap、Catalina三者结合为我们提供了一个很好的一键式启动模式
  • 理由2:明确职责,解耦
    • 进行了必要的解耦,使得我们可以定制启动流程,一旦耦合度太高很多功能的复杂度会直线提升
    • 在一键式启动模式中,catalina.sh负责验证和设置参数,Bootstrap作为入口隔离资源,Catalina读xml,职责明确,互不干涉

再回到之前的问题——为什么Bootstrap和Catalina要拆成两个包?

Bootstrap类的职责包含了程序启动入口、资源加载和隔离,这些职责并不是Tomcat必须的,这只是独立使用Tomcat应用时需要的,因此在bin目录下很合理。也就是说,如果你的程序选择内嵌Tomcat,就不需要/bin/bootstrap.jar来启动,所以拆分出去了。

另一方面,Catalina除了通过读xml文件启动Tomcat外,还可以通过getter、setter方法使用Server,其实更像是一个包装类,存在于/lib/catalina.jar目录下更合理。

总的来说,因为这两者职责不同,耦合度不高,所以拆到了/bin目录和/lib目录中,也因此导致了Bootstrap需要通过反射调用Catalina。

小疑问:为什么不直接使用Shared加载器来加载Catalina类?
猜测是Catalina的实例化过程仍然需要依赖Tomcat的核心类,但是Shared加载器没有加载,只好使用Catalina加载器。

补充

Tomcat Web应用中特殊的类资源查找的顺序

Tomcat实现的类加载机制与双亲委派有区别,大致区别是Web应用首先在自己的管理范围内查找,再根据双亲委派机制自上而下委托寻找。不过例外是jre基类是不允许覆盖的。
查找顺序:

  1. JVM Bootstrap加载器管理的类(包括Extension加载器)
  2. /WEB-INF/classes/下的类,也就是业务类
  3. /WEB-INF/lib/下的类,也就是业务依赖的库
  4. System加载器管理的类
  5. Common加载器管理的类

参考资料

Apache Tomcat 9 (9.0.19) - Class Loader How-To
Tomcat拾遗--BootStrap类的静态代码块和反射调用Catalina的意义是什么 - weixin_34409357的博客 - CSDN博客
类加载器的工作原理 - ImportNew

本文搬自我的博客,欢迎参观!

你可能感兴趣的:(Tomcat源码笔记 - Tomcat启动过程分析)