您的位置:首页 > 运维架构 > Tomcat

从ApacheTomcat架构谈面试到源码编译环境v10.0.12

2021-10-29 00:23 876 查看

概述

开启博客分享已近三个月,感谢所有花时间精力和小编一路学习和成长的伙伴们,有你们的支持,我们继续再接再厉 **本人博客网站 **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的实例)方式调试运行。

Servlet简介

大家都非常了解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。

    PipeLine和Valve(管道和阀门)

    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源码和设计模式做进一步剖析和分享

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: