一、Tomcat源码调试配置

1、下载

Tomcat的构建是基于Ant和Eclipse的,然而现在很多人都喜欢IDEA+Maven的项目构建方式,所以本文将基于这个环境来搭建源码的调试。我们需要以下工具:

Tomcat源码下载地址 https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.23/src/apache-tomcat-8.5.23-src.tar.gz
IDEA工具 https://www.jetbrains.com/idea/download
MAVEN http://maven.apache.org/download.cgi
JDK 自然不用多提了,但是要按照所选源码要求的版本,这里用的是JDK8

安装和下载这些软件包就可以开始搭建调试环境了。

2、项目结构

新建一个目录,比如:workspace\tomcat8,然后将tomcat8的源码解压至该目录
Tomcat - 图1
新建catalina-home目录,然后将apache-tomcat-8.5.23-src目录下的 conf文件夹拷贝到此处,该目录结构如下
Tomcat - 图2
除了conf目录其他都是可选的,webapps用于我们应用默认的部署目录,work logs是启动Tomcat自动生成的,其结构跟我们下载的二进制Tomcat程序是一样的.
配置Maven依赖
我们采用module的形式来组织目录,首先在根目录(D:\code\tomcat8)下创建pom.xml,其内容如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>gxf</groupId>
  7. <artifactId>apache-tomcat-8</artifactId>
  8. <name>apache-tomcat-8-source</name>
  9. <version>1.0</version>
  10. <packaging>pom</packaging>
  11. <modules>
  12. <module>apache-tomcat-8.5.33-src</module>
  13. </modules>
  14. </project>

这里主要指定module为Tomcat的源码目录,然后在apache-tomcat-8.5.23-src配置Tomcat源码额外的依赖,在该目录创建pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>org.apache.tomcat</groupId>
  7. <artifactId>Tomcat8.5.33</artifactId>
  8. <name>Tomcat8.5.33</name>
  9. <version>8.5</version>
  10. <build>
  11. <finalName>Tomcat8.0</finalName>
  12. <sourceDirectory>java</sourceDirectory>
  13. <testSourceDirectory>test</testSourceDirectory>
  14. <resources>
  15. <resource>
  16. <directory>java</directory>
  17. </resource>
  18. </resources>
  19. <testResources>
  20. <testResource>
  21. <directory>test</directory>
  22. </testResource>
  23. </testResources>
  24. <plugins>
  25. <plugin>
  26. <groupId>org.apache.maven.plugins</groupId>
  27. <artifactId>maven-compiler-plugin</artifactId>
  28. <version>2.0.2</version>
  29. <configuration>
  30. <encoding>UTF-8</encoding>
  31. <source>1.8</source>
  32. <target>1.8</target>
  33. </configuration>
  34. </plugin>
  35. </plugins>
  36. </build>
  37. <dependencies>
  38. <dependency>
  39. <groupId>org.easymock</groupId>
  40. <artifactId>easymock</artifactId>
  41. <version>3.5</version>
  42. <scope>test</scope>
  43. </dependency>
  44. <dependency>
  45. <groupId>junit</groupId>
  46. <artifactId>junit</artifactId>
  47. <version>4.12</version>
  48. <scope>test</scope>
  49. </dependency>
  50. <dependency>
  51. <groupId>ant</groupId>
  52. <artifactId>ant</artifactId>
  53. <version>1.7.0</version>
  54. </dependency>
  55. <dependency>
  56. <groupId>wsdl4j</groupId>
  57. <artifactId>wsdl4j</artifactId>
  58. <version>1.6.2</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>javax.xml</groupId>
  62. <artifactId>jaxrpc</artifactId>
  63. <version>1.1</version>
  64. </dependency>
  65. <dependency>
  66. <groupId>org.eclipse.jdt.core.compiler</groupId>
  67. <artifactId>ecj</artifactId>
  68. <version>4.6.1</version>
  69. </dependency>
  70. </dependencies>
  71. </project>

3、准备构建

使用IDEA打开tomcat8下面的pom.xml
Tomcat - 图3
配置编译环境
说明:如果编译build的时候出现Test测试代码报错,删除该代码即可。本文中的Tomcat源码util.TestCookieFilter类会报错,将其删除即可。也可以把Test目录删除。
在项目启动配置的VM options中添加如下参数

  1. -Dcatalina.home=catalina-home1
  2. -Dcatalina.base=catalina-home1
  3. -Djava.endorsed.dirs=catalina-home1/endorsed
  4. -Djava.io.tmpdir=catalina-home1/temp
  5. -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
  6. -Djava.util.logging.config.file=catalina-home1/conf/logging.properties

Tomcat - 图4
启动tomcat
Tomcat - 图5
启动成功后访问127.0.0.1:8080/
Tomcat - 图6
原因是我们直接启动org.apache.catalina.startup.Bootstrap的时候没有加载org.apache.jasper.servlet.JasperInitializer,从而无法编译JSP。这在Tomcat6/7是没有这个问题的。解决办法是在tomcat的源码org.apache.catalina.startup.ContextConfig中手动将JSP解析器初始化:
image.png
默认情况下我们必须把应用程序部署到catalina-home/webapps下,如何直接部署访问外部的应用程序?
我们只需要修改server.xml和web.xml配置即可。

  1. 编辑
  2. catalina-home/conf/server.xml
    1. <Context path="/tomcatsrc-web" reloadable="true" debug="0"
    2. docBase="c:\code\work\tomcatsrc2018\tomcatsrc-web\target\tomcatsrc-web-1.0-SNAPSHOT"
    3. workDir="c:\code\work\tomcatsrc2018\tomcatsrc-web\target\tomcatsrc-web-1.0-SNAPSHOT\work"
    4. crossContext="true" />

    二、Tomcat总体架构

    1、Tomcat历史

    Tomcat最初有sun公司的架构师James Duncan Davidson开发,名称”JavaWebServer”。
    1999与Apache软件基金会旗下的JServ项目合并,也就是Tomcat。
    2001 tomcat4.0 里程碑式的版本。完全重新设计了其架构,并实现了Servlet2.3和JSP 1.2规范。
    到目前,Tomcat已经成为成熟的Servlet容器产品,并作为 JBoss等应用的服务器的内嵌Servlet容器
规范JDK版本 6.X 7.X 8.X 8.5.X 9.X
JDK >=5.0 >=6.0 >=7.0 >=7.0 >=8.0
Servlet 2.5 3.0 3.1 3.1 4.0
JSP 2.1 2.2 2.3 2.3 2.3
EL 2.1 2.2 3.0 3.0 3.0
WebSocket N/A 1.1 1.1 1.1 1.1

Tomcat许可:完全免费,修改后不必公开源代码

2、总体结构演变

单个Server

Server:接受请求并解析,完成相关任务,返回处理结果
通常情况下使用Socket监听服务器指定端口来实现该功能,一个最简单的服务设计如下: Tomcat - 图8Start():启动服务器,打开socket连接,监听服务端口,接受客户端请求、处理、返回响应
Stop():关闭服务器,释放资源
缺点:请求监听和请求处理放一起扩展性很差(协议的切换 tomcat独立部署使用HTTP协议,与Apache集成时使用AJP协议)

改进:网络协议与请求处理分离

Tomcat - 图9一个Server包含多个Connector(链接器)和Container(容器)
Connector:开启Socket并监听客户端请求,返回响应数据;
Container:负责具体的请求处理
缺点:Connector接受的请求由那个Container处理,需要建立映射规则

改进:一个Server可以包含多个Service

Tomcat - 图10一个Server可以包含多个Service,每一个Service都是独立的,他们共享一个JVM以及系统类库。
一个Service负责维护多个Connector和一个Container,这样来自Connector的请求只能由它所属的Service维护的Container处理。
在这里Container是一个通用的概念,为了明确功能,并与Tomcat中的组件名称相同,可以将Container命名为Engineer Tomcat - 图11

改进:一个Engine可以处理多个Web应用

