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.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 "%r" %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 "%r" %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和上面的原理也是一樣的
本文由
傳智教育博學谷教研團隊發布,如果本文對您有幫助,歡迎
關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/517500.html
標籤:Java
