纯净、安全、绿色的下载网站

首页

当前位置:首页IT学院IT技术

Tomcat 架构原理 架构设计 解析Tomcat架构原理到架构设计

码哥字节   2021-06-22 我要评论
想了解解析Tomcat架构原理到架构设计的相关内容吗码哥字节在本文为您仔细讲解Tomcat 架构原理 架构设计的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Tomcat,架构原理,Tomcat,架构设计下面大家一起来学习吧

一、学习目的

1.1、掌握 Tomcat 架构设计与原理提高内功

宏观上看

Tomcat 作为一个 「Http 服务器 + Servlet 容器」对我们屏蔽了应用层协议和网络通信细节给我们的是标准的 RequestResponse 对象对于具体的业务逻辑则作为变化点交给我们来实现我们使用了SpringMVC 之类的框架可是却从来不需要考虑 TCP 连接、 Http 协议的数据处理与响应就是因为 Tomcat 已经为我们做好了这些我们只需要关注每个请求的具体业务逻辑

微观上看

Tomcat 内部也隔离了变化点与不变点使用了组件化设计目的就是为了实现「俄罗斯套娃式」的高度定制化(组合模式)而每个组件的生命周期管理又有一些共性的东西则被提取出来成为接口和抽象类让具体子类实现变化点也就是模板方法设计模式

当今流行的微服务也是这个思路按照功能将单体应用拆成「微服务」拆分过程要将共性提取出来而这些共性就会成为核心的基础服务或者通用库「中台」思想亦是如此

设计模式往往就是封装变化的一把利器合理的运用设计模式能让我们的代码与系统设计变得优雅且整洁

这就是学习优秀开源软件能获得的「内功」从不会过时其中的设计思想与哲学才是根本之道从中借鉴设计经验合理运用设计模式封装变与不变更能从它们的源码中汲取经验提升自己的系统设计能力

1.2、宏观理解一个请求如何与 Spring 联系起来

在工作过程中我们对 Java 语法已经很熟悉了甚至「背」过一些设计模式用过很多 Web 框架但是很少有机会将他们用到实际项目中让自己独立设计一个系统似乎也是根据需求一个个 Service 实现而已脑子里似乎没有一张 Java Web 开发全景图比如我并不知道浏览器的请求是怎么跟 Spring 中的代码联系起来的

为了突破这个瓶颈为何不站在巨人的肩膀上学习优秀的开源系统看大牛们是如何思考这些问题

学习 Tomcat 的原理我发现 Servlet 技术是 Web 开发的原点几乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封装Spring 应用本身就是一个 ServletDispatchSevlet)而 Tomcat 和 Jetty 这样的 Web 容器负责加载和运行 Servlet如图所示:

1.3、提升自己的系统设计能力

学习 Tomcat 我还发现用到不少 Java 高级技术比如 Java 多线程并发编程、Socket 网络编程以及反射等之前也只是了解这些技术为了面试也背过一些题但是总感觉「知道」与会用之间存在一道沟壑通过对 Tomcat 源码学习我学会了什么场景去使用这些技术

还有就是系统设计能力比如面向接口编程、组件化组合模式、骨架抽象类、一键式启停、对象池技术以及各种设计模式比如模板方法、观察者模式、责任链模式等之后我也开始模仿它们并把这些设计思想运用到实际的工作中

二、整体架构设计

今天咱们就来一步一步分析 Tomcat 的设计思路一方面我们可以学到 Tomcat 的总体架构学会从宏观上怎么去设计一个复杂系统怎么设计顶层模块以及模块之间的关系另一方面也为我们深入学习 Tomcat 的工作原理打下基础

Tomcat 启动流程:

startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()

Tomcat 实现的 2 个核心功能:

  • 处理 Socket 连接负责网络字节流与 RequestResponse 对象的转化
  • 加载并管理 Servlet 以及处理具体的 Request 请求

所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)连接器负责对外交流容器负责内部 处理

Tomcat为了实现支持多种 I/O 模型和应用层协议一个容器可能对接多个连接器就好比一个房间有多个门

  • Server 对应的就是一个 Tomcat 实例
  • Service 默认只有一个也就是一个 Tomcat 实例默认一个 Service
  • Connector:一个 Service 可能多个 连接器接受不同连接协议
  • Container: 多个连接器对应一个容器顶层容器其实就是 Engine

每个组件都有对应的生命周期需要启动同时还要启动自己内部的子组件比如一个 Tomcat 实例包含一个 Service一个 Service 包含多个连接器和一个容器而一个容器包含多个 Host Host 内部可能有多个 Contex t 容器而一个 Context 也会包含多个 Servlet所以 Tomcat 利用组合模式管理组件每个组件对待过个也想对待单个组一样对待整体上每个组件设计就像是「俄罗斯套娃」一样

2.1、连接器

在开始讲连接器前我先铺垫一下 Tomcat支持的多种 I/O 模型和应用层协议

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/ResponseServletRequest/ServletResponse 的转化

因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能分别是 EndPoint、Processor 和 Adapter

网络通信的 I/O 模型是变化的, 应用层协议也是变化的但是整体的处理逻辑是不变的EndPoint 负责提供字节流给 ProcessorProcessor负责提供 Tomcat Request 对象给 AdapterAdapter负责提供 ServletRequest对象给容器

2.2、封装变与不变

因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分抽象基类 AbstractProtocol实现了 ProtocolHandler接口每一种应用层协议有自己的抽象基类比如 AbstractAjpProtocolAbstractHttp11Protocol具体协议的实现类扩展了协议层抽象基类

这就是模板方法设计模式的运用

总结下来连接器的三个核心组件 EndpointProcessorAdapter来分别做三件事情其中 EndpointProcessor放在一起抽象成了 ProtocolHandler组件它们的关系如下图所示

ProtocolHandler 组件:

主要处理 网络连接 和 应用层协议 包含了两个重要部件 EndPoint 和 Processor两个组件组合形成 ProtocoHandler下面我来详细介绍它们的工作原理

EndPoint:

EndPoint是通信端点即通信监听的接口是具体的 Socket 接收和发送处理器是对传输层的抽象因此 EndPoint是用来实现 TCP/IP 协议数据读写的本质调用操作系统的 socket 接口

EndPoint是一个接口对应的抽象实现类是 AbstractEndpoint而 AbstractEndpoint的具体子类比如在 NioEndpointNio2Endpoint中有两个重要的子组件:AcceptorSocketProcessor

其中 Acceptor 用于监听 Socket 连接请求SocketProcessor用于处理 Acceptor 接收到的 Socket请求它实现 Runnable接口在 Run方法里调用应用层协议处理组件 Processor 进行处理为了提高处理能力SocketProcessor被提交到线程池来执行

我们知道对于 Java 的多路复用器的使用无非是两步:

  • 创建一个 Seletor在它身上注册各种感兴趣的事件然后调用 select 方法等待感兴趣的事情发生
  • 感兴趣的事情发生了比如可以读了这时便创建一个新的线程从 Channel 中读数据

在 Tomcat 中 NioEndpoint 则是 AbstractEndpoint 的具体实现里面组件虽然很多但是处理逻辑还是前面两步它一共包含 LimitLatchAcceptorPollerSocketProcessorExecutor 共 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 设计了一个包装类叫作 SocketWrapperHttp11Processor 只调用 SocketWrapper 的方法去读写数据

Executor就是线程池负责运行 SocketProcessor任务类SocketProcessorrun方法会调用 Http11Processor 来读取和解析请求数据我们知道Http11Processor是应用层协议的封装它会调用容器获得响应再把响应通过 Channel写出

工作流程如下所示:

Processor:

Processor 用来实现 HTTP 协议Processor 接收来自 EndPoint 的 Socket读取字节流解析成 Tomcat Request 和 Response 对象并通过 Adapter 将其提交到容器处理Processor 是对应用层协议的抽象

从图中我们看到EndPoint 接收到 Socket 连接后生成一个 SocketProcessor 任务提交到线程池去处理SocketProcessor 的 Run 方法会调用 HttpProcessor 组件去解析应用层协议Processor 通过解析生成 Request 对象后会调用 Adapter 的 Service 方法方法内部通过 以下代码将请求传递到容器中

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Adapter 组件:

由于协议的不同Tomcat 定义了自己的 Request 类来存放请求信息这里其实体现了面向对象的思维但是这个 Request 不是标准的 ServletRequest 所以不能直接使用 Tomcat 定义 Request 作为参数直接容器

Tomcat 设计者的解决方案是引入 CoyoteAdapter这是适配器模式的经典运用连接器调用 CoyoteAdapterSevice 方法传入的是 Tomcat Request 对象CoyoteAdapter负责将 Tomcat Request 转成 ServletRequest再调用容器的 Service方法

2.3、容器

连接器负责外部交流容器负责内部处理具体来说就是连接器处理 Socket 通信和应用层协议的解析得到 Servlet请求而容器则负责处理 Servlet请求

容器:顾名思义就是拿来装东西的 所以 Tomcat 容器就是拿来装载 Servlet

Tomcat 设计了 4 种容器分别是 EngineHostContextWrapperServer 代表 Tomcat 实例

要注意的是这 4 种容器不是平行关系属于父子关系如下图所示:

你可能会问为啥要设计这么多层次的容器这不是增加复杂度么?其实这背后的考虑是Tomcat 通过一种分层的架构使得 Servlet 容器具有很好的灵活性因为这里正好符合一个 Host 多个 Context 一个 Context 也包含多个 Servlet而每个组件都需要统一生命周期管理所以组合模式设计这些容器

Wrapper 表示一个 Servlet Context 表示一个 Web 应用程序而一个 Web 程序可能有多个 Servlet Host 表示一个虚拟主机或者说一个站点一个 Tomcat 可以配置多个站点(Host)一个站点( Host) 可以部署多个 Web 应用Engine 代表 引擎用于管理多个站点(Host)一个 Service 只能有 一个 Engine