在Engine容器中需要支持管理WEB应用,当接收到Connector的处理请求时,Engine容器能够找到一个合适的Web应用来处理,因此在上面设计的基础上增加Context来表示一个WEB应用,并且一个Engine可以包含多个Context。 Tomcat - 图12缺点:应用服务器需要将每个域名抽象为一个虚拟主机,

改进:Engine对应Host

Tomcat - 图13

改进:一个web应用中包含多个Servlet实例

在一个web应用中,可以包含多个Servlet实例来处理来自不同的链接请求,因此我们还需要一个组件概念来表示Servlet定义,即Wrapper。 Tomcat - 图14

定义统一接口Container

在前面的多次Container容器中,有Engine、Host、Context、Wrapper等,可以理解为Container的子类. Tomcat - 图15容器之间的组合关系是一种弱依赖,用虚线表示。

定义一个通用的LifeCycle接口

每一个组件都有启动、停止等生命周期方法,拥有生命周期的特征。所以定义一个通用的LifeCycle接口, Tomcat - 图16

最新架构

Tomcat - 图17

Pipeline和Value

Tomcat - 图18

Connector设计

功能:
监听服务器端口,读取来自客户端的请求
使用指定的协议解析请求数据
根据请求地址匹配正确的容器解析处理
将响应返回给客户端

3、类加载器

双亲委派机制 Tomcat - 图19

4、生命周期接口静态结构图

Tomcat - 图20

5、Tomcat启动过程时序图

Tomcat - 图21Tomcat - 图22

6、Tomcat的web请求和处理时序图

Tomcat - 图23Tomcat - 图24

三、源码分析

容器类图

Tomcat - 图25

初始化过程时序图

Tomcat - 图26

  • Digester利用jdk提供的sax解析功能,将server.xml的配置解析成对应的Bean,并完成注入,比如往Server中注入Service
  • EngineConfig,它是一个LifecycleListener实现,用于配置Engine,但是只会处理START_EVENT和STOP_EVENT事件
  • Connector默认会有两种:HTTP/1.1、AJP,不同的Connector内部持有不同的CoyoteAdapter和ProtocolHandler,在Connector初始化的时候,也会对ProtocolHandler进行初始化,完成端口的监听
  • ProtocolHandler常用的实现有Http11NioProtocol、AjpNioProtocol,还有apr系列的Http11AprProtocol、AjpAprProtocol,apr系列只有在使用apr包的时候才会使用到
  • 在ProtocolHandler调用init初始化的时候,还会去执行AbstractEndpoint的init方法,完成请求端口绑定、初始化NIO等操作,在tomcat7中使用JIoEndpoint阻塞IO,而tomcat8中直接移除了JIoEndpoint,具体信息请查看org.apache.tomcat.util.net这个包

    1、容器初始化

    BootStrap初始化

    BootStrap是Tomcat的入口。Main方法和static语句块
    Static语句块的作用(看代码):在静态代码块中设置catalinaHome和catalinaBase两个路径
    mian方法的作用
  1. 实例化BootStrap
  2. 初始化BootStrap
  3. daemon.load(args);
  4. daemon.start();

    初始化BootStrap过程

    1. public void init() throws Exception {
    2. // 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再单独分析
    3. initClassLoaders();
    4. Thread.currentThread().setContextClassLoader(catalinaLoader);
    5. SecurityClassLoad.securityClassLoad(catalinaLoader);
    6. // 反射方法实例化Catalina,后面初始化Catalina也用了很多反射,不知道意图是什么
    7. Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    8. Object startupInstance = startupClass.getConstructor().newInstance();
    9. // 反射调用setParentClassLoader方法,设置其parentClassLoader为sharedLoade
    10. String methodName = "setParentClassLoader";
    11. Class<?> paramTypes[] = new Class[1];
    12. paramTypes[0] = Class.forName("java.lang.ClassLoader");
    13. Object paramValues[] = new Object[1];
    14. paramValues[0] = sharedLoader;
    15. Method method =
    16. startupInstance.getClass().getMethod(methodName, paramTypes);
    17. method.invoke(startupInstance, paramValues);
    18. // 引用Catalina实例
    19. catalinaDaemon = startupInstance;
    20. }
    21. private void initClassLoaders() {
    22. try {
    23. commonLoader = createClassLoader("common", null);
    24. if( commonLoader == null ) {
    25. // no config file, default to this loader - we might be in a 'single' env.
    26. commonLoader=this.getClass().getClassLoader();
    27. }
    28. catalinaLoader = createClassLoader("server", commonLoader);
    29. sharedLoader = createClassLoader("shared", commonLoader);
    30. } catch (Throwable t) {
    31. handleThrowable(t);
    32. log.error("Class loader creation threw exception", t);
    33. System.exit(1);
    34. }
    35. }

    Load & Start

    初始化Bootstrap之后,接下来就是加载配置,启动容器。而load、start实际上是由Bootstrap反射调用Catalina的load、start,这一部分代码将在下面的Catalina部分进行分析
  • 启动时,Catalina.setAwait(true),其目的是为了让tomcat在关闭端口阻塞监听关闭命令,参考Catalina.await()方法
  • deamon.load(args),实际上会去调用Catalina#load(args)方法,会去初始化一些资源,优先加载conf/server.xml,找不到再去加载server-embed.xml;此外,load方法还会初始化Server
  • daemon.start(),实例上是调用Catalina.start()

daemon即Bootstrap实例
daemon.setAwait(true);
daemon.load(args);
daemon.start();

Catalina初始化

