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

1 Apache Tomcat源码环境构建

1.1 Apache Tomcat源码下载

https://tomcat.apache.org/dow...

环境: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⽂件,

⽂件内容如下



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

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对比一下



  
  
  
  
  
  
    
  
  
    
    
      
        
      
      
        
      
      
        
      
    
  

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




    
        demoServlet
        com.itheima.test.DemoServlet
    

    
        demoServlet
        /test.do
    

debug重启tomcat,访问 http://localhost:8080/demo/te...

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

file

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

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类打开入口,看拥有的属性和关系

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和上面的原理也是一样的

如果本文对您有帮助,欢迎关注点赞`,您的支持是我坚持创作的动力。

转载请注明出处!

你可能感兴趣的:(javatomcat后端)