可通过 Tomcat 配置文件加深对其层次关系理解

<Server port="8005" shutdown="SHUTDOWN"> // 顶层组件可包含多个 Service代表一个 Tomcat 实例

  <Service name="Catalina">  // 顶层组件包含一个 Engine 多个连接器
    <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>

如何管理这些容器?我们发现容器之间具有父子关系形成一个树形结构是不是想到了设计模式中的 组合模式 

Tomcat 就是用组合模式来管理这些容器的具体实现方法是所有容器组件都实现了 Container接口因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性这里单容器对象指的是最底层的 Wrapper组合容器对象指的是上面的 ContextHost或者 EngineContainer 接口定义如下:

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);
}

我们看到了getParentSetParentaddChildremoveChild等方法这里正好验证了我们说的组合模式我们还看到 Container接口拓展了 Lifecycle Tomcat 就是通过 Lifecycle 统一管理所有容器的组件的生命周期通过组合模式管理所有容器拓展 Lifecycle 实现对每个组件的生命周期管理 Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()

2.4、请求定位 Servlet 的过程

一个请求是如何定位到让哪个 WrapperServlet 处理的?答案是Tomcat 是用 Mapper 组件来完成这个任务的

Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet它的工作原理是:Mapper组件里保存了 Web 应用的配置信息其实就是容器组件与访问路径的映射关系比如 Host容器里配置的域名、Context容器里的 Web应用路径以及 Wrapper容器里 Servlet 映射的路径你可以想象这些配置信息就是一个多层次的 Map

当一个请求到来时Mapper 组件通过解析请求 URL 里的域名和路径再到自己保存的 Map 里去查找就能定位到一个 Servlet请你注意一个请求 URL 最后只会定位到一个 Wrapper容器也就是一个 Servlet

假如有用户访问一个 URL比如图中的http://user.shopping.com:8080/order/buyTomcat 如何将这个 URL 定位到一个 Servlet 呢?

1.首先根据协议和端口号确定 Service 和 EngineTomcat 默认的 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

连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet最先拿到请求的是 Engine 容器Engine 容器对请求做一些处理后会把请求传给自己子容器 Host 继续处理依次类推最后这个请求会传给 Wrapper 容器Wrapper 会调用最终的 Servlet 来处理那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道

Pipeline-Valve 是责任链模式责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理每个处理者负责做自己相应的处理处理完之后将再调用下一个处理者继续处理Valve 表示一个处理点(也就是一个处理阀门)因此 invoke方法就是来处理请求的

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

继续看 Pipeline 接口

public interface Pipeline {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

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

整个过程分是通过连接器中的 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);
    ...
}

Wrapper 容器的最后一个 Valve 会创建一个 Filter 链并调用 doFilter() 方法最终会调到 Servletservice方法

前面我们不是讲到了 Filter似乎也有相似的功能那 ValveFilter有什么区别吗?它们的区别是:

  • ValveTomcat的私有机制与 Tomcat 的基础架构 API是紧耦合的Servlet API是公有的标准所有的 Web 容器包括 Jetty 都支持 Filter 机制
  • 另一个重要的区别是 Valve工作在 Web 容器级别拦截所有应用的请求而 Servlet Filter 工作在应用级别只能拦截某个 Web 应用的所有请求如果想做整个 Web容器的拦截器必须通过 Valve来实现

Lifecycle 生命周期

前面我们看到 Container容器 继承了 Lifecycle 生命周期如果想让一个系统能够对外提供服务我们需要创建、组装并启动这些组件在服务停止的时候我们还需要释放资源销毁这些组件因此这是一个动态的过程也就是说Tomcat 需要动态地管理这些组件的生命周期

如何统一管理组件的创建、初始化、启动、停止和销毁?如何做到代码逻辑清晰?如何方便地添加或者删除组件?如何做到组件启动和停止不遗漏、不重复?

一键式启停:LifeCycle 接口

设计就是要找到系统的变化点和不变点这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程这些状态以及状态的转化是不变的而变化点是每个具体组件的初始化方法也就是启动方法是不一样的

因此Tomcat 把不变点抽象出来成为一个接口这个接口跟生命周期有关叫作 LifeCycleLifeCycle 接口里定义这么几个方法:init()、start()、stop() 和 destroy()每个具体的组件(也就是容器)去实现这些方法

在父组件的 init() 方法里需要创建子组件并调用子组件的 init() 方法同样在父组件的 start()方法里也需要调用子组件的 start() 方法因此调用者可以无差别的调用各组件的 init() 方法和 start() 方法这就是组合模式的使用并且只要调用最顶层组件也就是 Server 组件的 init()start() 方法整个 Tomcat 就被启动起来了所以 Tomcat 采取组合模式管理容器容器继承 LifeCycle 接口这样就可以向针对单个对象一样一键管理各个容器的生命周期整个 Tomcat 就启动起来

可扩展性:LifeCycle 事件

我们再来考虑另一个问题那就是系统的可扩展性因为各个组件init()start() 方法的具体实现是复杂多变的比如在 Host 容器的启动方法里需要扫描 webapps 目录下的 Web 应用创建相应的 Context 容器如果将来需要增加新的逻辑直接修改start() 方法?这样会违反开闭原则那如何解决这个问题呢?开闭原则说的是为了扩展系统的功能你不能直接修改系统中已有的类但是你可以定义新的类

组件的 init()start() 调用是由它的父组件的状态变化触发的上层组件的初始化会触发子组件的初始化上层组件的启动会触发子组件的启动因此我们把组件的生命周期定义成一个个状态把状态的转变看作是一个事件而事件是有监听器的在监听器里可以实现一些逻辑并且监听器也可以方便的添加和删除这就是典型的观察者模式

以下就是 Lyfecycle 接口的定义:

重用性:LifeCycleBase 抽象基类

再次看到抽象模板设计模式

有了接口我们就要用类去实现接口一般来说实现类不止一个不同的类在实现接口时往往会有一些相同的逻辑如果让各个子类都去实现一遍就会有重复代码那子类如何重用这部分逻辑呢?其实就是定义一个基类来实现共同的逻辑然后让各个子类去继承它就达到了重用的目的

Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口把一些公共的逻辑放到基类中去比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等而子类就负责实现自己的初始化、启动和停止等方法

public abstract class LifecycleBase implements Lifecycle{
    // 持有所有的观察者
    private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
    /**
     * 发布事件
     *
     * @param type  Event type
     * @param data  Data associated with event.
     */
    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);
        }
    }
}

Tomcat 为了实现一键式启停以及优雅的生命周期管理并考虑到了可扩展性和可重用性将面向对象思想和设计模式发挥到了极致Containaer接口维护了容器的父子关系Lifecycle 组合模式实现组件的生命周期维护生命周期每个组件有变与不变的点运用模板方法模式 分别运用了组合模式、观察者模式、骨架抽象类和模板方法

如果你需要维护一堆具有父子关系的实体可以考虑使用组合模式

观察者模式听起来 “高大上”其实就是当一个事件发生后需要执行一连串更新操作实现了低耦合、非侵入式的通知与更新机制

Container 继承了 LifeCycleStandardEngine、StandardHost、StandardContext 和 StandardWrapper 是相应容器组件的具体实现类因为它们都是容器所以继承了 ContainerBase 抽象基类而 ContainerBase 实现了 Container 接口也继承了 LifeCycleBase 类它们的生命周期管理接口和功能接口是分开的这也符合设计中接口分离的原则

三、Tomcat 为何打破双亲委派机制

3.1、双亲委派

我们知道 JVM的类加载器加载 Class 的时候基于双亲委派机制也就是会将加载交给自己的父加载器加载如果 父加载器为空则查找Bootstrap 是否加载过当无法加载的时候才让自己加载JDK 提供一个抽象类 ClassLoader这个抽象类中定义了三个关键方法对外使用loadClass(String name) 用于子类重写打破双亲委派:loadClass(String name, boolean resolve)

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 查找该 class 是否已经被加载过
        Class<?> c = findLoadedClass(name);
        // 如果没有加载过
        if (c == null) {
            // 委托给父加载器去加载递归调用
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                // 如果父加载器为空查找 Bootstrap 是否加载过
                c = findBootstrapClassOrNull(name);
            }
            // 若果依然加载不到则调用自己的 findClass 去加载
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
protected Class<?> findClass(String name){
    //1. 根据传入的类名 name到在特定目录下去寻找类文件把.class 文件读入内存
    ...

        //2. 调用 defineClass 将字节数组转成 Class 对象
        return defineClass(buf, off, len)
}

// 将字节码数组解析成一个 Class 对象用 native 方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){
    ...
}

JDK 中有 3 个类加载器另外你也可以自定义类加载器它们的关系如下图所示

  • BootstrapClassLoader是启动类加载器由 C 语言实现用来加载 JVM启动时所需要的核心类比如rt.jarresources.jar等
  • ExtClassLoader是扩展类加载器用来加载\jre\lib\ext目录下 JAR 包
  • AppClassLoader是系统类加载器用来加载 classpath下的类应用程序默认用它来加载类
  • 自定义类加载器用来加载自定义路径下的类

这些类加载器的工作原理是一样的区别是它们的加载路径不同也就是说 findClass这个方法查找的路径不同双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的假如你不小心写了一个与 JRE 核心类同名的类比如 Object类双亲委托机制能保证加载的是 JRE里的那个 Object类而不是你写的 Object类这是因为 AppClassLoader在加载你的 Object 类时会委托给 ExtClassLoader去加载而 ExtClassLoader又会委托给 BootstrapClassLoaderBootstrapClassLoader发现自己已经加载过了 Object类会直接返回不会去加载你写的 Object类我们最多只能 获取到 ExtClassLoader这里注意下

3.2、Tomcat 热加载

Tomcat 本质是通过一个后台线程做周期性的任务定期检测类文件的变化如果有变化就重新加载类我们来看 ContainerBackgroundProcessor具体是如何实现的

protected class ContainerBackgroundProcessor implements Runnable {

    @Override
    public void run() {
        // 请注意这里传入的参数是 " 宿主类 " 的实例
        processChildren(ContainerBase.this);
    }

    protected void processChildren(Container container) {
        try {
            //1. 调用当前容器的 backgroundProcess 方法
            container.backgroundProcess();

            //2. 遍历所有的子容器递归调用 processChildren
            // 这样当前容器的子孙都会被处理
            Container[] children = container.findChildren();
            for (int i = 0; i < children.length; i++) {
            // 这里请你注意容器基类有个变量叫做 backgroundProcessorDelay如果大于 0表明子容器有自己的后台线程无需父容器来调用它的 processChildren 方法
                if (children[i].getBackgroundProcessorDelay() <= 0) {
                    processChildren(children[i]);
                }
            }
        } catch (Throwable t) { ... }

Tomcat 的热加载就是在 Context 容器实现主要是调用了 Context 容器的 reload 方法抛开细节从宏观上看主要完成以下任务:

  • 停止和销毁 Context 容器及其所有子容器子容器其实就是 Wrapper也就是说 Wrapper 里面 Servlet 实例也被销毁了
  • 停止和销毁 Context 容器关联的 Listener 和 Filter
  • 停止和销毁 Context 下的 Pipeline 和各种 Valve
  • 停止和销毁 Context 的类加载器以及类加载器加载的类文件资源
  • 启动 Context 容器在这个过程中会重新创建前面四步被销毁的资源

在这个过程中类加载器发挥着关键作用一个 Context 容器对应一个类加载器类加载器在销毁的过程中会把它加载的所有类也全部销毁Context 容器在启动过程中会创建一个新的类加载器来加载新的类文件

3.3、Tomcat 的类加载器

Tomcat 的自定义类加载器 WebAppClassLoader打破了双亲委托机制它首先自己尝试去加载某个类如果找不到再代理给父类加载器其目的是优先加载 Web 应用自己定义的类具体实现就是重写 ClassLoader的两个方法:findClassloadClass

findClass 方法

org.apache.catalina.loader.WebappClassLoaderBase#findClass;

为了方便理解和阅读我去掉了一些细节:

public Class<?> findClass(String name) throws ClassNotFoundException {
    ...

    Class<?> clazz = null;
    try {
            //1. 先在 Web 应用目录下查找类
            clazz = findClassInternal(name);
    }  catch (RuntimeException e) {
           throw e;
       }

    if (clazz == null) {
    try {
            //2. 如果在本地目录没有找到交给父加载器去查找
            clazz = super.findClass(name);
    }  catch (RuntimeException e) {
           throw e;
       }

    //3. 如果父类也没找到抛出 ClassNotFoundException
    if (clazz == null) {
        throw new ClassNotFoundException(name);
     }

    return clazz;
}

1.先在 Web 应用本地目录下查找要加载的类

2.如果没有找到交给父加载器去查找它的父加载器就是上面提到的系统类加载器 AppClassLoader

3.如何父加载器也没找到这个类抛出 ClassNotFound异常

loadClass 方法

再来看 Tomcat 类加载器的 loadClass方法的实现同样我也去掉了一些细节:

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    synchronized (getClassLoadingLock(name)) {

        Class<?> clazz = null;

        //1. 先在本地 cache 查找该类是否已经加载过
        clazz = findLoadedClass0(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        //2. 从系统类加载器的 cache 中查找是否加载过
        clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve)
                resolveClass(clazz);
            return clazz;
        }

        // 3. 尝试用 ExtClassLoader 类加载器类加载为什么?
        ClassLoader javaseLoader = getJavaseClassLoader();
        try {
            clazz = javaseLoader.loadClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 4. 尝试在本地目录搜索 class 并加载
        try {
            clazz = findClass(name);
            if (clazz != null) {
                if (resolve)
                    resolveClass(clazz);
                return clazz;
            }
        } catch (ClassNotFoundException e) {
            // Ignore
        }

        // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载
            try {
                clazz = Class.forName(name, false, parent);
                if (clazz != null) {
                    if (resolve)
                        resolveClass(clazz);
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }
       }

    //6. 上述过程都加载失败抛出异常
    throw new ClassNotFoundException(name);
}

主要有六个步骤:

1.先在本地 Cache 查找该类是否已经加载过也就是说 Tomcat 的类加载器是否已经加载过这个类

2.如果 Tomcat 类加载器没有加载过这个类再看看系统类加载器是否加载过

3.如果都没有就让ExtClassLoader去加载这一步比较关键目的 防止 Web 应用自己的类覆盖 JRE 的核心类因为 Tomcat 需要打破双亲委托机制假如 Web 应用里自定义了一个叫 Object 的类如果先加载这个 Object 类就会覆盖 JRE 里面的那个 Object 类这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader去加载因为 ExtClassLoader会委托给 BootstrapClassLoader去加载BootstrapClassLoader发现自己已经加载了 Object 类直接返回给 Tomcat 的类加载器这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了也就避免了覆盖 JRE 核心类的问题

4.如果 ExtClassLoader加载器加载失败也就是说 JRE核心类中没有这类那么就在本地 Web 应用目录下查找并加载

5.如果本地目录下没有这个类说明不是 Web 应用自己定义的类那么由系统类加载器去加载这里请你注意Web 应用是通过Class.forName调用交给系统类加载器的因为Class.forName的默认加载器就是系统类加载器

6.如果上述加载过程全部失败抛出 ClassNotFound异常

3.4、Tomcat 类加载器层次

Tomcat 作为 Servlet容器它负责加载我们的 Servlet类此外它还负责加载 Servlet所依赖的 JAR 包并且 Tomcat本身也是也是一个 Java 程序因此它需要加载自己的类和依赖的 JAR 包首先让我们思考这一下这几个问题:

1.假如我们在 Tomcat 中运行了两个 Web 应用程序两个 Web 应用中有同名的 Servlet但是功能不同Tomcat 需要同时加载和管理这两个同名的 Servlet类保证它们不会冲突因此 Web 应用之间的类需要隔离

2.假如两个 Web 应用都依赖同一个第三方的 JAR 包比如 Spring那 Spring的 JAR 包被加载到内存后Tomcat要保证这两个 Web 应用能够共享也就是说 Spring的 JAR 包只被加载一次否则随着依赖的第三方 JAR 包增多JVM的内存会膨胀

3.跟 JVM 一样我们需要隔离 Tomcat 本身的类和 Web 应用的类

1. WebAppClassLoader

Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader 并且给每个 Web 应用创建一个类加载器实例我们知道Context 容器组件对应一个 Web 应用因此每个 Context容器负责创建和维护一个 WebAppClassLoader加载器实例这背后的原理是不同的加载器实例加载的类被认为是不同的类即使它们的类名相同这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间每一个 Web 应用都有自己的类空间Web 应用之间通过各自的类加载器互相隔离

2.SharedClassLoader

本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类在双亲委托机制里各个子加载器都可以通过父加载器去加载类那么把需要共享的类放到父加载器的加载路径下不就行了吗

因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader作为 WebAppClassLoader的父加载器专门来加载 Web 应用之间共享的类如果 WebAppClassLoader自己没有加载到某个类就会委托父加载器 SharedClassLoader去加载这个类SharedClassLoader会在指定目录下加载共享类之后返回给 WebAppClassLoader这样共享的问题就解决了

3. CatalinaClassloader

如何隔离 Tomcat 本身的类和 Web 应用的类?

要共享可以通过父子关系要隔离那就需要兄弟关系了兄弟关系就是指两个类加载器是平行的它们可能拥有同一个父加载器基于此 Tomcat 又设计一个类加载器 CatalinaClassloader专门来加载 Tomcat 自身的类

这样设计有个问题那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢?

老办法还是再增加一个 CommonClassLoader作为 CatalinaClassloaderSharedClassLoader的父加载器CommonClassLoader能加载的类都可以被 CatalinaClassLoaderSharedClassLoader使用

四、整体架构设计解析收获总结

通过前面对 Tomcat 整体架构的学习知道了 Tomcat 有哪些核心组件组件之间的关系以及 Tomcat 是怎么处理一个 HTTP 请求的下面我们通过一张简化的类图来回顾一下从图上你可以看到各种组件的层次关系图中的虚线表示一个请求在 Tomcat 中流转的过程

4.1、连接器

Tomcat 的整体架构包含了两个核心组件连接器和容器连接器负责对外交流容器负责内部处理连接器用 ProtocolHandler接口来封装通信协议和 I/O模型的差异ProtocolHandler内部又分为 EndPointProcessor模块EndPoint负责底层 Socket通信Proccesor负责应用层协议解析连接器通过适配器 Adapter调用容器

对 Tomcat 整体架构的学习我们可以得到一些设计复杂系统的基本思路首先要分析需求根据高内聚低耦合的原则确定子模块然后找出子模块中的变化点和不变点用接口和抽象基类去封装不变点在抽象基类中定义模板方法让子类自行实现抽象方法也就是具体子类去实现变化点

4.2、容器

运用了组合模式 管理容器、通过 观察者模式 发布启动事件达到解耦、开闭原则骨架抽象类和模板方法抽象变与不变变化的交给子类实现从而实现代码复用以及灵活的拓展使用责任链的方式处理请求比如记录日志等

4.3、类加载器

Tomcat 的自定义类加载器 WebAppClassLoader为了隔离 Web 应用打破了双亲委托机制它首先自己尝试去加载某个类如果找不到再代理给父类加载器其目的是优先加载 Web 应用自己定义的类防止 Web 应用自己的类覆盖 JRE 的核心类使用 ExtClassLoader 去加载这样即打破了双亲委派又能安全加载

五、实际场景运用

简单的分析了 Tomcat 整体架构设计从 【连接器】 到 【容器】并且分别细说了一些组件的设计思想以及设计模式接下来就是如何学以致用借鉴优雅的设计运用到实际工作开发中学习从模仿开始

5.1、责任链模式

在工作中有这么一个需求用户可以输入一些信息并可以选择查验该企业的 【工商信息】、【司法信息】、【中登情况】等如下如所示的一个或者多个模块而且模块之间还有一些公共的东西是要各个模块复用

这里就像一个请求会被多个模块去处理所以每个查询模块我们可以抽象为 处理阀门使用一个 List 将这些 阀门保存起来这样新增模块我们只需要新增一个阀门即可实现了开闭原则同时将一堆查验的代码解耦到不同的具体阀门中使用抽象类提取 “不变的”功能

具体示例代码如下所示:

首先抽象我们的处理阀门 NetCheckDTO是请求信息

/**
 * 责任链模式:处理每个模块阀门
 */
public interface Valve {
    /**
     * 调用
     * @param netCheckDTO
     */
    void invoke(NetCheckDTO netCheckDTO);
}

定义抽象基类复用代码

public abstract class AbstractCheckValve implements Valve {
    public final AnalysisReportLogDO getLatestHistoryData(NetCheckDTO netCheckDTO, NetCheckDataTypeEnum checkDataTypeEnum){
        // 获取历史记录省略代码逻辑
    }

    // 获取查验数据源配置
    public final String getModuleSource(String querySource, ModuleEnum moduleEnum){
       // 省略代码逻辑
    }
}

定义具体每个模块处理的业务逻辑比如 【百度负面新闻】对应的处理

@Slf4j
@Service
public class BaiduNegativeValve extends AbstractCheckValve {
    @Override
    public void invoke(NetCheckDTO netCheckDTO) {

    }
}

最后就是管理用户选择要查验的模块我们通过 List 保存用于触发所需要的查验模块

@Slf4j
@Service
public class NetCheckService {
    // 注入所有的阀门
    @Autowired
    private Map<String, Valve> valveMap;

    /**
     * 发送查验请求
     *
     * @param netCheckDTO
     */
    @Async("asyncExecutor")
    public void sendCheckRequest(NetCheckDTO netCheckDTO) {
        // 用于保存客户选择处理的模块阀门
        List<Valve> valves = new ArrayList<>();

        CheckModuleConfigDTO checkModuleConfig = netCheckDTO.getCheckModuleConfig();
        // 将用户选择查验的模块添加到 阀门链条中
        if (checkModuleConfig.getBaiduNegative()) {
            valves.add(valveMap.get("baiduNegativeValve"));
        }
        // 省略部分代码.......
        if (CollectionUtils.isEmpty(valves)) {
            log.info("网查查验模块为空没有需要查验的任务");
            return;
        }
        // 触发处理
        valves.forEach(valve -> valve.invoke(netCheckDTO));
    }
}

5.2、模板方法模式

需求是这样的可根据客户录入的财报 excel 数据或者企业名称执行财报分析

对于非上市的则解析 excel -> 校验数据是否合法->执行计算

上市企业:判断名称是否存在 不存在则发送邮件并中止计算-> 从数据库拉取财报数据初始化查验日志、生成一条报告记录触发计算-> 根据失败与成功修改任务状态 

重要的 ”变“ 与 ”不变“

  • 不变的是整个流程是初始化查验日志、初始化一条报告、前期校验数据(若是上市公司校验不通过还需要构建邮件数据并发送)、从不同来源拉取财报数据并且适配通用数据、然后触发计算任务异常与成功都需要修改状态
  • 变化的是上市与非上市校验规则不一样获取财报数据方式不一样两种方式的财报数据需要适配

整个算法流程是固定的模板但是需要将算法内部变化的部分具体实现延迟到不同子类实现这正是模板方法模式的最佳场景

public abstract class AbstractAnalysisTemplate {
    /**
     * 提交财报分析模板方法定义骨架流程
     * @param reportAnalysisRequest
     * @return
     */
    public final FinancialAnalysisResultDTO doProcess(FinancialReportAnalysisRequest reportAnalysisRequest) {
        FinancialAnalysisResultDTO analysisDTO = new FinancialAnalysisResultDTO();
		// 抽象方法:提交查验的合法校验
        boolean prepareValidate = prepareValidate(reportAnalysisRequest, analysisDTO);
        log.info("prepareValidate 校验结果 = {} ", prepareValidate);
        if (!prepareValidate) {
			// 抽象方法:构建通知邮件所需要的数据
            buildEmailData(analysisDTO);
            log.info("构建邮件信息data = {}", JSON.toJSONString(analysisDTO));
            return analysisDTO;
        }
        String reportNo = FINANCIAL_REPORT_NO_PREFIX + reportAnalysisRequest.getUserId() + SerialNumGenerator.getFixLenthSerialNumber();
        // 生成分析日志
        initFinancialAnalysisLog(reportAnalysisRequest, reportNo);
		// 生成分析记录
        initAnalysisReport(reportAnalysisRequest, reportNo);

        try {
            // 抽象方法:拉取财报数据不同子类实现
            FinancialDataDTO financialData = pullFinancialData(reportAnalysisRequest);
            log.info("拉取财报数据完成, 准备执行计算");
            // 测算指标
            financialCalcContext.calc(reportAnalysisRequest, financialData, reportNo);
			// 设置分析日志为成功
            successCalc(reportNo);
        } catch (Exception e) {
            log.error("财报计算子任务出现异常", e);
			// 设置分析日志失败
            failCalc(reportNo);
            throw e;
        }
        return analysisDTO;
    }
}

最后新建两个子类继承该模板并实现抽象方法这样就将上市与非上市两种类型的处理逻辑解耦同时又复用了代码

5.3、策略模式

需求是这样要做一个万能识别银行流水的 excel 接口假设标准流水包含【交易时间、收入、支出、交易余额、付款人账号、付款人名字、收款人名称、收款人账号】等字段现在我们解析出来每个必要字段所在 excel 表头的下标但是流水有多种情况:

1.一种就是包含所有标准字段

2.收入、支出下标是同一列通过正负来区分收入与支出

3.收入与支出是同一列有一个交易类型的字段来区分

4.特殊银行的特殊处理

也就是我们要根据解析对应的下标找到对应的处理逻辑算法我们可能在一个方法里面写超多 if else 的代码整个流水处理都偶合在一起假如未来再来一种新的流水类型还要继续改老代码最后可能出现 “又臭又长难以维护” 的代码复杂度

这个时候我们可以用到策略模式将不同模板的流水使用不同的处理器处理根据模板找到对应的策略算法去处理即使未来再加一种类型我们只要新加一种处理器即可高内聚低耦合且可拓展

定义处理器接口不同处理器去实现处理逻辑将所有的处理器注入到 BankFlowDataHandlerdata_processor_map中根据不同的场景取出对已经的处理器处理流水

public interface DataProcessor {
    /**
     * 处理流水数据
     * @param bankFlowTemplateDO 流水下标数据
     * @param row
     * @return
     */
    BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO, List<String> row);

    /**
     * 是否支持处理该模板,不同类型的流水策略根据模板数据判断是否支持解析
     * @return
     */
    boolean isSupport(BankFlowTemplateDO bankFlowTemplateDO);
}

// 处理器的上下文
@Service
@Slf4j
public class BankFlowDataContext {
    // 将所有处理器注入到 map 中
    @Autowired
    private List<DataProcessor> processors;

    // 找对对应的处理器处理流水
    public void process() {
         DataProcessor processor = getProcessor(bankFlowTemplateDO);
      	 for(DataProcessor processor : processors) {
           if (processor.isSupport(bankFlowTemplateDO)) {
             // row 就是一行流水数据
        		 processor.doProcess(bankFlowTemplateDO, row);
             break;
           }
         }

    }


}

定义默认处理器处理正常模板新增模板只要新增处理器实现 DataProcessor即可

/**
 * 默认处理器:正对规范流水模板
 *
 */
@Component("defaultDataProcessor")
@Slf4j
public class DefaultDataProcessor implements DataProcessor {

    @Override
    public BankTransactionFlowDO doProcess(BankFlowTemplateDO bankFlowTemplateDO) {
        // 省略处理逻辑细节
        return bankTransactionFlowDO;
    }

    @Override
    public String strategy(BankFlowTemplateDO bankFlowTemplateDO) {
      // 省略判断是否支持解析该流水
      boolean isDefault = true;

      return isDefault;
    }
}

通过策略模式我们将不同处理逻辑分配到不同的处理类中这样完全解耦便于拓展

使用内嵌 Tomcat 方式调试源代码:GitHub: https://github.com/UniqueDong/tomcat-embedded


相关文章

猜您喜欢

  • OpenCV轮廓外接多边形 OpenCV实现轮廓外接多边形

    想了解OpenCV实现轮廓外接多边形的相关内容吗我有一個夢想在本文为您仔细讲解OpenCV轮廓外接多边形的相关知识和一些Code实例欢迎阅读和指正我们先划重点:OpenCV轮廓外接多边形,OpenCV轮廓外接,OpenCV外接多边形下面大家一起来学习吧..
  • python 图片处理 python 对图片进行简单的处理

    想了解python 对图片进行简单的处理的相关内容吗临时营地在本文为您仔细讲解python 图片处理的相关知识和一些Code实例欢迎阅读和指正我们先划重点:python,图片处理下面大家一起来学习吧..

网友评论

Copyright 2022 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们