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.0http://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 或 shBootstrap{  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和下面的原理也是一样的

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

转载请注明出处!