由前面的分析,可知Bootstrap中的load逻辑实际上是交给Catalina去处理的,下面我们对Catalina的初始化过程进行分析
public void load(init)
load阶段主要是通过读取conf/server.xml或者server-embed.xml,实例化Server、Service、Connector、Engine、Host等组件,并调用Lifecycle#init()完成初始化动作,以及发出INITIALIZING、INITIALIZED事件

  • 首先初始化jmx的环境变量
  • 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类,如果我们要改变server.xml的某个属性值(比如优化tomcat线程池),直接查看对应实现类的setXXX方法即可
  • 解析conf/server.xml或者server-embed.xml,并且实例化对应的组件并且赋值操作,比如Server、Container、Connector等等
  • 为Server设置catalina信息,指定Catalina实例,设置catalina的home、base路径
  • 调用StarndServer#init()方法,完成各个组件的初始化,并且由parent组件初始化child组件,一层套一层,这个设计真心牛逼

    1. public void load() {
    2. initDirs();
    3. // 初始化jmx的环境变量
    4. initNaming();
    5. // Create and execute our Digester
    6. // 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类
    7. Digester digester = createStartDigester();
    8. InputSource inputSource = null;
    9. InputStream inputStream = null;
    10. File file = null;
    11. try {
    12. // 首先尝试加载conf/server.xml,省略部分代码......
    13. // 如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中),省略部分代码......
    14. // 如果还是加载不到xml,则直接return,省略部分代码......
    15. try {
    16. inputSource.setByteStream(inputStream);
    17. // 把Catalina作为一个顶级实例
    18. digester.push(this);
    19. // 解析过程会实例化各个组件,比如Server、Container、Connector等
    20. digester.parse(inputSource);
    21. } catch (SAXParseException spe) {
    22. // 处理异常......
    23. }
    24. } finally {
    25. // 关闭IO流......
    26. }
    27. // 给Server设置catalina信息
    28. getServer().setCatalina(this);
    29. getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    30. getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    31. // Stream redirection
    32. initStreams();
    33. // 调用Lifecycle的init阶段
    34. try {
    35. getServer().init();
    36. } catch (LifecycleException e) {
    37. // ......
    38. }
    39. // ......
    40. }

    Server初始化

    Catalina在load结束之前,会调用Server的init()完成各个组件的初始化,下面我们来分析下各个组件在init初始化过程中都做了哪些操作:
    StandardServer是由Catalina进行init初始化的,调用的是LifecycleBase父类的init方法,而StandardServer继承至LifecycleMBeanBase,重写了initInternal方法。
    StandardServer初始化的时序图如下所示,为了表述清楚,这里把LifecycleBase、LifecycleMBeanBase拆开了,实际上是同一个StandardServer实例对象,存在继承关系 。 Tomcat - 图27 由上图可以很清晰地看到,StandardServer的初始化过程,先由父类LifecycleBase改变当前的state值并发出事件通知,那么这个时候StandardServer的子容器StandardService内部的state是否会发生改变呢,是否会发出事件通知呢? 当然是不会的,因为这个state值不是LifecycleBase的静态成员变量,StandardServer只能改变自己的值,而StandardService只有在被StandardServer调用init初始化的时候才会改变,二者拥有独立的状态。考虑到有其它线程可能会改变StandardServer的state值,比如利用jmx执行init操作,因此要考虑并发问题,所以LifecycleBase#init()使用了synchronized锁,并且state是volatile修饰的。
    LifecycleBase改变state、发出事件通知之后,便会执行StandardServer自身的initInternal,我们来看看这个里面都干嘛了

    1. protected void initInternal() throws LifecycleException {
    2. super.initInternal();
    3. // 往jmx中注册全局的String cache,尽管这个cache是全局听,但是如果在同一个jvm中存在多个Server,
    4. // 那么则会注册多个不同名字的StringCache,这种情况在内嵌的tomcat中可能会出现
    5. onameStringCache = register(new StringCache(), "type=StringCache");
    6. // 注册MBeanFactory,用来管理Server
    7. MBeanFactory factory = new MBeanFactory();
    8. factory.setContainer(this);
    9. onameMBeanFactory = register(factory, "type=MBeanFactory");
    10. // 往jmx中注册全局的NamingResources
    11. globalNamingResources.init();
    12. // Populate the extension validator with JARs from common and shared class loaders
    13. if (getCatalina() != null) {
    14. // 忽略ClassLoader操作
    15. }
    16. // 初始化内部的Service
    17. for (int i = 0; i < services.length; i++) {
    18. services[i].init();
    19. }
    20. }
  • 先是调用super.initInternal(),把自己注册到jmx

  • 然后注册StringCache和MBeanFactory
  • 初始化NamingResources,就是server.xml中指定的GlobalNamingResources
  • 调用Service子容器的init方法,让Service组件完成初始化,注意:在同一个Server下面,可能存在多个Service组件

    Service初始化

    StandardService和StandardServer都是继承至LifecycleMBeanBase,因此公共的初始化逻辑都是一样的,这里不做过多介绍,我们直接看下initInternal。

    1. protected void initInternal() throws LifecycleException {
    2. // 往jmx中注册自己
    3. super.initInternal();
    4. // 初始化Engine
    5. if (engine != null) {
    6. engine.init();
    7. }
    8. // 存在Executor线程池,则进行初始化,默认是没有的
    9. for (Executor executor : findExecutors()) {
    10. if (executor instanceof JmxEnabled) {
    11. ((JmxEnabled) executor).setDomain(getDomain());
    12. }
    13. executor.init();
    14. }
    15. // 暂时不知道这个MapperListener的作用
    16. mapperListener.init();
    17. // 初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听
    18. synchronized (connectorsLock) {
    19. for (Connector connector : connectors) {
    20. try {
    21. connector.init();
    22. } catch (Exception e) {
    23. // 省略部分代码,logger and throw exception
    24. }
    25. }
    26. }
    27. }
  • 首先,往jmx中注册StandardService

  • 初始化Engine,而Engine初始化过程中会去初始化Realm(权限相关的组件)
  • 如果存在Executor线程池,还会进行init操作,这个Excecutor是tomcat的接口,继承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
  • 初始化Connector连接器,默认有http1.1、ajp连接器,而这个Connector初始化过程,又会对ProtocolHandler进行初始化,开启应用端口的监听,后面会详细分析

    Engine初始化

    StandardEngine在init阶段,需要获取Realm,这个Realm是干嘛用的?
    Realm(域)是用于对单个用户进行身份验证的底层安全领域的只读外观,并标识与这些用户相关联的安全角色。
    域可以在任何容器级别上附加,但是通常只附加到Context,或者更高级别的容器。

    1. /* StandardEngine初始化的代码如下:*/
    2. @Override
    3. protected void initInternal() throws LifecycleException {
    4. getRealm();
    5. super.initInternal();
    6. }
    7. public Realm getRealm() {
    8. Realm configured = super.getRealm();
    9. if (configured == null) {
    10. configured = new NullRealm();
    11. this.setRealm(configured);
    12. }
    13. return configured;
    14. }
    15. /* StandardEngine继承至ContainerBase,而ContainerBase重写了initInternal()方法,用于初始化start、stop线程池*/
    16. // 默认是1个线程
    17. private int startStopThreads = 1;
    18. protected ThreadPoolExecutor startStopExecutor;
    19. @Override
    20. protected void initInternal() throws LifecycleException {
    21. BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    22. startStopExecutor = new ThreadPoolExecutor(
    23. getStartStopThreadsInternal(),
    24. getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
    25. startStopQueue,
    26. new StartStopThreadFactory(getName() + "-startStop-"));
    27. // 允许core线程超时未获取任务时退出
    28. startStopExecutor.allowCoreThreadTimeOut(true);
    29. super.initInternal();
    30. }
    31. private int getStartStopThreadsInternal() {
    32. int result = getStartStopThreads();
    33. if (result > 0) {
    34. return result;
    35. }
    36. result = Runtime.getRuntime().availableProcessors() + result;
    37. if (result < 1) {
    38. result = 1;
    39. }
    40. return result;
    41. }

    这个startStopExecutor线程池有什么用呢?

  • 在start的时候,如果发现有子容器,则会把子容器的start操作放在线程池中进行处理

  • 在stop的时候,也会把stop操作放在线程池中处理

在前面的文章中我们介绍了Container组件,StandardEngine作为顶层容器,它的直接子容器是StardandHost,但是对StandardEngine的代码分析,我们并没有发现它会对子容器StardandHost进行初始化操作,StandardEngine不按照套路出牌,而是把初始化过程放在start阶段。个人认为Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。

Connector初始化

