1.源码下载与构建
1.1 主要流程
#1.下载源代码
http://archive.apache.org/dist/tomcat/tomcat-8/v8.5.55/bin/
#2.解压源代码, 并进入该目录
/Programming/apache-tomcat-8.5.55-src
#3.在源代码目录的bin同级目录下, 创建 resources目录
#4.将与bin目录同级的conf和webapps目录移动到新创建的 resources 目录下
#5.在bin同级目录下, 创建pom.xml文件, 注意artifactId, 见下文图示
#6.IDEA中导入源代码, 见下文图示
#7.修改源码手动将JSP解析器初始化
找到ContextConfig中的configureStart方法,在 webConfig(); 后面加上:
context.addServletContainerInitializer(new JasperInitializer(), null);
#9.启动源代码, 见下文图示
apache-tomcat-8.5.55-src\java\org\apache\catalina\startup\Bootstrap.java
#启动参数配置, 见下文图示
-Dcatalina.home=/Programming/apache-tomcat-8.5.55-src/resources
-Dcatalina.base=/Programming/apache-tomcat-8.5.55-src/resources
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/Programming/apache-tomcat-8.5.55-src/resources/conf/logging.properties
-Duser.language=en
-Duser.region=US
-Dfile.encoding=UTF-8
#10.启动后, 访问即可
http://localhost:8080
1.2 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.55-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>8</source>
<target>8</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.3 源码导入
tomcat源码导入.png1.4 启动类&&启动参数设置
tomcat源码启动类参数设置.png2.tomcat 基本介绍
2.1 tomcat 目录结构
bin 用于存放Tomcat的启动、停止等批处理脚本和Shell脚本
conf 用于存放Tomcat的相关配置文件
conf/Catalina 用于存储针对每个虚拟机的Context配置
conf/context.xml 用于定义所有Web应用均需要加载的Context配置,如果Web应用指定了自己的context.xml,那么该文件的配置将被覆盖
conf/catalina.properties Tomcat环境变量配置
conf/catalina.policy 当Tomcat在安全模式下运行时,此文件为默认的安全策略配置
conf/logging.properties Tomcat日志配置文件,可通过该文件修改Tomcat日志级别以及日志路径等
conf/server.xml Tomcat服务器核心配置文件,用于配置Tomcat的链接器、监听端口、处理请求的虚拟主机等。可以说,Tomcat主要根据该文件的配置信息创建服务器实例
conf/tomcat-users.xml 用于定义Tomcat默认用户及角色映射信息,Tomcat的Manager模块即用该文件中定义的用户进行安全认证
conf/web.xml Tomcat中所有应用默认的部署描述文件,主要定义了基础Servlet和MIME映射。如果应用中不包含Web.xml,那么Tomcat将使用此文件初始化部署描述,反之,Tomcat会在启动时将默认部署描述与自定义配置进行合并
lib Tomcat服务器依赖库目录,包含Tomcat服务器运行环境依赖Jar包
logs Tomcat默认的日志存放路径
webapps Tomcat默认的Web应用部署目录
sork Web应用JSP代码生成和编译临时目录
2.2 tomcat 8.5 之后的新特性
>> 自8.0版本开始,Tomcat支持Servlet 3.1 、JSP 2.3、EL 3.0、WebSocket 1.l;并且自9.0版本开始支持Servlet 4.0。
>> 为了让用户提前体验Servlet 4.0的新特性,在8.5版本中,Tomcat提供了一套Servlet 4.0预览API ( servlet4preview,它们并不属于规范,而是Tomcat的一部分,也不会包含到9.0版本当中)。
>> 自8.0版本开始,默认的HTTP、AJP链接器采用NIO,而非Tomcat 7以及之前版本的BIO;并且自8.5开始,Tomcat移除了对BIO的支持。
>> 在8.0版本中,Tomcat提供了一套全新的资源实现,采用单独、一致的方法配置Web应用的附加资源,以替代原有的Aliases、VirtualLoader、VirtualDirContext、JAR。新的资源方案可以用于实现覆盖。例如可以将一个WAR作为多个Web应用的基础,同时这些Web应用各自拥有自己的定制功能。
>> 自8.0版本开始,链接器新增支持JDK 7的NIO2。
>> 自8.0版本开始,链接器新增支持HTTP/2协议。
>> 默认采用异步日志处理方式。
2.3 tomcat项目部署的几种方式
3.tomcat整体架构
#总体来讲
tomcat 是一个 <Http 服务器 && Servlet> 容器:
>> 屏蔽了应用层协议和网络通信细节,封装了标准的 Request 和 Response 对象;
>> 对于具体的业务逻辑则作为变化点,交给使用者来实现。
>> 比如使用 SpringMVC 框架,不必考虑 TCP 连接等, 因为 Tomcat 已经已封装好, 使用者只需要关注每个请求的具体业务逻辑。
class Tomcat {
List<Servlet> servletContainer;
}
#基于设计模式: 封装变与不变
Tomcat 内部隔离了变化点与不变点,使用了组件化设计,目的就是为了实现「俄罗斯套娃式」的高度定制化(组合模式),
而每个组件的生命周期管理又有一些共性的东西,则被提取出来成为接口和抽象类,让具体子类实现变化点,也就是模板方法设计模式(LifeCycleBase)。
当今流行的微服务也是这个思路,按照功能将单体应用拆成「微服务」,拆分过程要将共性提取出来,而这些共性就会成为核心的基础服务或者通用库。「中台」思想亦是如此。
#Servlet 是 Web 开发的基石
很多 Java Web 框架(比如 Spring)都是基于 Servlet 的封装(Netty 除外, Netty 不遵循 Servlet 规范),
Spring 应用本身就是一个 Servlet(DispatchSevlet),Tomcat/Jetty/Undertow 等 Web 容器,负责加载和运行 Servlet。
Servlet的位置.png
3.1 tomcat基础组件
Server 表示整个Servlet容器,因此Tomcat运行环境中只有唯一一个Server实例, 负责管理和启动多个Service, 同时监听8005端口对应的shutdown命令, 用于关闭整个Server
Service Service包含一个Container(即Engine)和多个Connector的集合,这些Connector共享同一个Container来处理其请求。在同一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立, 但共享同一JVM资源.
Connector 即Tomcat链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同协议以及不同的I/O方式
Container Container表示能够执行客户端请求并返回响应的一类对象。在Tomcat中存在不同级别的容器:Engine、Host、Context、Wrapper
Engine Engine表示整个Servlet引擎。在Tomcat中,Engine为最高层级的容器对象。尽管Engine不是直接处理请求的容器,却是获取目标容器的入口
Host Host作为一类容器,表示Servlet引擎(即Engine)中的虚拟机,与一个服务器的网络名有关,如域名等。客户端可以使用这个网络名连接服务器,这个名称必须要在DNS服务器上注册
Context Context作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示一个独立的Web应用,并且一个Engine可以包含多个Context.
Wrapper Wrapper作为一类容器,用于表示Web应用中定义的Servlet
Executor 表示Tomcat组件间可以共享的线程池
Loader:封装了 Java ClassLoader,用于 Container 加载类文件
Realm:Tomcat 中为 web 应用程序提供访问认证和角色管理的机制;
Pipeline:在容器中充当管道的作用,管道中可以设置各种 valve(阀门),请求和响应在经由管 道中各个阀门处理,提供了一种灵活可配置的处理请求和响应的机制。
JMX:Java SE 中定义技术规范,是一个为应用程序、设备、系统等植入管理功能的框架,通过 JMX 可以远程监控 Tomcat 的运行状态
Naming:命名服务,JNDI, Java 命名和目录接口,是一组在 Java 应用中访问命名和目录服务的 API。命名服务将名称和对象联系起来,使得我们可以用名称访问对象,目录服务也是一种命名 服务,对象不但有名称,还有属性。Tomcat 中可以使用 JNDI 定义数据源、配置信息,用于开发 与部署的分离。
Jasper:Tomcat 的 Jsp 解析引擎,用于将 Jsp 转换成 Java 文件,并编译成 class 文件。Session:负责管理和创建 session,以及 Session 的持久化(可自定义),支持 session 的集群。
此外,Tomcat的Container还有一个很重要的功能,就是后台处理。
在很多情况下,我们的Container需要执行一些异步处理,而且是定期执行,如每隔30秒执行一次,Tomcat对于Web应用文件变更的扫描就是通过该机制实现的。
Tomcat针对后台处理,在Container上定义了backgroundProcess()方法,并且其基础抽象类( ContainerBase)确保在启动组件的同时,异步启动后台处理。
因此,在绝大多数情况下,各个容器组件仅需要实现Container的background-Process()方法即可,不必考虑创建异步线程。
tomcat整体架构图.png
Tomcat的启动方式可以作为非常好的示范来指导中间件产品设计。
它实现了启动入口与核心环境的解耦,这样不仅简化了启动(不必配置各种依赖库,因为只有独立的几个API ),
而且便于我们更灵活地组织中间件产品的结构,尤其是类加载器的方案,
否则,我们所有的依赖库将统一放置到一个类加载器中,而无法做到灵活定制。
3.1.1 Connector (连接器)
#Tomcat支持的 I/O 模型有:
>> NIO:非阻塞 I/O,采用 Java NIO 类库实现。
>> NIO2:异步I/O,采用 JDK 7 最新的 NIO2 类库实现。
>> APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。
#Tomcat 支持的应用层协议有:
>> HTTP/1.1:这是大部分 Web 应用采用的访问协议。
>> AJP:用于和 Web 服务器集成(如 Apache)。
>> HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
所以一个容器可能对接多个连接器。
连接器对 Servlet 容器屏蔽了网络协议与 I/O 模型的区别,
无论是 Http 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。
#细化连接器的功能需求就是:
>> 监听网络端口。
>> 接受网络连接请求。
>> 读取请求网络字节流。
>> 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
>> 将 Tomcat Request 对象转成标准的 ServletRequest。
>> 调用 Servlet容器,得到 ServletResponse。
>> 将 ServletResponse转成 Tomcat Response 对象。
>> 将 Tomcat Response 转成网络字节流。
>> 将响应字节流写回给浏览器。
#需求列清楚后,我们要考虑的下一个问题是,连接器应该有哪些子模块?
优秀的模块化设计应该考虑高内聚、低耦合。
>> 高内聚是指相关度比较高的功能要尽可能集中,不要分散。
>> 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
#我们发现连接器需要完成 3 个高内聚的功能:
>> 网络通信。
>> 应用层协议解析。
>> Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。
网络通信的 I/O 模型是变化的, 应用层协议也是变化的,但是整体的处理逻辑是不变的,
>> EndPoint 负责提供字节流给 Processor,
>> Processor负责提供 Tomcat Request 对象给 Adapter,
>> Adapter负责提供 ServletRequest对象给容器。
#封装变与不变
因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类 AbstractProtocol实现了 ProtocolHandler接口。
每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol和 AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
这就是模板方法设计模式的运用。
Tomcat_Connector_Container.png
3.1.1.1 ProtocolHandler组件
ProtocoHandler主要处理网络连接和应用层协议,包含了两个重要部件 EndPoint 和 Processor。
#1.EndPoint
EndPoint是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,
因此 EndPoint是用来实现 TCP/IP 协议数据读写的,本质调用操作系统的 socket 接口。
EndPoint是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint的具体子类,
比如在 NioEndpoint和 Nio2Endpoint中,有两个重要的子组件:Acceptor和 SocketProcessor。
>> Acceptor 用于监听 Socket 连接请求。
>> SocketProcessor用于处理 Acceptor 接收到的 Socket请求,它实现 Runnable接口,在 Run方法里调用应用层协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。
#2.对于 Java 的多路复用器的使用,无非是两步:
创建一个 Seletor,在它身上注册各种感兴趣的事件,然后调用 select 方法,等待感兴趣的事情发生。
感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从 Channel 中读数据。
在 Tomcat 中 NioEndpoint 则是 AbstractEndpoint 的具体实现,里面组件虽然很多,但是处理逻辑还是前面两步。
它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor和 Executor 共 5 个组件,分别分工合作实现整个 TCP/IP 协议的处理。
>> LimitLatch 是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。
>> Acceptor跑在一个单独的线程里,它在一个死循环里调用 accept方法来接收新连接,
一旦有新的连接请求到来,accept方法返回一个 Channel 对象,接着把 Channel对象交给 Poller 去处理。
>> Poller 的本质是一个 Selector,也跑在单独线程里。
Poller在内部维护一个 Channel数组,它在一个死循环里不断检测 Channel的数据就绪状态,
一旦有 Channel可读,就生成一个 SocketProcessor任务对象扔给 Executor去处理。
>> SocketProcessor 实现了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); 代码则是获取 handler 并执行处理 socketWrapper,
最后通过 socket 获取合适应用层协议处理器,也就是调用 Http11Processor 组件来处理请求。
Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象,Http11Processor 并不是直接读取 Channel 的。
这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 AsynchronousSocketChannel 和 SocketChannel,
为了对 Http11Processor 屏蔽这些差异,Tomcat 设计了一个包装类叫作 SocketWrapper,Http11Processor 只调用 SocketWrapper 的方法去读写数据。
>> Executor就是线程池,负责运行 SocketProcessor任务类,SocketProcessor 的 run方法会调用 Http11Processor 来读取和解析请求数据。
我们知道,Http11Processor是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel写出。
#3.Processor
Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,
读取字节流解析成 Tomcat Request 和 Response 对象,
并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。
#4.请求由Connector传导到Container
EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,
SocketProcessor 的 Run 方法会调用 HttpProcessor 组件去解析应用层协议,
Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法,
方法内部通过 以下代码将请求传递到容器中。
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
ProtocolHandler工作流程.png
Processor的位置.png
3.1.1.2 Adapter 组件
由于协议的不同,Tomcat 定义了自己的 Request 类来存放请求信息,这里其实体现了面向对象的思维。
但是这个 Request 不是标准的 ServletRequest ,所以不能直接使用 Tomcat 定义 Request 作为参数直接容器。
Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,
传入的是 Tomcat Request 对象,CoyoteAdapter负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service方法。
3.1.2 容器
连接器负责外部交流,容器负责内部处理。
Connector: 连接器处理 Socket 通信和应用层协议的解析,得到 Servlet请求;
Container: 容器则负责处理 Servlet请求。
容器就是拿来装东西的, 所以 Tomcat 容器就是拿来装载 Servlet。
Tomcat 设计了 4 种容器(父子关系),分别是 Engine、Host、Context和 Wrapper。Server 代表 Tomcat 实例。
#你可能会问,为啥要设计这么多层次的容器,这不是增加复杂度么?
原因在于Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。
因为这里一个 Host 多个 Context, 一个 Context 也包含多个 Servlet,而每个组件都需要统一生命周期管理,所以组合模式设计这些容器:
>> Wrapper 表示一个 Servlet ,Context 表示一个 Web 应用程序,而一个 Web 程序可能有多个 Servlet ;
>> Host 表示一个虚拟主机,或者说一个站点。
>> 一个 Tomcat 可以配置多个站点(Host);
>> 一个站点( Host) 可以部署多个 Web 应用;
>> Engine 代表 引擎,用于管理多个站点(Host)
>> 一个 Service 只能有一个 Engine。
#可通过 Tomcat 配置文件加深对其层次关系理解。
// Server 顶层组件,可包含多个 Service,代表一个 Tomcat 实例
<Server port="8005" shutdown="SHUTDOWN">
// 顶层组件,包含一个 Engine ,多个连接器
<Service name="Catalina">
// 连接器
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
// 连接器
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
// 容器组件:一个 Engine 处理 Service 所有请求,包含多个 Host
<Engine name="Catalina" defaultHost="localhost">
// 容器组件:处理指定Host下的客户端请求, 可包含多个 Context
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
// 容器组件:处理特定 Context Web应用的所有客户端请求
<Context></Context>
</Host>
</Engine>
</Service>
</Server>
3.1.2.1 如何管理这些容器: 组合模式
我们发现容器之间具有父子关系,形成一个树形结构,Tomcat 就是用组合模式来管理这些容器的。
具体实现方法是,所有容器组件都实现了 Container 接口,
因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。
这里单容器对象指的是最底层的 Wrapper,组合容器对象指的是上面的 Context、Host或者 Engine。
Container 接口定义如下:
public interface Container extends Lifecycle {
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
}
getParent、SetParent、addChild和 removeChild等方法,这里正好验证了组合模式。
Container接口拓展了 Lifecycle ,Tomcat 就是通过 Lifecycle 统一管理所有容器的组件的生命周期。
通过组合模式管理所有容器,拓展 Lifecycle 实现对每个组件的生命周期管理,
Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()。
3.2 tomcat服务器启动与请求处理过程
tomcat应用服务器启动过程示意图.png tomcat请求处理过程示意图.png3.2.1 服务器启动原理
#Tomcat整体架构
>> Server 对应的就是一个 Tomcat 实例。
>> Service 默认只有一个,也就是一个 Tomcat 实例默认一个 Service。
>> Connector:一个 Service 可能多个 连接器,接受不同连接协议。
>> Container: 多个连接器对应一个容器,顶层容器其实就是 Engine。
每个组件都有对应的生命周期,需要启动,同时还要启动自己内部的子组件,
比如一个 Tomcat 实例包含一个 Service,一个 Service 包含多个连接器和一个容器。
而一个容器包含多个 Host, Host 内部可能有多个 Contex t 容器,而一个 Context 也会包含多个 Servlet,
所以 Tomcat 利用组合模式管理组件每个组件,对待过个也想对待单个组一样对待。
整体上每个组件设计就像是「俄罗斯套娃」一样。
#Tomcat 启动流程:
startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()
#Tomcat 有 2 个核心功能:
>> 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
>> 加载并管理 Servlet ,以及处理具体的 Request 请求。
所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部处理。
Tomcat为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。
组合/观察者/模板模式在LifeCycle中的运用
#Lifecycle 生命周期
前面我们看到 Container容器 继承了 Lifecycle 生命周期。
如果想让一个系统能够对外提供服务,我们需要创建、组装并启动这些组件;
在服务停止的时候,我们还需要释放资源,销毁这些组件,因此这是一个动态的过程。
也就是说,Tomcat 需要动态地管理这些组件的生命周期。
如何统一管理组件的创建、初始化、启动、停止和销毁?如何做到代码逻辑清晰?
如何方便地添加或者删除组件?如何做到组件启动和停止不遗漏、不重复?
#答案是: 一键式启停:LifeCycle 接口
设计就是要找到系统的变化点和不变点。
这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。
而变化点是每个具体组件的初始化方法,也就是启动方法是不一样的。
因此,Tomcat 把不变点抽象出来成为一个接口,这个接口跟生命周期有关,叫作 LifeCycle。
LifeCycle 接口里定义这么几个方法:init()、start()、stop() 和 destroy(),每个具体的组件(也就是容器)去实现这些方法。
在父组件的 init() 方法里需要创建子组件并调用子组件的 init() 方法。
同样,在父组件的 start()方法里也需要调用子组件的 start() 方法,
因此调用者可以无差别的调用各组件的 init() 方法和 start() 方法,这就是组合模式的使用,
并且只要调用最顶层组件,也就是 Server 组件的 init()和start() 方法,整个 Tomcat 就被启动起来了。
所以 Tomcat 采取组合模式管理容器,容器继承 LifeCycle 接口,
这样就可以向针对单个对象一样一键管理各个容器的生命周期,整个 Tomcat 就启动起来。
#重用性(模板设计模式):LifeCycleBase 抽象基类
有了接口,我们就要用类去实现接口。一般来说实现类不止一个,不同的类在实现接口时往往会有一些相同的逻辑,
如果让各个子类都去实现一遍,就会有重复代码。那子类如何重用这部分逻辑呢?
其实就是定义一个基类来实现共同的逻辑,然后让各个子类去继承它,就达到了重用的目的。
Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口,把一些公共的逻辑放到基类中去,
比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。
#可扩展性:LifeCycle Event
我们再来考虑另一个问题,那就是系统的可扩展性。
因为各个组件init() 和 start() 方法的具体实现是复杂多变的,
比如在 Host 容器的启动方法里需要扫描 webapps 目录下的 Web 应用,创建相应的 Context 容器,
如果将来需要增加新的逻辑,直接修改start() 方法?这样会违反开闭原则,那如何解决这个问题呢?
开闭原则说的是为了扩展系统的功能,你不能直接修改系统中已有的类,但是你可以定义新的类。
组件的 init() 和 start() 调用是由它的父组件的状态变化触发的,上层组件的初始化会触发子组件的初始化,
上层组件的启动会触发子组件的启动,因此我们把组件的生命周期定义成一个个状态,把状态的转变看作是一个事件。
而事件是有监听器的,在监听器里可以实现一些逻辑,并且监听器也可以方便的添加和删除,这就是典型的观察者模式。
#接口分离的原则的体现
Container 继承了 LifeCycle,
StandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相应容器组件的具体实现类,因为它们都是容器,所以继承了 ContainerBase 抽象基类,
ContainerBase 实现了 Container 接口,也继承了 LifeCycleBase 类,它们的生命周期管理接口和功能接口是分开的,
这也符合设计中接口分离的原则。
#如果你需要维护一堆具有父子关系的实体,可以考虑使用组合模式。
public abstract class LifecycleBase implements Lifecycle {
// 持有所有的观察者
private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
// 发布事件
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
// 模板方法定义整个启动流程,启动所有容器
@Override
public final synchronized void init() throws LifecycleException {
//1. 状态检查
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}
try {
//2. 触发 INITIALIZING 事件的监听器
setStateInternal(LifecycleState.INITIALIZING, null, false);
// 3. 调用具体子类的初始化方法
initInternal();
// 4. 触发 INITIALIZED 事件的监听器
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setStateInternal(LifecycleState.FAILED, null, false);
throw new LifecycleException(sm.getString("lifecycleBase.initFail",toString()), t);
}
}
}
3.2.2 服务器接收请求原理
从本质上讲,应用服务器的请求处理开始于监听的Socket端口接收到数据,结束于将服务器处理结果写入Socket输出流。
#一个请求是如何定位到让哪个 Wrapper 的 Servlet 处理的?
Tomcat 是用 Mapper 组件来定位到让哪个 Wrapper 的 Servlet 处理请求的。
Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet,它的工作原理是:
Mapper组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,
比如 Host容器里配置的域名、Context容器里的 Web应用路径,以及 Wrapper容器里 Servlet 映射的路径,
你可以想象这些配置信息就是一个多层次的 Map。
当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。
一个请求 URL 最后只会定位到一个 Wrapper容器,也就是一个 Servlet。
#MVC框架进一步封装
当然,如果我们的应用不是基于简单的Servlet API,而是基于当前成熟的MVC框架(如Apache Struts、Spring MVC),
那么在多数情况下请求将进一步匹配到Servlet下的一个控制器——这部分已经不属于应用服务器的处理范畴,而是由具体的MVC框架进行匹配。
当Servlet或者控制器的业务处理结束后,处理结果将被写入一个与通信方案无关的响应对象。
最后,该响应对象将按照既定协议写入输出流。
3.2.3 Tomcat处理http请求流程
假如有用户访问一个 URL,比如http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
1.首先根据协议和端口号确定 Service 和 Engine。
Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。
上面例子中的 URL 访问的是 8080 端口,因此这个请求会被 HTTP 连接器接收,
而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。
我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,
具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。
2.根据域名选定 Host。
Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,
比如例子中的 URL 访问的域名是user.shopping.com,因此 Mapper 会找到 Host2 这个容器。
3.根据 URL 路径找到 Context 组件。
Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,
比如例子中访问的是 /order,因此找到了 Context4 这个 Context 容器。
4.根据 URL 路径找到 Wrapper(Servlet)。
Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的 Wrapper 和 Servlet。
Tomcat处理http请求流程.png
SpringMVC对HttpServlet的改造.png
3.2.4 父子容器的请求传递: Pipeline-Valve
连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,
Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,
依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。
那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。
#1.Pipeline-Valve 是责任链模式:
责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,
每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理,
Valve 表示一个处理点(也就是一个处理阀门),因此 invoke方法就是来处理请求的。
"
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}
public interface Pipeline {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
"
#2.Pipeline && Valve
Pipeline中有 addValve方法。Pipeline 中维护了 Valve链表,Valve可以插入到 Pipeline中,对请求做某些处理。
我们还发现 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,
Valve完成自己的处理后,调用 getNext.invoke() 来触发下一个 Valve 调用。
其实每个容器都有一个 Pipeline 对象,只要触发了这个 Pipeline 的第一个 Valve,这个容器里 Pipeline中的 Valve 就都会被调用到。
但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。
答案是利用 Pipeline 中的 getBasic方法。
这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline中必不可少的一个 Valve,
负责调用下层容器的 Pipeline 里的第一个 Valve。
#3.整个过程分是通过连接器中的 CoyoteAdapter 触发,它会调用 Engine 的第一个 Valve
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
// 省略其他代码
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
...
}
Pipeline-Valve.png
Pipeline-Valve.png
3.2.5 Filter机制
Wrapper 容器的最后一个 Valve 会创建一个 Filter 链,并调用 doFilter() 方法,最终会调到 Servlet的 service方法。
前面我们不是讲到了 Valve,似乎也有相似的功能,那 Valve 和 Filter有什么区别吗?
>> Valve是 Tomcat的私有机制,与 Tomcat 的基础架构 API是紧耦合的。
Servlet API是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。
>> Valve工作在 Web 容器级别,拦截所有应用的请求;而 Servlet Filter 工作在应用级别,只能拦截某个 Web 应用的所有请求。
如果想做整个 Web容器的拦截器,必须通过 Valve来实现。
3.3 tomcat生命周期事件
Tomcat生命周期事件与状态映射.png3.4 tomcat类加载器
参考此文:
https://www.cnblogs.com/psy-code/p/14853753.html
3.5 tomcat热部署与热加载
得益于tomcat的类加载器设计模型, tomcat支持热部署与热加载.
3.5.1 tomcat 热加载
3.5.2 tomcat 热部署
3.6 tomcat 四种线程模型
#通过配置方法 server.xml来改变
#1.NIO 同步非阻塞
比传统BIO能更好的支持大并发,tomcat 8.0 后默认采用该模式 <Connector port="8080" protocol="HTTP/1.1"/> 改为 protocol="org.apache.coyote.http11.Http11NioProtocol"
#2.BIO 阻塞式IO
tomcat7之前默认,采用传统的java IO进行操作,该模式下每个请求都会创建一个线程,适用于并发量小的场景 protocol =" org.apache.coyote.http11.Http11Protocol"
#3.APR
tomcat 以JNI形式调用http服务器的核心动态链接库来处理文件读取或网络传输操作,需要编译安装APR库 protocol ="org.apache.coyote.http11.Http11AprProtocol"
#4.AIO 异步非阻塞 (NIO2)
tomcat8.0后支持 protocol ="org.apache.coyote.http11.Http11Nio2Protocol" 多用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持
参考资源
<<Tomcat架构解析>> 刘光瑞
<<看透springMvc源代码分析与实践>> 韩路彪
https://www.jianshu.com/p/075ec0deb1f3
网友评论