最近迷上了源码,Tomcat源码,看我这篇就够了

5,132 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情

1 Apache Tomcat源码环境构建

1.1 Apache Tomcat源码下载

tomcat.apache.org/download-80…

环境:jdk11

下载对应的zip包

file 下载到本地任意磁盘下

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⽂件,

⽂件内容如下

<?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 中声明, 但该模块未将它导出到未命名模块)

file

原因:sun的包对ide编译环境不可见造成的,鼠标放在代码中报红的地方,根据idea的提示操作即可。

file

注意!不要用maven去编译它,这个参数你加入的是idea的环境,所以,用idea编译和启动

file

2)常见错误二

file 原因:jdk版本的事,选jdk11

file - project structure

file

3)常见错误三

运⾏ Bootstrap 类的 main 函数,此时就启动了tomcat,启动时候会去加载所配置的 conf ⽬录下 的server.xml等配置⽂件,所以访问8080端⼝即可,但此时我们会遇到如下的⼀个错误

file 原因是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文件

file 访问

http://localhost:8080/

file 可以看到,tomcat成功启动。

2 Tomcat架构与源码剖析

2.1 Apache Tomcat总体架构

file 从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

file file

虚拟主机

把webapps复制一份,叫webapps2,然后修改里面ROOT的index.jsp , 随便改一下

修改web.xml添加虚拟主机,参考下面:(记得把 localhost2 加入到 hosts文件中)

重启访问 http://localhost2/ 试试,和localhost对比一下

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  <GlobalNamingResources>
    <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>
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
      <Host name="localhost2"  appBase="webapps2"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %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组件,画图理解下

这里大家先有个印象,下面源码会看到互相之间的调用

file 下面的源码我们会详细看到处理的转交过程:

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的启动入口了,执行过这个脚本之后发生了什么呢?

file 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生命周期接口方法:

file 源码如下

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(参数)

file

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、实例 赋值

file

    //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:

file

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过程很相似

file

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)案例

file

源码:

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

<?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

确认控制台打印信息,打断点可以正常进来:

file

基于请求的环境准备工作完成!

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追踪MapElement的继承实现
  • 从MappedHost类打开入口,看拥有的属性和关系

file

4)接受请求的流程

file

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

file

将断点打在 org.apache.catalina.startup.Catalina#start, 下面的 stop() 一行

在命令行键入:telnet ip port 后,然后键入大写的SHUTDOWN。其中port默认为8005

file 然后输入大写【SHUTDOWN】,会被断点捕获到。

结论:通过使用telnet关闭8005端口也正好印证了上面的 结论。

shutdown.bat和上面的原理也是一样的

往期干货:

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!