Connector也是继承至LifecycleMBeanBase,公共的初始化逻辑都是一样的。我们先来看下Connector的默认配置,大部分属性配置都可以在Connector类中找到,tomcat默认开启了HTTP/1.1、AJP/1.3,其实AJP的用处不大,可以去掉。

  1. <!--(在Server.xml中)-->
  2. <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
  3. <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Connector定义了很多属性,比如port、redirectPort、maxCookieCount、maxPostSize等等,比较有意思的是竟然找不到connectionTimeout的定义,全文搜索后发现使用了属性名映射,估计是为了兼容以前的版本(org.apache.catalina.connector.Connector)

  1. protected void initInternal() throws LifecycleException {
  2. // 注册jmx
  3. super.initInternal();
  4. // 初始化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的
  5. adapter = new CoyoteAdapter(this);
  6. // protocolHandler需要指定Adapter用于处理请求
  7. protocolHandler.setAdapter(adapter);
  8. // Make sure parseBodyMethodsSet has a default
  9. if (null == parseBodyMethodsSet) {
  10. setParseBodyMethods(getParseBodyMethods());
  11. }
  12. // apr支持,忽略部分代码......
  13. // 初始化ProtocolHandler,这个init不是Lifecycle定义的init,而是ProtocolHandler接口的init
  14. try {
  15. protocolHandler.init();
  16. } catch (Exception e) {
  17. throw new LifecycleException(
  18. sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
  19. }
  20. }

initInternal过程如下所示:

  • 实例化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的,后续的博客会进行深入分析
  • 为ProtocolHander指定CoyoteAdapter用于处理请求
  • 初始化ProtocolHander,

    ProtocolHandler初始化

    首先,我们来认识下ProtocolHandler,它是一个抽象的协议实现,它不同于JNI这样的Jk协议,它是单线程、基于流的协议。ProtocolHandler是一个Cycote连接器实现的主要接口,而Adapter适配器是由一个Coyote Servlet容器实现的主要接口,定义了处理请求的抽象接口。
    ProtocolHandler的子类如下所示,AbstractProtocol(org.apache.coyote)是基本的实现,而NIO默认使用的是Http11NioProtocol
    Tomcat - 图28
    调用ProtocolHandler的init进行初始化是调用的AbstractProtocol,首先完成jmx的注册,然后对NioEndpoint进行初始化.

    1. public abstract class AbstractProtocol<S> implements ProtocolHandler,
    2. MBeanRegistration {
    3. public void init() throws Exception {
    4. // 完成jmx注册
    5. if (oname == null) {
    6. oname = createObjectName();
    7. if (oname != null) {
    8. Registry.getRegistry(null, null).registerComponent(this, oname, null);
    9. }
    10. }
    11. if (this.domain != null) {
    12. rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
    13. Registry.getRegistry(null, null).registerComponent(
    14. getHandler().getGlobal(), rgOname, null);
    15. }
    16. String endpointName = getName();
    17. endpoint.setName(endpointName.substring(1, endpointName.length()-1));
    18. endpoint.setDomain(domain);
    19. // 初始化endpoint
    20. endpoint.init();
    21. }
    22. }

    NioEndpoint初始化过程,最重要的是完成端口和地址的绑定监听工作(org.apache.tomcat.util.net.NioEndpoint)

    1. public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {
    2. public void bind() throws Exception {
    3. // 实例化ServerSocketChannel,并且绑定端口和地址
    4. serverSock = ServerSocketChannel.open();
    5. socketProperties.setProperties(serverSock.socket());
    6. InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
    7. // 设置最大连接数,原来是在这里设置的
    8. serverSock.socket().bind(addr,getAcceptCount());
    9. serverSock.configureBlocking(true); //mimic APR behavior
    10. // 初始化acceptor、poller线程的数量
    11. // Initialize thread count defaults for acceptor, poller
    12. if (acceptorThreadCount == 0) {
    13. // FIXME: Doesn't seem to work that well with multiple accept threads
    14. acceptorThreadCount = 1;
    15. }
    16. if (pollerThreadCount <= 0) {
    17. pollerThreadCount = 1;
    18. }
    19. setStopLatch(new CountDownLatch(pollerThreadCount));
    20. // 如果有必要的话初始化ssl
    21. initialiseSsl();
    22. // 初始化selector
    23. selectorPool.open();
    24. }
    25. }

    总结:
    至此,整个初始化过程便告一段落。整个初始化过程,由parent组件控制child组件的初始化,一层层往下传递,直到最后全部初始化OK。下图描述了整体的传递流程
    Tomcat - 图29
    默认情况下,Server只有一个Service组件,Service组件先后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector连接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理。

    2. 容器启动

    前面分析了tomcat的初始化过程,是由Bootstrap反射调用Catalina的load方法完成tomcat的初始化,包括server.xml的解析、实例化各大组件、初始化组件等逻辑。那么tomcat又是如何启动webapp应用,又是如何加载应用程序的ServletContextListener,以及Servlet呢?
    Service、Engine、Host、Pipeline、Valve 组件的启动逻辑:
    Tomcat - 图30

    1. Bootstrap

    启动过程和初始化一样,由Bootstrap反射调用Catalina的start方法

    1. public void start() throws Exception {
    2. if( catalinaDaemon==null ) init();
    3. Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null);
    4. method.invoke(catalinaDaemon, (Object [])null);
    5. }

    2. Catalina

    主要分为以下三个步骤,其核心逻辑在于Server组件:

  • 调用Server的start方法,启动Server组件

  • 注册jvm关闭的勾子程序,用于安全地关闭Server组件,以及其它组件
  • 开启shutdown端口的监听并阻塞,用于监听关闭指令

    1. // org.apache.catalina.startup.Catalina
    2. public void start() {
    3. // 省略若干代码......
    4. // Start the new server
    5. try {
    6. getServer().start();
    7. } catch (LifecycleException e) {
    8. // 省略......
    9. return;
    10. }
    11. // 注册勾子,用于安全关闭tomcat
    12. if (useShutdownHook) {
    13. if (shutdownHook == null) {
    14. shutdownHook = new CatalinaShutdownHook();
    15. }
    16. Runtime.getRuntime().addShutdownHook(shutdownHook);
    17. }
    18. // Bootstrap中会设置await为true,其目的在于让tomcat在shutdown端口阻塞监听关闭命令
    19. if (await) {
    20. await();
    21. stop();
    22. }
    23. }

    3. Server

    1. protected void startInternal() throws LifecycleException {
    2. fireLifecycleEvent(CONFIGURE_START_EVENT, null);
    3. setState(LifecycleState.STARTING);
    4. globalNamingResources.start();
    5. // Start our defined Services
    6. synchronized (servicesLock) {
    7. for (int i = 0; i < services.length; i++) {
    8. services[i].start();
    9. }
    10. }
    11. }

    先是由LifecycleBase统一发出STARTING_PREP事件,StandardServer额外还会发出CONFIGURE_START_EVENT、STARTING事件,用于通知LifecycleListener在启动前做一些准备工作,比如NamingContextListener会处理CONFIGURE_START_EVENT事件,实例化tomcat相关的上下文,以及ContextResource资源.
    然后,启动内部的NamingResourcesImpl实例,这个类封装了各种各样的数据,比如ContextEnvironment、ContextResource、Container等等,它用于Resource资源的初始化,以及为webapp应用提供相关的数据资源,比如 JNDI 数据源(对应ContextResource).
    接着,启动Service组件,这一块的逻辑将在下面进行详细分析,最后由LifecycleBase发出STARTED事件,完成start.

    4. Service

    StandardService的start代码如下所示:

  • 启动Engine,Engine的child容器都会被启动,webapp的部署会在这个步骤完成;

  • 启动Executor,这是tomcat用Lifecycle封装的线程池,继承至java.util.concurrent.Executor以及tomcat的Lifecycle接口
  • 启动Connector组件,由Connector完成Endpoint的启动,这个时候意味着tomcat可以对外提供请求服务了。
    1. protected void startInternal() throws LifecycleException {
    2. setState(LifecycleState.STARTING);
    3. // 启动Engine
    4. if (engine != null) {
    5. synchronized (engine) {
    6. engine.start();
    7. }
    8. }
    9. // 启动Executor线程池
    10. synchronized (executors) {
    11. for (Executor executor: executors) {
    12. executor.start();
    13. }
    14. }
    15. // 启动MapperListener
    16. mapperListener.start();
    17. // 启动Connector
    18. synchronized (connectorsLock) {
    19. for (Connector connector: connectors) {
    20. try {
    21. // If it has already failed, don't try and start it
    22. if (connector.getState() != LifecycleState.FAILED) {
    23. connector.start();
    24. }
    25. } catch (Exception e) {
    26. // logger......
    27. }
    28. }
    29. }
    30. }

    5. Engine

    StandardEngine、StandardHost、StandardContext、StandardWrapper各个容器存在父子关系,一个父容器包含多个子容器,并且一个子容器对应一个父容器。Engine是顶层父容器,它不存在父容器,关于各个组件的详细介绍,请参考《tomcat框架设计》。各个组件的包含关系如下图所示,默认情况下,StandardEngine只有一个子容器StandardHost,一个StandardContext对应一个webapp应用,而一个StandardWrapper对应一个webapp里面的一个 Servlet
    Tomcat - 图31
    由类图可知,StandardEngine、StandardHost、StandardContext、StandardWrapper都是继承至ContainerBase,各个容器的启动,都是由父容器调用子容器的start方法,也就是说由StandardEngine启动StandardHost,再StandardHost启动StandardContext,以此类推。
    由于它们都是继续至ContainerBase,当调用 start 启动Container容器时,首先会执行 ContainerBase 的 start 方法,它会寻找子容器,并且在线程池中启动子容器,StandardEngine也不例外。
    5.1、ContainerBase
    ContainerBase的startInternal方法如下所示,主要分为以下3个步骤:
  1. 启动子容器
  2. 启动Pipeline,并且发出STARTING事件
  3. 如果backgroundProcessorDelay参数 >= 0,则开启ContainerBackgroundProcessor线程,用于调用子容器的backgroundProcess。

    1. // org.apache.catalina.core.ContainerBase:
    2. protected synchronized void startInternal() throws LifecycleException {
    3. // 省略若干代码......
    4. // 把子容器的启动步骤放在线程中处理,默认情况下线程池只有一个线程处理任务队列
    5. Container children[] = findChildren();
    6. List<Future<Void>> results = new ArrayList<>();
    7. for (int i = 0; i < children.length; i++) {
    8. results.add(startStopExecutor.submit(new StartChild(children[i])));
    9. }
    10. // 阻塞当前线程,直到子容器start完成
    11. boolean fail = false;
    12. for (Future<Void> result : results) {
    13. try {
    14. result.get();
    15. } catch (Exception e) {
    16. log.error(sm.getString("containerBase.threadedStartFailed"), e);
    17. fail = true;
    18. }
    19. }
    20. // 启用Pipeline
    21. if (pipeline instanceof Lifecycle)
    22. ((Lifecycle) pipeline).start();
    23. setState(LifecycleState.STARTING);
    24. // 开启ContainerBackgroundProcessor线程用于调用子容器的backgroundProcess方法,默认情况下backgroundProcessorDelay=-1,不会启用该线程
    25. threadStart();
    26. }

    5.2、启动子容器
    startStopExecutor是在init阶段创建的线程池,默认情况下 coreSize = maxSize = 1,也就是说默认只有一个线程处理子容器的 start,通过调用 Container.setStartStopThreads(int startStopThreads) 可以改变默认值 1。如果我们有4个webapp,希望能够尽快启动应用,我们只需要设置Host的startStopThreads值即可,如下所示。

    1. <!-- server.xml -->
    2. <Host name="localhost" appBase="webapps"
    3. unpackWARs="true" autoDeploy="true" startStopThreads="4">
    4. <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
    5. prefix="localhost_access_log" suffix=".txt"
    6. pattern="%h %l %u %t &quot;%r&quot; %s %b" />
    7. </Host>

    ContainerBase会把StartChild任务丢给线程池处理,得到Future,并且会遍历所有的Future进行阻塞result.get(),这个操作是将异步启动转同步,子容器启动完成才会继续运行。我们再来看看submit到线程池的StartChild任务,它实现了java.util.concurrent.Callable接口,在call里面完成子容器的start动作。

    1. private static class StartChild implements Callable<Void> {
    2. private Container child;
    3. public StartChild(Container child) {
    4. this.child = child;
    5. }
    6. @Override
    7. public Void call() throws LifecycleException {
    8. child.start();
    9. return null;
    10. }
    11. }

    5.2.1、启动Pipeline
    pipeline是什么?
    Pipeline是管道组件,用于封装了一组有序的Valve,便于Valve顺序地传递或者处理请求。
    Pipeline的接口定义如下,定义了 Valve 的常用操作,以及 Container 的 getter/setter 方法,它的默认实现类是 org.apache.catalina.core.StandardPipeline,同时它也是一个Lifecycle组件.

    1. // org.apache.catalina.Pipeline
    2. public interface Pipeline {
    3. public Valve getBasic();
    4. public void setBasic(Valve valve);
    5. public void addValve(Valve valve);
    6. public Valve[] getValves();
    7. public void removeValve(Valve valve);
    8. public Valve getFirst();
    9. public boolean isAsyncSupported();
    10. public Container getContainer();
    11. public void setContainer(Container container);
    12. public void findNonAsyncValves(Set<String> result);
    13. }

    Valve是什么?
    Valve 是阀门组件,穿插在 Container 容器中,可以把它理解成请求拦截器,在 tomcat 接收到网络请求与触发 Servlet 之间执行。
    Valve的接口如下所示,我们主要关注它的invoke方法,Request、Response分别是HttpServletRequest、HttpServletResponse的实现类

    1. public interface Valve {
    2. public Valve getNext();
    3. public void backgroundProcess();
    4. public void invoke(Request request, Response response) throws IOException, ServletException;
    5. public boolean isAsyncSupported();
    6. }

    我们再来看看 Pipeline 启动过程,默认使用 StandardPipeline 实现类,它也是一个Lifecycle。在容器启动的时候,StandardPipeline 会遍历 Valve 链表,如果 Valve 是 Lifecycle 的子类,则会调用其 start 方法启动 Valve 组件,代码如下

    1. public class StandardPipeline extends LifecycleBase
    2. implements Pipeline, Contained {
    3. // 省略若干代码......
    4. protected synchronized void startInternal() throws LifecycleException {
    5. Valve current = first;
    6. if (current == null) {
    7. current = basic;
    8. }
    9. while (current != null) {
    10. if (current instanceof Lifecycle)
    11. ((Lifecycle) current).start();
    12. current = current.getNext();
    13. }
    14. setState(LifecycleState.STARTING);
    15. }
    16. }

    tomcat为我们提供了一系列的Valve
    - AccessLogValve,记录请求日志,默认会开启
    - RemoteAddrValve,可以做访问控制,比如限制IP黑白名单
    - RemoteIpValve,主要用于处理 X-Forwarded-For 请求头,用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段

    6. StandardHost

    前面我们分析了 StandardEngine 的启动逻辑,它会启动其子容器 StandardHost,接下来我们看下 StandardHost 的 start 逻辑。其实, StandardHost 重写的 startInternal 方法主要是为了查找报告错误的 Valve 阀门。

    1. protected synchronized void startInternal() throws LifecycleException {
    2. // errorValve默认使用org.apache.catalina.valves.ErrorReportValve
    3. String errorValve = getErrorReportValveClass();
    4. if ((errorValve != null) && (!errorValve.equals(""))) {
    5. try {
    6. boolean found = false;
    7. // 如果所有的阀门中已经存在这个实例,则不进行处理,否则添加到 Pipeline 中
    8. Valve[] valves = getPipeline().getValves();
    9. for (Valve valve : valves) {
    10. if (errorValve.equals(valve.getClass().getName())) {
    11. found = true;
    12. break;
    13. }
    14. }
    15. // 如果未找到则添加到 Pipeline 中,注意是添加到 basic valve 的前面
    16. // 默认情况下,first valve 是 AccessLogValve,basic 是 StandardHostValve
    17. if(!found) {
    18. Valve valve =
    19. (Valve) Class.forName(errorValve).getConstructor().newInstance();
    20. getPipeline().addValve(valve);
    21. }
    22. } catch (Throwable t) {
    23. // 处理异常,省略......
    24. }
    25. }
    26. // 调用父类 ContainerBase,完成统一的启动动作
    27. super.startInternal();
    28. }

    StandardHost Pipeline 包含的 Valve 组件:

  4. basic:org.apache.catalina.core.StandardHostValve

  5. first:org.apache.catalina.valves.AccessLogValve

