从ApacheTomcat架构谈面试到源码编译环境v10.0.12
阅读原文时间:2023年07月09日阅读:1

概述

开启博客分享已近三个月,感谢所有花时间精力和小编一路学习和成长的伙伴们,有你们的支持,我们继续再接再厉

**本人博客网站 **IT小神 www.itxiaoshen.com

Tomcat官网 http://tomcat.apache.org/

Apache Tomcat软件是Jakarta Servlet、Jakarta Server Pages、Jakarta Expression Language、Jakarta WebSocket、Jakarta annotation和Jakarta Authentication规范的开源实现。简单来说也是一个基于Servlet、JSP规范的轻量级Web应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,也是大部分Java技术栈开发和调试Web程序的首选,目前官方最新版本为10.0.12 Released,而10.1.0-M6 Released还是alpha阶段。

Tomcat最初是由Sun的软件架构师詹姆斯·邓肯·戴维森开发的,后来他帮助将其变为开源项目,并由Sun贡献给Apache软件基金会,成为Apache的定级项目

Tomcat、Jetty、Undertow这几个都是非常有名实现Servlet规范的应用服务器,Tomcat本身也是业界上非常优秀的中间件,简单可可以将Tomcat看成是一个Http服务器+Servlet容器,Servlet 容器是管理和运行 Servlet 的;相信大家对这只三脚猫Logo都是非常熟悉了,不管是在学校和还是工作都经常使用它,特别是Java程序员最初学习编程都经历过在Idea或Eclipse中配置Tomcat启动Web项目,当然还可以通过基于tomcat maven插件或者SpringBoot内嵌web容器(我们在前面《Spring Boot浅聊入门v2.5.3文章》说到Spring Boot内嵌Tomcat容器其实就是New 创建Tomcat的实例)方式调试运行。

大家都非常了解Java Web的三大组件Servlet,Filter,Listener,但我们本篇决定不展开聊Servlet规范部分,当前面试官也会让你谈谈对于Servlet的理解,我们这里只是简单提下后续有时间再针对Servlet相关知道再来深入研究分析,这里主要为了学习tomcat架构实现而简单说下Servlet容器工作流程:

  • Web客户向Servlet容器(比如tomcat)发出Http请求;
  • Servlet容器解析Web客户的Http请求;
  • Servlet容器创建一个HttpRequest对象,在这个对象中封装Http请求信息;
  • Servlet容器创建一个HttpResponse对象;
  • Servlet容器调用HttpServlet的service方法,把HttpRequest和HttpResponse对象作为service方法的参数传给HttpServlet对象;
    • HttpServlet事实上是servlet的一种子类实例也是最一般的实例。当编写一个servlet时,必须直接或间接实现servlet接口,最可能实现的方法就是扩展javax.servlet.genericservlet或javax.servlet.http.httpservlet,其中genericservlet类提供了servlet接口的基本实现,httpservlet类扩展了genericservlet并且提供了servlet接口中具体于http的实现。
  • HttpServlet调用HttpRequest的有关方法,获取HTTP请求信息;
  • HttpServlet调用HttpResponse的有关方法,生成响应数据;
    • 一般通过HttpServletRequest和HttpServletResponse获取HTTP请求信息和返回响应。事实上servlet理论上可以处理多种形式的请求响应形式 http只是其中之一 所以HttpServletRequest HttpServletResponse分别是ServletRequest和ServletResponse的子类。一般,HttpServlet对应HttpServletRequest和HttpServletResponse。
  • Servlet容器把HttpServlet的响应结果传给Web客户。

Tomcat10.0官方用户指南 https://tomcat.apache.org/tomcat-10.0-doc/index.html

#安装Tomcat需要先保证有安装JDK环境,建议JDK版本为8以上,JDK17现在都已可用了
#解压文件
unzip apache-tomcat-10.0.12.zip
#进入目录
cd apache-tomcat-10.0.12/bin
#启动tomcat
./startup.sh
#查看默认配置8080端口服务是否启动
netstat -lntp | grep 8080

架构源码剖析

Tomcat源码中大量使用模板方法和适配器的设计模式,封装很多的组件,组件之间呈现出明显的层级关系,一层套着一层,这就是经典的套娃式架构设计;个人推荐从tomca配置文件开始理解其架构这个也是基于套娃式架构设计的优点得来。Tomcat 模块分层结构及相关模块的功能说明如下图:

Tomcat 核心组件架构图如下所示:

上面有些功能我们可以通过上面的1.3章节提供的官方用户指南查阅到相关信息,这里简单罗列几条说明

  • Listener 组件

    • 可以在 Tomcat 生命周期中完成某些容器相关的监听器。
  • JNDI

    • JNDI是 Java 命名与目录接口,是属于 J2EE 规范的,Tomcat 对其进行了实现。JNDI 在 J2EE 中的角色就是“交换机”,即 J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制(你可以简单理解为给资源取个名字,再根据名字来找资源)。
  • Cluster 组件

    • 提供了集群功能,可以将对应容器需要共享的数据同步到集群中的其他 Tomcat 实例中。
  • Realm 组件

    • 提供了容器级别的用户-密码-权限的数据对象,配合资源认证模块使用。
  • Loader 组件

    • Web 应用加载器,用于加载 Web 应用的资源,它要保证不同 Web 应用之间的资源隔离。
  • Manager 组件

    • Servlet 映射器,它属于 Context 内部的路由映射器,只负责该 Context 容器的路由导航。

Catalina 是 Tomcat 中的一个重要组件,它负责的是解析 Tomcat 的配置文件(server.xml),以此来创建服务器 Server 组件并进行管理。下面是Tomcat的conf目录下大名鼎鼎server.xml核心配置的内容结构,Server-Service-(Connector+Engine),在GlobalNamingResouce 域中可以定义全局资源 tomcat-user.xml,在web.xml文件中也有常见如session-config配置session超时时间默认是30分钟。

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <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">
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    <Connector executor="tomcatThreadPool"
               port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    </Connector>
    <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>
    </Engine>
  </Service>
</Server>
  • Server :代表了一个 Tomcat 实例,包含了 Servlet 容器以及其他组件,负责组装并启动 Servlet 引擎、Tomcat 连接器。每个Tomcat都只有一个Server,表示整个服务环境。一个Server中可以有多个Service。Server就管理多个Service。tomcat源码中服务继承自LifeCycle,tomcat服务组件顶层接口,有唯一实现StandardServer。
  • Service:服务是 Server 内部的组件,一个Server中可以有多个Service,它将若干个 Connector 组件绑定到一个 Container,每一个Service中可以有多个Connector和一个Container(Engine)。
    • Connector主要用来接收请求,解析请求内容,封装request和response,然后将准备好的数据交给Container处理。
    • Container就是我们常说的容器,里面可以有多个Host,一个host表示一个虚拟主机,就是一个对应一个WebApps. 最后Container处理完请求之后会将响应内容返回给Connecter,再由Connecter返回给客户端。
    • Executor 执行器:tomcat线程池的配置,提供给Connector。
  • Connecter:负责处理用户的 servlet 请求,并返回对象给 web 用户的模块

我们知道tomcat是处理http的请求,意味着tomcat使用的是 HTTP 协议进行数据传输,而HTTP 协议是一种应用层协议,其本质就是一种浏览器与服务器之间约定好的通信格式,因此tomcat作为一个Http服务器需要包含接受连接、解析请求数据、处理请求和发送响应这几大块功能。在这里我们再分析tomcat最核心的两个功能:

  • 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
  • 加载和管理 Servlet,由 Servlet 具体负责处理 Request 请求。

Tomcat 最底层使用的是Socket进行连接的,所以这里也涉及网络IO模型,通过socket监听本机端口,接收和处理监听端口的网络请求,Request和Response是按照Http协议来封装的,所以Connector同时需要实现TCP/IP协议和Http协议;基于核心功能Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别实现,连接器复杂对外接收和解析请求,封装request和response,最后把数据交给Containner,容器负责内部处理。

  • 网络通信。
  • 应用层协议解析。
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。

连接器主要需要完成以下三个核心功能:

  • socket 通信,也就是网络通信。
  • 应用层协议解析,解析处理应用层协议,封装成一个 Request 对象。
  • 将 Request 转换为 ServletRequest,将 Response 转换为 ServletResponse。

Tomcat 通过 EndPoint、Processor 和 Adapter这 3 个组件来实现连接器,这三个组件之间通过抽象接口进行交互。从一个请求的正向流程来看, Endpoint 负责提供请求字节流给 Processor,Processor 负责提供 Tomcat 定义的 Request 对象给 Adapter,Adapter 负责提供标准的 ServletRequest 对象给 Servlet 容器:

  • Endpoint 和 Processor 可以自由组合,放在一起抽象成了 ProtocolHandler 组件,连接器用 ProtocolHandler 来处理网络连接和应用层协议。Connector中具体用事件处理器来处理请求【ProtocoHandler】;不同的ProtocoHandler代表不同的连接类型【所以一个Service中可以有多个Connector】 例如:Http11Protocol使用普通的Socket来连接的,Http11NioProtocol使用NioSocket连接。

  • EndPoint:对接 I/O 模型,提供字节流给Processor,监听通信端口,是对传输层的抽象,处理底层Socket的网络连接,用来实现 TCP/IP 协议的。 是一个接口,对应的抽象类为AbstractEndPoint,有很多实现类,比如NioEndPoint,JIoEndPoint等。在其中有两个组件,一个 是Acceptor,另外一个是SocketProcessor。 Acceptor用于监听Socket连接请求,SocketProcessor用于处理接收到的Socket请求。EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
  • Processor:对接应用层协议,提供Tomcat Request对象给Adapter,Processor是用于实现HTTP协议的,也就是说Processor是针对应用层协议的抽象。 Processor接受来自EndPoint的Socket,然后解析成Tomcat Request和Tomcat Response对象,最后通过Adapter 提交给容器。 对应的抽象类为AbstractProcessor,有很多实现类,比如AjpProcessor、Http11Processor等。
  • Adapter:遵循 Servlet 规范,提供ServletRequest给容器,ProtocolHandler接口负责解析请求并生成 Tomcat Request 类。 需要把这个 Request 对象转换成 ServletRequest。 Tomcat 引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方 法,将请求适配到Servlet容器 Container 架构。

容器主要用于封装和管理Servlet以及具体处理Request请求。Tomcat 中设计四大Servlet容器组件,分别是Engine、Host、Context、Wrapper--->Servlet,这 4 种容器不是平行关系,而是父子关系。在tomcat源码org.apache.catalina.core.StandardEngine四个都有其标准实现,每一个容器都有一个 Pipeline 对象。

  • Engine:整个 Catalina 的 Servlet 容器引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但是一个引擎可包含多个 Host。
  • Host:代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可包含多个 Context。
  • Context:表示一个 Web 应用程序,相当于我们在webapp下的应用,一个 Web 应用通常有多个 Servlet,一个Web应用可包含多个 Wrapper子容器。
  • Wrapper:servlet包装类,包装Servlet负责管理整个 Servlet 的生命周期,包括装载、初始化、资源回收等,每一个Wraper都包装着Servlet。

Engine、Host、Context、Wrapper容器都继承ContainerBase,而ContainerBase有StandardPipeLine对象,每个容器构造方法setBasic设置默认Valve(StandardEngineValve),PipeLine和Valve组件的init、start。

Connector使用容器Container处理请求时,connector.getService().getContainer().getPipeLine().getFirst().invoke(request,response);

整个 Tomcat 其实就是一个 Catalina 实例,Tomcat 启动的时候会初始化这个实例,Catalina 实例通过加载server.xml 完成其他实例的创建,创建并管理一个 Server,Server 创建并管理多个服务, 每个服务又可以有多个Connector 和一个 Container。

我们知道启动tomcat是执行startup.sh脚本文件,这个脚本文件里又执行catalina.sh最终是执行org.apache.catalina.startup.Bootstrap类。

因此得知Tomcat从Bootstrap类main方法开始,以链的方式逐级调用各模块的init方法进行初始化, 待各个模块都初始化后, 又会逐级调用各个模块的start()方法启动各个模块;commonLoader、catalinaLoader、sharedLoader这三个类加载器打破双亲委派。下面是tomcat启动流程时序图:

接下来我们演示内嵌tomcat运行示例,创建maven项目,pom文件

<?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">
    <parent>
        <artifactId>spring-extend</artifactId>
        <groupId>com.itxs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>tomcat-test</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.28</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>8.5.28</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.9</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

创建一个可以测试Servlet,MyFirstServlet.java

package com.itxs;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyFirstServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws  IOException {
        doPost(req, resp);
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        resp.getWriter().print("hello tomcat servlet,access success!!!");
    }
}

TomcatApplication.java文件

package com.itxs;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import java.io.File;

public class TomcatApplication {

    public static int TOMCAT_PORT = 8080;
    public static String TOMCAT_HOSTNAME = "127.0.0.1";
    public static String WEBAPP_PATH = "src/main";

    public static void main(String[] args) throws LifecycleException {
        TomcatApplication.run();
    }

