天天看點

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

1 Apache Tomcat源碼環境建構

1.1 Apache Tomcat源碼下載下傳

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

環境:jdk11

下載下傳對應的zip包

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

下載下傳到本地任意磁盤下

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 中聲明, 但該子產品未将它導出到未命名子產品)

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

原因:sun的包對ide編譯環境不可見造成的,滑鼠放在代碼中報紅的地方,根據idea的提示操作即可。

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

注意!不要用maven去編譯它,這個參數你加入的是idea的環境,是以,用idea編譯和啟動

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析
2)常見錯誤二
最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

原因:jdk版本的事,選jdk11

file - project structure

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析
3)常見錯誤三

運⾏ Bootstrap 類的 main 函數,此時就啟動了tomcat,啟動時候會去加載所配置的 conf ⽬錄下 的server.xml等配置⽂件,是以通路8080端⼝即可,但此時我們會遇到如下的⼀個錯誤

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

原因是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檔案

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

通路

http://localhost:8080/
           
最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

可以看到,tomcat成功啟動。

2 Tomcat架構與源碼剖析

2.1 Apache Tomcat總體架構

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 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

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析
最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

虛拟主機

把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元件,畫圖了解下

這裡大家先有個印象,下面源碼會看到互相之間的調用

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

下面的源碼我們會詳細看到處理的轉交過程:

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的啟動入口了,執行過這個腳本之後發生了什麼呢?

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 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生命周期接口方法:

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

源碼如下

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(參數)

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

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、執行個體 指派

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析
//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:

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

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過程很相似
最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

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

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

源碼:

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

确認控制台列印資訊,打斷點可以正常進來:

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

基于請求的環境準備工作完成!

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類打開入口,看擁有的屬性和關系
最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

4)接受請求的流程

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

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

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

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

在指令行鍵入:telnet ip port 後,然後鍵入大寫的SHUTDOWN。其中port預設為8005

最近迷上了源碼,Tomcat源碼,看我這篇就夠了1 Apache Tomcat源碼環境建構2 Tomcat架構與源碼剖析

然後輸入大寫【SHUTDOWN】,會被斷點捕獲到。

結論:通過使用telnet關閉8005端口也正好印證了上面的 結論。

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