需要注意的是,在往 Pipeline 中添加 Valve 阀门时,是添加到 first 后面,basic 前面。
由上面的代码可知,在 start 的时候,StandardHost 并没有做太多的处理,那么 StandardHost 又是怎么知道它有哪些 child 容器需要启动呢?
tomcat 在这块的逻辑处理有点特殊,使用 HostConfig 加载子容器,而这个 HostConfig 是一个 LifecycleListener,它会处理 start、stop 事件通知,并且会在线程池中启动、停止 Context 容器,接下来看下 HostConfig 是如何工作的
以下是 HostConfig 处理事件通知的代码,我们着重关注下 start 方法,这个方法里面主要是做一些应用部署的准备工作,比如过滤无效的webapp、解压war包等,而主要的逻辑在于 deployDirectories 中,它会往线程池中提交一个 DeployDirectory 任务,并且调用 Future#get() 阻塞当前线程,直到 deploy 工作完成
org.apache.catalina.startup.HostConfig

  1. public void lifecycleEvent(LifecycleEvent event) {
  2. // (省略若干代码) 判断事件是否由 Host 发出,并且为 HostConfig 设置属性
  3. if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
  4. check();
  5. } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
  6. beforeStart();
  7. } else if (event.getType().equals(Lifecycle.START_EVENT)) {
  8. start();
  9. } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
  10. stop();
  11. }
  12. }
  13. public void start() {
  14. // (省略若干代码)
  15. if (host.getDeployOnStartup())
  16. deployApps();
  17. }
  18. protected void deployApps() {
  19. File appBase = host.getAppBaseFile();
  20. File configBase = host.getConfigBaseFile();
  21. // 过滤出 webapp 要部署应用的目录
  22. String[] filteredAppPaths = filterAppPaths(appBase.list());
  23. // 部署 xml 描述文件
  24. deployDescriptors(configBase, configBase.list());
  25. // 解压 war 包,但是这里还不会去启动应用
  26. deployWARs(appBase, filteredAppPaths);
  27. // 处理已经存在的目录,前面解压的 war 包不会再行处理
  28. deployDirectories(appBase, filteredAppPaths);
  29. }

而这个 DeployDirectory 任务很简单,只是调用 HostConfig#deployDirectory(cn, dir)

  1. private static class DeployDirectory implements Runnable {
  2. // (省略若干代码)
  3. @Override
  4. public void run() {
  5. config.deployDirectory(cn, dir);
  6. }
  7. }

我们再回到 HostConfig,看看 deployDirectory 的具体逻辑,分为以下几个步骤:

  1. 使用 digester,或者反射实例化 StandardContext
  2. 实例化 ContextConfig,并且为 Context 容器注册事件监听器,和 StandardHost 的套路一样,借助 XXXConfig 完成容器的启动、停止工作
  3. 将当前 Context 实例作为子容器添加到 Host 容器中,添加子容器的逻辑在 ContainerBase 中已经实现了,如果当前 Container 的状态是 STARTING_PREP 并且 startChildren 为 true,则还会启动子容器.

    1. protected void deployDirectory(ContextName cn, File dir) {
    2. Context context = null;
    3. File xml = new File(dir, Constants.ApplicationContextXml);
    4. File xmlCopy = new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
    5. // 实例化 StandardContext
    6. if (deployThisXML && xml.exists()) {
    7. synchronized (digesterLock) {
    8. // 省略若干异常处理的代码
    9. context = (Context) digester.parse(xml);
    10. }
    11. // (省略)为 Context 设置 configFile
    12. } else if (!deployThisXML && xml.exists()) {
    13. // 异常处理
    14. context = new FailedContext();
    15. } else {
    16. context = (Context) Class.forName(contextClass).getConstructor().newInstance();
    17. }
    18. // 实例化 ContextConfig,作为 LifecycleListener 添加到 Context 容器中,这和 StandardHost 的套路一样,都是使用 XXXConfig
    19. Class<?> clazz = Class.forName(host.getConfigClass());
    20. LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
    21. context.addLifecycleListener(listener);
    22. context.setName(cn.getName());
    23. context.setPath(cn.getPath());
    24. context.setWebappVersion(cn.getVersion());
    25. context.setDocBase(cn.getBaseName());
    26. // 实例化 Context 之后,为 Host 添加子容器
    27. host.addChild(context);
    28. }

    现在有两个疑问:

  4. 为什么要使用 HostConfig 组件启动 Context 容器呢,不可以直接在 Host 容器中直接启动吗?

HostConfig 不仅仅是启动、停止 Context 容器,还封装了很多应用部署的逻辑,此外,还会对 web.xml、context.xml 文件的改动进行监听,默认情况会重新启动 Context 容器。而这个 Host 只是负责管理 Context 的生命周期,基于单一职责的原则,tomcat 利用事件通知的方式,很好地解决了藕合问题,Context 容器也是如此,它会对应一个 ContextConfig。

  1. Context 容器又是如何启动的?

前面我们也提到了,HostConfig 将当前 Context 实例作为子容器添加到 Host 容器中(调用 ContainerBase.addChild 方法 ),而 Context 的启动就是在添加的时候调用的,Context 启动的时候会解析web.xml,以及启动 Servlet、Listener,Servlet3.0还支持注解配置。
首先我们思考几个问题:

  1. tomcat 如何支持 servlet3.0 的注解编程,比如对 javax.servlet.annotation.WebListener 注解的支持?

如果 tomcat 利用 ClassLoader 加载 webapp 下面所有的 class,从而分析 Class 对象的注解,这样子肯定会导致很多问题,比如 MetaSpace 出现内存溢出,而且加载了很多不想干的类,我们知道 jvm 卸载 class 的条件非常苛刻,这显然是不可取的。因此,tomcat 开发了字节码解析的工具类,位于 org.apache.tomcat.util.bcel,bcel 即 :Byte Code Engineering Library,专门用于解析 class 字节码,而不是像我们前面猜测的那样,把类加载到 jvm 中。

  1. 假如 webapp 目录有多个应用,使用的开源框架的 jar 版本不尽一致,tomcat 是怎样避免出现类冲突?

不同的 webapp 使用不同的 ClassLoader 实例加载 class,因此 webapp 内部加载的 class 是不同的,自然不会出现类冲突,当然这里要排除 ClassLoader 的 parent 能够加载的 class。

7. Context 容器

首先,我们来看下StandardContext重要的几个属性,包括了我们熟悉的 ServletContext、servlet容器相关的Listener(比如 SessionListener 和 ContextListener)、FilterConfig。

  1. protected ApplicationContext context:即ServletContext上下文
  2. private InstanceManager instanceManager:根据 class 实例化对象,比如 ListenerFilterServlet 实例对象
  3. private List<Object> applicationEventListenersListSessionListenerContextListner 等集合
  4. private HashMap<String, ApplicationFilterConfig> filterConfigsfiler 名字与 FilterConfig 的映射关系
  5. private Loader loader:用于加载class等资源
  6. private final ReadWriteLock loaderLock:用于对loader的读写操作
  7. protected Manager managerSession管理器
  8. private final ReadWriteLock managerLock:用于对manager的读写操作
  9. private HashMap<String, String> servletMappingsurlServlet名字的映射关系
  10. private HashMap<Integer, ErrorPage> statusPages:错误码与错误页的映射
  11. private JarScanner jarScanner:用于扫描jar包资源
  12. private CookieProcessor cookieProcessorcookies处理器,默认使用Rfc6265CookieProcessor

StandardContext 和其他 Container 一样,也是重写了 startInternal 方法。由于涉及到 webapp 的启动流程,需要很多准备工作,比如使用 WebResourceRoot 加载资源文件、利用 Loader 加载 class、使用 JarScanner 扫描 jar 包,等等。因此StandardContext 的启动逻辑比较复杂,这里描述下几个重要的步骤:

  1. 创建工作目录,比如$CATALINA_HOME\work\Catalina\localhost\examples;实例化 ContextServlet,应用程序拿到的是 ApplicationContext的外观模式
  2. 实例化 WebResourceRoot,默认实现类是 StandardRoot,用于读取 webapp 的文件资源
  3. 实例化 Loader 对象,Loader 是 tomcat 对于 ClassLoader 的封装,用于支持在运行期间热加载 class
  4. 发出 CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,主要目的是从 webapp 中读取 servlet 相关的 Listener、Servlet、Filter 等
  5. 实例化 Sesssion 管理器,默认使用 StandardManager
  6. 调用 listenerStart,实例化 servlet 相关的各种 Listener,并且调用

ServletContextListener

  1. 处理 Filter
  2. 加载 Servlet

下面,将分析下几个重要的步骤

7.1 触发 CONFIGURE_START_EVENT 事件