    public static void run() throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(TomcatApplication.TOMCAT_PORT);
        tomcat.setHostname(TomcatApplication.TOMCAT_HOSTNAME);
        tomcat.setBaseDir("."); // tomcat 信息保存在项目下
        StandardContext myContext = null;
        try {
            myContext = (StandardContext) tomcat.addWebapp("/itxs", System.getProperty("user.dir") + File.separator + TomcatApplication.WEBAPP_PATH);
            myContext.setReloadable(false);
            // 上下文监听器
            myContext.addLifecycleListener(new AprLifecycleListener());
            // 注册servlet
            tomcat.addServlet("/itxs", "myFirstServlet",new MyFirstServlet());
            // servlet mapping
            myContext.addServletMappingDecoded("/first.do", "myFirstServlet");
            tomcat.start();
            tomcat.getServer().await();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }
}

运行main方法

并访问http://127.0.0.1:8080/itxs/first.do,收到结果页面

源码编译

官方下载tomcat源码,目前最新稳定版本为10.0.12

#由于tomcat源码依赖jakartaee-migration模块,而jakartaee-migration未发布到maven repository,我们需要git clone到本地,再mvn install来部署解决tomcat源码的编译问题,https://github.com/apache/tomcat-jakartaee-migration
#解压文件,进入到tomcat-jakartaee-migration-main目录,地址栏上输入cmd进入命令行窗口并且自动进入当前目录

#执行maven安装到本地仓库
mvn clean install

#加压apache-tomcat-10.0.12-src.zip的文件,Tomcat源码并非maven项目结构,但可以通过pom指定java代码目录(无需按照src/main结构来),项目目录下创建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>com.github.sources</groupId>
    <artifactId>source-tomcat</artifactId>
    <version>10.0-SNAPSHOT</version>
    <name>source-tomcat</name>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant</artifactId>
            <version>1.10.1</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</groupId>
            <artifactId>org.eclipse.jdt.core</artifactId>
            <version>3.25.0</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jdt.core.compiler</groupId>
            <artifactId>ecj</artifactId>
            <version>4.6.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>jakartaee-migration</artifactId>
            <version>0.2.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>biz.aQute.bnd</groupId>
            <artifactId>biz.aQute.bndlib</artifactId>
            <version>5.2.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>Tomcat10.0</finalName>
        <sourceDirectory>java</sourceDirectory>
        <testSourceDirectory>test</testSourceDirectory>
        <resources>
            <resource>
                <directory>java</directory>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>test</directory>
            </testResource>
        </testResources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>


#由于源码apache-tomcat-10.0.12的pom文件里的jakartaee-migration的版本为0.2.1-SNAPSHOT,我们改为上面tomcat-jakartaee-migration-main的项目的版本1.0.1-SNAPSHOT

#我们在JDTCompiler里注释下面三行源码, 不能会报没有 CompilerOptions.VERSION_16
//                settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_16);
//                settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_16);
//                settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_16);

前面章节我们已提到tomcat main函数的入口在org.apache.catalina.startup.Bootstrap里,知道main函数

会遇到测试类的报错如下,可以尝试运行忽略测试类,或者直接删除maven compile时产生的test文件夹,我们这里直接删除test文件夹

由于目前tomcat启动找不到配置文件,因此我们在源码根目录下创建source文件夹,并将conf和webapps这两个目录转移到source文件夹中

然后在运行设置里添加如下vm参数

-Dcatalina.home=F:\develop\apache-tomcat-10.0.12-src\source
-Dcatalina.base=F:\develop\apache-tomcat-10.0.12-src\source
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=F:\develop\apache-tomcat-10.0.12-src\source\conf\logging.properties

配置好指定的catalina.home和catalina.base以及日志相关jvm参数后,启动Tomcat,到这里,我们是可以启动tomcat程序,但是访问http://localhost:8080/ 会报500错误,启动Tomcat BootStrap时未加载JSP编译器JasperInitializer

#在org.apache.catalina.startup.ContextConfig#configureStart中
webConfig(); // 这句下面添加如下初始化语句
context.addServletContainerInitializer(new JasperInitializer(), null);

重新启动Tomcat,查看http://localhost:8080,出现tomcat的页面

性能调优

关于性能调优方面可以考虑一下几个:

  • 内存:jvm参数,这个是实际生产中用的较多的。
  • 并发优化:connector开启线程池及线程池优化;
  • 缓存优化:connector 压缩和客户端连接超时;
  • IO优化:protocol 选择BIO NIO NIO2(AIO);
  • 组件优化:APR sendfile epoll openssl Tomcat native;

我们本篇只是简单理解tomcat架构和搭建tomcat源码调试环境,后续有时间我们再对tomcat源码和设计模式做进一步剖析和分享