关于java:最近迷上了源码Tomcat源码看我这篇就够了

0次阅读

共计 12551 个字符,预计需要花费 32 分钟才能阅读完成。

1 Apache Tomcat 源码环境构建

1.1 Apache Tomcat 源码下载

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

环境: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.com、www.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/te…

确认控制台打印信息,打断点能够失常进来:

基于申请的环境筹备工作实现!

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 类关上入口,看领有的属性和关系
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 和下面的原理也是一样的

如果本文对您有帮忙,欢送 关注 点赞`,您的反对是我保持创作的能源。

转载请注明出处!

正文完
 0