ContextConfig 它是一个 LifycycleListener,它在 Context 启动过程中是承担了一个非常重要的角色。StandardContext 会发出 CONFIGURE_START_EVENT 事件,而 ContextConfig 会处理该事件,主要目的是通过 web.xml 或者 Servlet3.0 的注解配置,读取 Servlet 相关的配置信息,比如 Filter、Servlet、Listener 等,其核心逻辑在 ContextConfig#webConfig() 方法中实现。下面,我们对 ContextConfig 进行详细分析
首先,是通过 WebXmlParser 对 web.xml 进行解析,如果存在 web.xml 文件,则会把文件中定义的 Servlet、Filter、Listener 注册到 WebXml 实例中

  1. protected void webConfig() {
  2. WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(), context.getXmlValidation(), context.getXmlBlockExternal());
  3. Set<WebXml> defaults = new HashSet<>();
  4. defaults.add(getDefaultWebXmlFragment(webXmlParser));
  5. // 创建 WebXml实例,并解析 web.xml 文件
  6. WebXml webXml = createWebXml();
  7. InputSource contextWebXml = getContextWebXmlSource();
  8. if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
  9. ok = false;
  10. }
  11. }

如果没有 web.xml 文件,tomcat 会先扫描 WEB-INF/classes 目录下面的 class 文件,然后扫描 WEB-INF/lib 目录下面的 jar 包,解析字节码读取 servlet 相关的注解配置类,这里不得不吐槽下 serlvet3.0 注解,对 servlet 注解的处理相当重量级。tomcat 不会预先把该 class 加载到 jvm 中,而是通过解析字节码文件,获取对应类的一些信息,比如注解、实现的接口等,核心代码如下所示:

  1. protected void processAnnotationsStream(InputStream is, WebXml fragment,
  2. boolean handlesTypesOnly, Map<String,JavaClassCacheEntry> javaClassCache)
  3. throws ClassFormatException, IOException {
  4. // is 即 class 字节码文件的 IO 流
  5. ClassParser parser = new ClassParser(is);
  6. // 使用 JavaClass 封装 class 相关的信息
  7. JavaClass clazz = parser.parse();
  8. checkHandlesTypes(clazz, javaClassCache);
  9. if (handlesTypesOnly) {
  10. return;
  11. }
  12. AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
  13. if (annotationsEntries != null) {
  14. String className = clazz.getClassName();
  15. for (AnnotationEntry ae : annotationsEntries) {
  16. String type = ae.getAnnotationType();
  17. if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
  18. processAnnotationWebServlet(className, ae, fragment);
  19. }else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
  20. processAnnotationWebFilter(className, ae, fragment);
  21. }else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
  22. fragment.addListener(className);
  23. } else {
  24. // Unknown annotation - ignore
  25. }
  26. }
  27. }
  28. }

Tomcat 使用自己的工具类 ClassParser 通过对字节码文件进行解析,获取其注解,并把 WebServlet、WebFilter、WebListener 注解的类添加到 WebXml 实例中,统一由它对 ServletContext 进行参数配置。tomcat 对字节码的处理是由org.apache.tomcat.util.bcel 包完成的,bcel 即 Byte Code Engineering Library,其实现比较繁锁,需要对字节码结构有一定的了解。
配置信息读取完毕之后,会把 WebXml 装载的配置赋值给 ServletContext,在这个时候,ContextConfig 会往 StardardContext 容器中添加子容器(即 Wrapper 容器),部分代码如下所示:

  1. private void configureContext(WebXml webxml) {
  2. // 设置 Filter 定义
  3. for (FilterDef filter : webxml.getFilters().values()) {
  4. if (filter.getAsyncSupported() == null) {
  5. filter.setAsyncSupported("false");
  6. }
  7. context.addFilterDef(filter);
  8. }
  9. // 设置 FilterMapping,即 Filter 的 URL 映射
  10. for (FilterMap filterMap : webxml.getFilterMappings()) {
  11. context.addFilterMap(filterMap);
  12. }
  13. // 往 Context 中添加子容器 Wrapper,即 Servlet
  14. for (ServletDef servlet : webxml.getServlets().values()) {
  15. Wrapper wrapper = context.createWrapper();
  16. // 省略若干代码。。。
  17. wrapper.setOverridable(servlet.isOverridable());
  18. context.addChild(wrapper);
  19. }
  20. // ......
  21. }

tomcat 还会加载 WEB-INF/classes/META-INF/resources/、WEB-INF/lib/xxx.jar/META-INF/resources/ 的静态资源,这一块的作用暂时不清楚,关键代码如下所示:

  1. protected void processResourceJARs(Set<WebXml> fragments) {
  2. for (WebXml fragment : fragments) {
  3. URL url = fragment.getURL();
  4. if ("jar".equals(url.getProtocol()) "" url.toString().endsWith(".jar")) {
  5. try (Jar jar = JarFactory.newInstance(url)) {
  6. jar.nextEntry();
  7. String entryName = jar.getEntryName();
  8. while (entryName != null) {
  9. if (entryName.startsWith("META-INF/resources/")) {
  10. context.getResources().createWebResourceSet(
  11. WebResourceRoot.ResourceSetType.RESOURCE_JAR,
  12. "/", url, "/META-INF/resources");
  13. break;
  14. }
  15. jar.nextEntry();
  16. entryName = jar.getEntryName();
  17. }
  18. }
  19. } else if ("file".equals(url.getProtocol())) {
  20. File file = new File(url.toURI());
  21. File resources = new File(file, "META-INF/resources/");
  22. if (resources.isDirectory()) {
  23. context.getResources().createWebResourceSet(
  24. WebResourceRoot.ResourceSetType.RESOURCE_JAR,
  25. "/", resources.getAbsolutePath(), null, "/");
  26. }
  27. }
  28. }
  29. }

7.2 启动 Wrapper 容器

ContextConfig 把 Wrapper 子容器添加到 StandardContext 容器中之后,便会挨个启动 Wrapper 子容器。但是实际上,由于 StandardContext 至 ContainerBase,在添加子容器的时候,便会调用 start 方法启动 Wrapper。

  1. for (Container child : findChildren()) {
  2. if (!child.getState().isAvailable()) {
  3. child.start();
  4. }
  5. }

7.3 调用 ServletContainerInitializer

在初始化 Servlet、Listener 之前,便会先调用 ServletContainerInitializer,进行额外的初始化处理。注意:ServletContainerInitializer 需要的是 Class 对象,而不是具体的实例对象,这个时候 servlet 相关的 Listener 并没有被实例化,因此不会产生矛盾

  1. // 指定 ServletContext 的相关参数
  2. mergeParameters();
  3. // 调用 ServletContainerInitializer#onStartup()
  4. for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
  5. initializers.entrySet()) {
  6. try {
  7. entry.getKey().onStartup(entry.getValue(),
  8. getServletContext());
  9. } catch (ServletException e) {
  10. log.error(sm.getString("standardContext.sciFail"), e);
  11. ok = false;
  12. break;
  13. }
  14. }

7.4 启动 Servlet 相关的 Listener

WebConfig 加载 Listener 时,只是保存了 className,实例化动作由 StandardContext 触发。前面在介绍 StandardContext 的时候提到了 InstanceManager,创建实例的逻辑由 InstanceManager 完成。
Listener 监听器分为 Event、Lifecycle 监听器,WebConfig 在加载 Listener 的时候是不会区分的,实例化之后才会分开存储。在完成 Listener 实例化之后,tomcat 容器便启动 OK 了。此时,tomcat 需要通知应用程序定义的 ServletContextListener,方便应用程序完成自己的初始化逻辑,它会遍历 ServletContextListener 实例,并调用其 contextInitialized 方法,比如 spring 的 ContextLoaderListener
有以下 Event 监听器,主要是针对事件通知:
- ServletContextAttributeListener
- ServletRequestAttributeListener
- ServletRequestListener
- HttpSessionIdListener
- HttpSessionAttributeListener
有以下两种 Lifecycle 监听器,主要是针对 ServletContext、HttpSession 的生命周期管理,比如创建、销毁等
- ServletContextListener
- HttpSessionListener

7.5 初始化 Filter

