从ApacheTomcat架构谈面试到源码编译环境v10.0.12
概述
开启博客分享已近三个月,感谢所有花时间精力和小编一路学习和成长的伙伴们,有你们的支持,我们继续再接再厉 **本人博客网站 **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的实现。
-
一般通过HttpServletRequest和HttpServletResponse获取HTTP请求信息和返回响应。事实上servlet理论上可以处理多种形式的请求响应形式 http只是其中之一 所以HttpServletRequest HttpServletResponse分别是ServletRequest和ServletResponse的子类。一般,HttpServlet对应HttpServletRequest和HttpServletResponse。
官方用户指南
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是 Java 命名与目录接口,是属于 J2EE 规范的,Tomcat 对其进行了实现。JNDI 在 J2EE 中的角色就是“交换机”,即 J2EE 组件在运行时间接地查找其他组件、资源或服务的通用机制(你可以简单理解为给资源取个名字,再根据名字来找资源)。
-
提供了集群功能,可以将对应容器需要共享的数据同步到集群中的其他 Tomcat 实例中。
-
提供了容器级别的用户-密码-权限的数据对象,配合资源认证模块使用。
-
Web 应用加载器,用于加载 Web 应用的资源,它要保证不同 Web 应用之间的资源隔离。
-
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 "%r" %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。
我们知道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源码和设计模式做进一步剖析和分享
- 源码编译安装LNMP架构环境
- Linux+php+apache+oracle环境搭建之CentOS下源码编译安装PHP
- ubuntu环境源码编译android
- lamp环境下源码编译安装intl扩展
- 源码编译搭建LAMP环境
- hadoop源码编译环境安装配置
- Thingsboard入门教程:本地环境搭建和源码编译安装,献给thingsboard编译失败的同学,教程不断完善中,文章最后是thingsboard常见编译失败的问题总结
- SUSE Linux 64位环境下编译hadoop2.2.0源码
- centos7下搭建lnmp环境包含yum源直接安装和源码编译安装,以及php5.3和php7.6版本
- 源码编译方式给LAMP环境加个XCache扩展
- Mac os android源码下载,环境配置 mountain lion10.8.2 配置源码下载环境,并编译源码
- JMeter二次开发(1)-eclipse环境配置及源码编译
- 配置android编译环境和源码下载
- Android 7.1 源码下载编译 (Ubuntu 14.0.4 环境下)
- Android 源码下载 编译 环境变量设置
- opensuse linux环境下telnet源码编译安装步骤
- android源码编译环境,应用程序编译环境
- Freescale I.mx 6 Android 4.2.2源码编译环境搭建(基于ubuntu12.04 LTS)
- LAMP架构——PHP-5.6.30源码编译安装
- 源码编译安装ansible环境以及排错记录