美文网首页
最近迷上了源码,Tomcat 源码,看我这篇就够了

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

作者: Kyriez7 | 来源:发表于2022-10-19 10:17 被阅读0次

    1 Apache Tomcat 源码环境构建

    1.1 Apache Tomcat 源码下载

    https://tomcat.apache.org/download-80.cgi

    环境:jdk11

    下载对应的 zip 包

    下载到本地任意磁盘下

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

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

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

    2)常见错误二

    原因:jdk 版本的事,选 jdk11

    file - project structure

    3)常见错误三

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

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

    访问

    http://localhost:8080/
    
    

    可以看到,tomcat 成功启动。

    2 Tomcat 架构与源码剖析

    2.1 Apache Tomcat 总体架构

    从 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.comwww.bat.com 分割开来互不干扰;

    Context:每个虚拟主机又可以支持多个 web 应用部署在它下边,这就是我们所熟知的上下文对象 Context,上下文是使用由 Servlet 规范中指定的 Web 应用程序格式表示,不论是压缩过的 war 包形式的文件还是未压缩的目录形式;

    Wrapper:在上下文中又可以部署多个 servlet,并且每个 servlet 都会被一个包装组件(Wrapper)所包含(一个 wrapper 对应一个 servlet)

    去掉注释的 server.xml

    虚拟主机

    把 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 组件,画图理解下

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

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

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

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

    源码如下

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

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

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

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

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

    源码:

    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

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

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

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

    4)接受请求的流程

    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

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

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

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

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

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

    相关文章

      网友评论

          本文标题:最近迷上了源码,Tomcat 源码,看我这篇就够了

          本文链接:https://www.haomeiwen.com/subject/tsdezrtx.html