ContextConfig 在处理 CONFIGURE_START_EVENT 事件的时候,会使用 FilterDef 保存 Filter 信息。而 StandardContext 会把 FilterDef 转化成 ApplicationFilterConfig,在 ApplicationFilterConfig 构造方法中完成 Filter 的实例化,并且调用 Filter 接口的 init 方法,完成 Filter 的初始化。ApplicationFilterConfig 是 javax.servlet.FilterConfig
接口的实现类。

  1. public boolean filterStart() {
  2. boolean ok = true;
  3. synchronized (filterConfigs) {
  4. filterConfigs.clear();
  5. for (Entry<String,FilterDef> entry : filterDefs.entrySet()) {
  6. String name = entry.getKey();
  7. try {
  8. // 在构造方法中完成 Filter 的实例化,并且调用 Filter 接口的 init 方法,完成 Filter 的初始化
  9. ApplicationFilterConfig filterConfig =
  10. new ApplicationFilterConfig(this, entry.getValue());
  11. filterConfigs.put(name, filterConfig);
  12. } catch (Throwable t) {
  13. // 省略 logger 处理
  14. ok = false;
  15. }
  16. }
  17. }
  18. return ok;
  19. }

7.6 处理 Wrapper 容器

Servlet 对应 tomcat 的 Wrapper 容器,完成 Filter 初始化之后便会对 Wrapper 容器进行处理,如果 Servlet 的 loadOnStartup >= 0,便会在这一阶段完成 Servlet 的加载,并且值越小越先被加载,否则在接受到请求的时候才会加载 Servlet。
加载过程,主要是完成 Servlet 的实例化,并且调用 Servlet 接口的 init 方法,具体的逻辑将在下文进行详细分析
// StandardWrapper 实例化并且启动 Servlet,由于 Servlet 存在 loadOnStartup 属性
// 因此使用了 TreeMap,根据 loadOnStartup 值 对 Wrapper 容器进行排序,然后依次启动 Servlet

  1. if (ok) {
  2. if (!loadOnStartup(findChildren())){
  3. log.error(sm.getString("standardContext.servletFail"));
  4. ok = false;
  5. }
  6. }

loadOnStartup 方法使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同的 Wrapper 放在同一个 List 中,代码如下所示:

  1. public boolean loadOnStartup(Container children[]) {
  2. // 使用 TreeMap 对 Wrapper 进行排序,loadOnStartup 值越小越靠前,值相同的 Wrapper 放在同一个 List 中
  3. TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
  4. for (int i = 0; i < children.length; i++) {
  5. Wrapper wrapper = (Wrapper) children[i];
  6. int loadOnStartup = wrapper.getLoadOnStartup();
  7. if (loadOnStartup < 0)
  8. continue;
  9. Integer key = Integer.valueOf(loadOnStartup);
  10. ArrayList<Wrapper> list = map.get(key);
  11. if (list == null) {
  12. list = new ArrayList<>();
  13. map.put(key, list);
  14. }
  15. list.add(wrapper);
  16. }
  17. // 根据 loadOnStartup 值有序加载 Wrapper 容器
  18. for (ArrayList<Wrapper> list : map.values()) {
  19. for (Wrapper wrapper : list) {
  20. try {
  21. wrapper.load();
  22. } catch (ServletException e) {
  23. if(getComputedFailCtxIfServletStartFails()) {
  24. return false;
  25. }
  26. }
  27. }
  28. }
  29. return true;
  30. }

8. Wrapper 容器

Wrapper 容器是 tomcat 所有容器中最底层子容器,它没有子容器,并且父容器是 Context,对这一块不了解的童鞋请移步前面的博客《tomcat框架设计》。默认实现是 StandardWrapper,我们先来看看类定义,它继承至 ContainBase,实现了 servlet 的 ServletConfig 接口,以及 tomcat 的 Wrapper 接口,说明 StandardWrapper 不仅仅是一个 Wrapper 容器实现,还是 ServletConfig 实现,部分代码如下所示:

  1. public class StandardWrapper extends ContainerBase
  2. implements ServletConfig, Wrapper, NotificationEmitter {
  3. // Wrapper 的门面模式,调用 Servlet 的 init 方法传入的是该对象
  4. protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);
  5. protected volatile Servlet instance = null; // Servlet 实例对象
  6. protected int loadOnStartup = -1; // 默认值为 -1,不立即启动 Servlet
  7. protected String servletClass = null;
  8. public StandardWrapper() {
  9. super();
  10. swValve=new StandardWrapperValve();
  11. pipeline.setBasic(swValve);
  12. broadcaster = new NotificationBroadcasterSupport();
  13. }
  14. }

由前面对 Context 的分析可知,StandardContext 在启动的时候会发出CONFIGURE_START_EVENT 事件,ContextConfig 会处理该事件,通过解析 web.xml 或者读取注解信息获取 Wrapper 子容器,并且会添加到 Context 容器中。由于 StandardContext 继承至 ContainerBase,在调用 addChild 的时候默认会启动 child 容器(即 Wrapper),我们来看看 StandardWrapper 的启动逻辑

8.1 启动 Wrapper 容器

StandardWrapper 没有子容器,启动逻辑相对比较简单清晰,它重写了 startInternal 方法,主要是完成了 jmx 的事件通知,先后向 jmx 发出 starting、running 事件,代码如下所示:

  1. protected synchronized void startInternal() throws LifecycleException {
  2. // 发出 j2ee.state.starting 事件通知
  3. if (this.getObjectName() != null) {
  4. Notification notification =
  5. new Notification("j2ee.state.starting", this.getObjectName(), sequenceNumber++);
  6. broadcaster.sendNotification(notification);
  7. }
  8. // ConainerBase 的启动逻辑
  9. super.startInternal();
  10. setAvailable(0L);
  11. // 发出 j2ee.state.running 事件通知
  12. if (this.getObjectName() != null) {
  13. Notification notification =
  14. new Notification("j2ee.state.running", this.getObjectName(), sequenceNumber++);
  15. broadcaster.sendNotification(notification);
  16. }
  17. }

8.2 加载 Wrapper

由前面对 Context 容器的分析可知,Context 完成 Filter 初始化之后,如果 loadOnStartup >= 0 便会调用 load 方法加载 Wrapper 容器。StandardWrapper 使用 InstanceManager 实例化 Servlet,并且调用 Servlet 的 init 方法进行初始化,传入的 ServletConfig 是 StandardWrapperFacade 对象。

  1. public synchronized void load() throws ServletException {
  2. // 实例化 Servlet,并且调用 init 方法完成初始化
  3. instance = loadServlet();
  4. if (!instanceInitialized) {
  5. initServlet(instance);
  6. }
  7. if (isJspServlet) {
  8. // 处理 jsp Servlet
  9. }
  10. }

总结

tomcat 实现了 javax.servlet.ServletContext 接口,在 Context 启动的时候会实例化该对象。由 Context 容器通过 web.xml 或者 扫描 class 字节码读取 servlet3.0 的注解配置,从而加载 webapp 定义的 Listener、Servlet、Filter 等 servlet 组件,但是并不会立即实例化对象。全部加载完毕之后,依次对 Listener、Filter、Servlet 进行实例化、并且调用其初始化方法,比如 ServletContextListener#contextInitialized()、Flter#init() 等。

3. 接口功能解析

ServletContainerInitializer

从Servlet 3.0开始,tomcat会在启动执行此接口的onStartup方法。
默认情况下,此接口的实现类不会被加载到tomcat中被执行。
若想要被加载到tomcat中被执行,则需要在项目META-INFO/services/下新建文件javax.servlet.ServletContainerInitializer,并在此文件中添加一行内容为实现类的全限定名。

@HandlesTypes

修饰ServletContainerInitializer接口的实现类,参数为数组,传入interface.class,可以将该接口的所有实现类注入到onStartup(Set> c, ServletContext ctx)的c中,然后便可以在这个方法中或这些注入的实现类中进行servlet的初始化等操作。
spring-web中自动注入servlet和启动IOC容器便是根据这个原理实现的。类: SpringServletContainer..