先总后分。养成学习好习惯。全文围绕此图展开。
从图上我们可以看到各种组件的层次关系,
图中的虚线表示一个请求在 Tomcat 中流转的过程。

再来个鱼骨图:

Tomcat系统架构(上): 连接器是如何设计的?

在面试时我们可能经常被问到:你做的 XX 项目的架构是如何设计的,请讲一下实现的思
路。对于面试官来说,可以通过你对复杂系统设计的理解,了解你的技术水平以及处理复杂问题的思路。

需求分析:

Tomcat需要实现的核心功能

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

架构设计:

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

tomcat设计了两个核心组件

tomcat 连接器(Connector)

连接器(Connector):连接器负责对外交流

Tomcat container 内部容器

容器(Container):容器负责内部处理

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 性能。

Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比
一个房间有多个门。
但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。
这里请你注意,Service 本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。
Tomcat 内可能有多个 Service,这样的设计也是出于灵活性的考虑。通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

从图上你可以看到,最顶层是 Server,这里的 Server 指的就是一个 Tomcat 实例。一个
Server 中有一个或者多个 Service,一个 Service 中有多个连接器和一个容器。连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通信。

容器结构图 xml结构关系表示图

<Server><Service><Connector></Connector><Engine><Host><Context></Context></Host></Engine></Service>
</Server>

Tomcat连接器(Connector):连接器负责对外交流

连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器
中获取到的都是一个标准的 ServletRequest 对象。

我们可以把连接器的功能需求进一步细化,比如:
1 监听网络端口。
2 接受网络连接请求。
3 读取请求网络字节流。
4 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
将 Tomcat Request 对象转成标准的 ServletRequest。调用 Servlet 容器,得到 ServletResponse。将 ServletResponse 转成 Tomcat Response 对象。

需求列清楚后,我们要考虑的下一个问题是,连接器应该有哪些子模块?
优秀的模块化设计应该考虑高内聚、低耦合。

高内聚是指相关度比较高的功能要尽可能集中,不要分散。 低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块
产生强依赖。

通过分析连接器的详细功能列表,我们发现
连接器需要完成 3 个高内聚的功能:
1 网络通信。 EndPoint 端点
2 应用层协议解析。 Processor 解析器
3 Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化 . Adapter 适配器

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

组件之间通过抽象接口交互。这样做还有一个好处是封装变化。这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。

网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。

但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容
器。

如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。

由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:
Http11NioProtocol 和 AjpNioProtocol。


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

两个问题?

第一,如何debug源码呢?
第二,tomcat和netty有什么区别呢?为什么netty常常用做底层通讯模块,而tomcat作为
web容器呢?
回复: 1)软件系统本质是对信息的处理,要跟踪信息在流动过程中的经过的关键环节,并在这些地方下断点,看看变量的值是什么。比如你可以在业务代码中下个断点,看看调用栈,看Tomcat和Spring是怎么调到你的代码的,然后在这个调用栈中的关键函数里上下都看看,先熟悉个大概,然后带着问题去深入调试。
2)你可以把Netty理解成Tomcat中的连接器,它们都负责网络通信,都利用了Java NIO非阻塞特性。但Netty素以高性能高并发著称,为什么Tomcat不把连接器替换成Netty呢?第一个原因是Tomcat的连接器性能已经足够好了,同样是Java NIO编程,套路都差不多。第二个原因是Tomcat做为Web容器,需要考虑到Servlet规范,Servlet规范规定了对HTTP Body的读写是阻塞的,因此即使用到了Netty,也不能充分发挥它的优势。所以Netty一般用在非HTTP协议和Servlet的场景下。

问题:源码如何阅读效果好啊?现在源码一大堆,不知从何下手。
回复: 抓主线,抓主干,每个系统中都有一个关键的核心类,紧紧抓住这些类,先不要分散,在逐步看旁枝,等你学习弄明白一个经典的系统,很多套路你就明白了。

Tomcat系统架构(下):聊聊多层容器的设计


容器,顾名思义就是用来装载东西的器具,在 Tomcat 里,容器就是用来装载 Servlet
的。

那 Tomcat 的 Servlet 容器是如何设计的呢?

容器的层次结构

Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不
是平行关系,而是父子关系。下面我画了一张图帮你理解它们的关系。

你可能会问,为什么要设计成这么多层次的容器,这不是增加了复杂度吗?其实这背后的考虑是,Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。

Context 表示一个 Web 应用程序;
Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;
Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;
Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。

组合模式是啥 TODO

你可以再通过 Tomcat 的 server.xml 配置文件来加深对 Tomcat 容器的理解。Tomcat 采
用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照
一定的格式要求配置在这个顶层容器中。
那么,Tomcat 是怎么管理这些容器的呢?你会发现这些容器具有父子关系,形成一个树形
结构,你可能马上就想到了设计模式中的组合模式。
没错,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 接口,LifeCycle
接口用来统一管理各组件的生命周期,

问题:请求定位 Servlet 的过程

你可能好奇,设计了这么多层次的容器,Tomcat 是怎么确定请求是由哪个 Wrapper 容器
里的 Servlet 来处理的呢?
答案是,Tomcat 是用 Mapper 组件来完成这个任务的。

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

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

读到这里你可能感到有些抽象,接下来我通过一个例子来解释这个定位的过程。
假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:manage.shopping.com和user.shopping.com,网站管理人员通过
manage.shopping.com域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是
两个单独的 Web 应用。终端客户通过user.shopping.com域名去搜索商品和下订单,
搜索功能和订单管理也是两个独立的 Web 应用。

针对这样的部署,Tomcat 会创建一个 Service 组件(端口)和一个 Engine 容器组件,在 Engine容器下创建两个 Host 子容器,在每个 Host 容器下创建两个 Context 子容器。由于一个Web 应用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创建多个 Wrapper
子容器。每个容器都有对应的访问路径,你可以通过下面这张图来帮助你理解。
假如有用户访问一个 URL,比如图中的http://user.shopping.com:8080/order/buy,
Tomcat 如何将这个 URL 定位到一个 Servlet 呢?

首先,根据协议和端口号选定 Service 和 Engine。
我们知道 Tomcat 的每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监
听 8080 端口、默认的 AJP 连接器监听 8009 端口。上面例子中的 URL 访问的是 8080 端
口,因此这个请求会被 HTTP 连接器接收,而一个连接器是属于一个 Service 组件的,这
样 Service 组件就确定了。我们还知道一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因此 Service 确定了也就意味着 Engine 也确定了。
然后,根据域名选定 Host
Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比
如例子中的 URL 访问的域名是user.shopping.com,因此 Mapper 会找到 Host2 这个容器。
之后,根据 URL 路径找到 Context 组件。
Host 确定以后,Mapper 根据 URL 的路径来匹配相应的 Web 应用的路径,比如例子中访
问的是 /order,因此找到了 Context4 这个 Context 容器。
最后,根据 URL 路径找到 Wrapper(Servlet)。
Context 确定后,Mapper 再根据 web.xml 中配置的 Servlet 映射路径来找到具体的
Wrapper 和 Servlet。

看到这里,我想你应该已经了解了什么是容器,以及 Tomcat 如何通过一层一层的父子容
器找到某个 Servlet 来处理请求。需要注意的是,并不是说只有 Servlet 才会去处理请求,
实际上这个查找路径上的父子容器都会对请求做一些处理。

我在上一期说过,连接器中的Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。
Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依
次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

责任链模式是啥

Valve 表示一个处理点,比如权限认证和记录日志。如果你还不太理解的话,可以来看看
Valve 和 Pipeline 接口中的关键方法。

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

由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有
getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。请你继
续看 Pipeline 接口。

public interface Pipeline extends Contained {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。
我还是通过一张图来解释。

整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve:

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

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

你可能会问,前面我们不是讲到了 Filter,似乎也有相似的功能,那 Valve 和 Filter 有什么
区别吗?它们的区别是:
Valve 是 Tomcat 的私有机制,与 Tomcat 的基础架构 /API 是紧耦合的。Servlet API
是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。

另一个重要的区别是 Valve 工作在 Web 容器级别,拦截所有应用的请求;而 Servlet
Filter 工作在应用级别,只能拦截某个 Web 应用的所有请求。如果想做整个 Web 容器
的拦截器,必须通过 Valve 来实现。

精华

今天我们学习了 Tomcat 容器的层次结构、根据请求定位 Servlet 的过程,以及请求在容
器中的调用过程。Tomcat 设计了多层容器是为了灵活性的考虑,灵活性具体体现在一个
Tomcat 实例(Server)可以有多个 Service,每个 Service 通过多个连接器监听不同的端
口,而一个 Service 又可以支持多个虚拟主机。一个 URL 网址可以用不同的主机名、不同
的端口和不同的路径来访问特定的 Servlet 实例。
请求的链式调用是基于 Pipeline-Valve 责任链来完成的,这样的设计使得系统具有良好的
可扩展性,如果需要扩展容器本身的功能,只需要增加相应的 Valve 即可。

思考

Tomcat 内的 Context 组件跟 Servlet 规范中的 ServletContext 接口有什么区别?
跟Spring 中的 ApplicationContext 又有什么关系?

1)Servlet规范中ServletContext表示web应用的上下文环境,而web应用对应tomcat的
概念是Context,所以从设计上,ServletContext自然会成为tomcat的Context具体实现
的一个成员变量.

问题: 请问到业务的controller是从哪部分进去的呢 ?
回复: Wrapper -> Filter -> DispatcherServlet -> Controller

问题:Basic value 有些疑惑。比如engine容器下有多个host容器,那engine容器的basic value是怎么知道要指向哪个host容器的value呢?
回复: 好问题,Mapper组件在映射请求的时候,会在Request对象中存储相应的Host、
Context等对象,这些选定的容器用来处理这个特定的请求,因此Engine中的Valve是从Request对象拿到Host容器的。

问题:能提供一份tomcat多host的配置吗?
回复:

问题: 我之前一直误以为一个 server表示我们一个应用(实际上只是代表一个tomcat实
例),所以一直不理解为什么 server 下通过 host 可以配置多个应用 ,学了这节,发现是一
个context对应一个应用, 自己百度了一下,原来可以通过 host 或者 service 来让
tomcat 访问不同的目录来访问多个应用 。
回复: 1,你说的对,在同一个Tomcat实例里部署多个Web应用是为了节省内存等资源,不过配置部署有点复杂,应用之间互相影响,加上现在硬件成本将低,多应用部署比较少见了。
2,Servlet接口中定义了service方法,没有doGet/doPost。HttpServlet是一个实现类,实现了service方法,同时留出了doGet/doPost方法让程序员来实现。
你可以通过web.xml配置一个或多个Filter,Servlet容器在调用Servlet的service之前,需要调用这些Filter,于是把这些Filter创建出来,形成链表,依次调用,这个Filter链中的最后一个Filter会负责调用Servlet的service方法。

几个问题
1.Wrapper容器里有且只有一个Servlet,Context里可以有多个Servlet即可以有多个
Wrapper,这样理解对吗 ?
2.PepeLine负责维护链式Vavle,具体Vavle负责处理具体请求?
回复:
1, 对的
2,Vavle可以理解对请求进行“拦截”和“修正”,类似Filter,但是Valve用来扩展容器本身的
功能的,真正处理请求并产生响应的是Servlet。

几个问题:最外层是server,一个server对应一个Tomcat实例,server下面可以由很多service组件,service组件是区分协议和端口号(问题1:域名访问的话请求到达这里是已经被解析成port了吗?如果已经解析了后面为啥还能拿到二级域名)。每个service对应多个连接器和一个engine。(问题2:为啥要设置多个连接器对应一个engine,是为了提高连接器接受请求的并发吗?)
回复: 1,DNS解析是在客户端完成的,你需要在客户端把两个域名指向同一个IP,可以通过hosts配置,因此只要你使用的端口相同,两个域名其实访问的是同一个Service组件。而Tomcat设计通过请求URL中Host来转发到不同Host组件的。
2. 不同的连接器代表一种通信的路径,比如同时支持HTTP和HTTPS,不是为了并发。

容器结构图 xml结构关系表示图

<Server><Service><Connector></Connector><Engine><Host><Context></Context></Host></Engine></Service>
</Server>

Tomcat如何实现一键式启停?

一图胜千言,回顾上面的学习。总结如下图。

上面这张图描述了组件之间的静态关系,如果想让一个系统能够对外提供服务,我们需要创建、组装并启动这些组件;在服务停止的时候,我们还需要释放资源,销毁这些组件,因此这是一个动态的过程。也就是说,Tomcat 需要动态地管理这些组件的生命周期。

在我们实际的工作中,如果你需要设计一个比较大的系统或者框架时,你同样也需要考虑这几个问题:

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

今天我们就来解决上面的问题,在这之前,先来看看组件之间的关系。如果你仔细分析过这些组件,可以发现它们具有两层关系。

第一层关系是组件有大有小,大组件管理小组件,比如 Server 管理 Service,Service 又
管理连接器和容器。
第二层关系是组件有外有内,外层组件控制内层组件,比如连接器是外层组件,负责对外
交流,外层组件调用内层组件完成业务功能。也就是说,请求的处理过程是由外层组件来驱动的

这两层关系决定了系统在创建组件时应该遵循一定的顺序。

第一个原则是先创建子组件,再创建父组件,子组件需要被“注入”到父组件中。
第二个原则是先创建内层组件,再创建外层组件,内层组建需要被“注入”到外层组件。

创建组件时潜在的危险

因此,最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。不知道你注意到没有,这个思路其实很有问题!因为这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。

创建组件时解决方法

为了解决这个问题,我们希望找到一种通用的、统一的方法来管理组件的生命周期,就像汽车“一键启动”那样的效果。

一键式启停:LifeCycle 接口

我在前面说到过,设计就是要找到系统的变化点和不变点。这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。而变化点是每个具体组件的初始化方法,也就是启动方法是不一样的。因此,我们把不变点抽象出来成为一个接口,这个接口跟生命周期有关,叫作 LifeCycle。LifeCycle 接口里应该定义这么几个方法:init()、start()、stop() 和 destroy(),每个具体的组件去实现这些方法。

理所当然,在父组件的 init() 方法里需要创建子组件并调用子组件的 init() 方法。同样,在
父组件的 start() 方法里也需要调用子组件的 start() 方法,因此调用者可以无差别的调用各
组件的 init() 方法和 start() 方法,这就是组合模式的使用,并且只要调用最顶层组件,也
就是 Server 组件的 init() 和 start() 方法,整个 Tomcat 就被启动起来了。下面是
LifeCycle 接口的定义。

可扩展性:LifeCycle 事件

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

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

具体来说就是在 LifeCycle 接口里加入两个方法:添加监听器和删除监听器。除此之外,我们还需要定义一个 Enum 来表示组件有哪些状态,以及处在什么状态会触发什么样的事件。因此 LifeCycle 接口和 LifeCycleState 就定义成了下面这样。

从图上你可以看到,组件的生命周期有 NEW、INITIALIZING、INITIALIZED、
STARTING_PREP、STARTING、STARTED 等,而一旦组件到达相应的状态就触发相应的事件,比如 NEW 状态表示组件刚刚被实例化;而当 init() 方法被调用时,状态就变成
INITIALIZING 状态,这个时候,就会触发 BEFORE_INIT_EVENT 事件,如果有监听器在监听这个事件,它的方法就会被调用。

重用性:LifeCycleBase 抽象基类

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

而基类中往往会定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。

回到 LifeCycle 接口,Tomcat 定义一个基类 LifeCycleBase 来实现 LifeCycle 接口,把一
些公共的逻辑放到基类中去,比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等,而子类就负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名,我们把具体子类的实现方法改个名字,在后面加上 Internal,叫 initInternal()、startInternal() 等。我们再来看引入了基类 LifeCycleBase 后的类图:

从图上可以看到,LifeCycleBase 实现了 LifeCycle 接口中所有的方法,还定义了相应的抽
象方法交给具体子类去实现,这是典型的模板设计模式。

下面是 LifeCycleBase 的 init() 方法实现。

@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) {...}
}

总之,LifeCycleBase 调用了抽象方法来实现骨架逻辑。讲到这里, 你可能好奇,
LifeCycleBase 负责触发事件,并调用监听器的方法,那是什么时候、谁把监听器注册进来的呢?
分为两种情况

1 Tomcat 自定义了一些监听器,这些监听器是父组件在创建子组件的过程中注册到子组件 的。比如MemoryLeakTrackingListener 监听器,用来检测 Context 容器中的内存泄 漏,这个监听器是 Host 容器在创建 Context 容器时注册到 Context 中的。
2 我们还可以在 server.xml 中定义自己的监听器,Tomcat在启动时会解析 server.xml, 创建监听器并注册到容器组件

Tomcat 组件的生命周期管理总体类图
通过上面的学习,我相信你对 Tomcat 组件的生命周期的管理有了深入的理解,我们再来
看一张总体类图继续加深印象。

值得我们特别学习的是

ContainerBase 实现了 Container 接口,也继承了 LifeCycleBase 类,它们的生命周期管
理接口和功能接口是分开的,这也符合设计中接口分离的原则。

课后思考

从文中最后的类图上你会看到所有的容器组件都扩展了 ContainerBase,跟 LifeCycleBase
一样,ContainerBase 也是一个骨架抽象类,请你思考一下,各容器组件有哪些“共同的
逻辑”需要 ContainerBase 由来实现呢?

ContainerBase提供了针对Container接口的通用实现,所以最重要的职责包含两个:

  1. 维护容器通用的状态数据
  2. 提供管理状态数据的通用方法

思考题
1,容器的创建/初始化/销毁
2,容器添加/删除子容器
3,如果还要监听容器状态变化的话还需要有添加/移除事件的方法。

Tomcat的“高层们”都负责做什么?

启动 Tomcat的流程图:

1.Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本会启动一个 JVM 来运行
Tomcat 的启动类 Bootstrap。
2.Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。关于 Tomcat
为什么需要自己的类加载器?
3.Catalina 是一个启动类,它通过解析 server.xml、创建相应的组件,并调用 Server 的
start 方法。
4.Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start 方法。
5.Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine
的 start 方法。

另一方面,软件系统中往往都有一些起管理作用的组件,你可以学习和借鉴 Tomcat是如何实现这些组件的。

Catalina

Catalina 的主要任务就是创建 Server,它不是直接 new 一个 Server 实例就完事了,而是
需要解析 server.xml,把在 server.xml 里配置的各种组件一一创建出来,接着调用 Server
组件的 init 方法和 start 方法,这样整个 Tomcat 就启动起来了。作为“管理者”,
Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,
Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩
子”。

public void start() {//1. 如果持有的 Server 实例为空,就解析 server.xml 创建出来if (getServer() == null) {load();}//2. 如果创建失败,报错退出if (getServer() == null) {log.fatal(sm.getString("catalina.noServer"));return;}//3. 启动 Servertry {getServer().start();} catch (LifecycleException e) {return;}// 创建并注册关闭钩子if (useShutdownHook) {if (shutdownHook == null) {shutdownHook = new CatalinaShutdownHook();}Runtime.getRuntime().addShutdownHook(shutdownHook);}// 用 await 方法监听停止请求if (await) {await();stop();}
}

本期精华
今天我们学习了 Tomcat 启动过程,具体是由启动类和“高层”组件来完成的,它们都承
担着“管理”的角色,负责将子组件创建出来,并把它们拼装在一起,同时也掌握子组件
的“生杀大权”。

所以当我们在设计这样的组件时,需要考虑两个方面:

首先要选用合适的数据结构来保存子组件,比如 Server 用数组来保存 Service 组件,并且
采取动态扩容的方式,这是因为数组结构简单,占用内存小;再比如 ContainerBase 用
HashMap 来保存子容器,虽然 Map 占用内存会多一点,但是可以通过 Map 来快速的查
找子容器。因此在实际的工作中,我们也需要根据具体的场景和需求来选用合适的数据结
构。

其次还需要根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。这正是“管理者”应该考虑的事情。

课后思考
Server 组件的在启动连接器和容器时,都分别加了锁,这是为什么呢?

问题:tomcat一般生产环境线程数大小建议怎么设置呢???
回复:

理论上:

线程数=((线程阻塞时间 + 线程忙绿时间) / 线程忙碌时间) * cpu核数

如果线程始终不阻塞,一直忙碌,会一直占用一个CPU核,因此可以直接设置 线程数=CPU核数。

但是现实中线程可能会被阻塞,比如等待IO。因此根据上面的公式确定线程数。
那怎么确定线程的忙碌时间和阻塞时间?要经过压测,在代码中埋点统计。

Tomcat学习 之一 Tomcat优化思路 官方文档 通读一遍才是正道
Tomcat优化思路 线程池内取一个 直到达到临界点(最大连接数) 请求队列(记事本)* 记下来那些来不及处理的请求 请求队列满了 就达到瓶颈了*

Tomcat 使用Java 编写 内存 CPU 在不改变业务代码的思路 :主要有下面三种思路

  • 第一点 优化思路,增大线程池数量和增大请求队列大小
  • 第二点 优化思路 减少业务线程的执行时间(内存 CPU 加大)
  • 第三点优化思路,改变Tomcat的线程模型的优化

(干货!最全)Tomcat入门相关推荐

  1. 全干货之Python3快速入门(五)——Python3函数

    全干货之Python3快速入门(五)--Python3函数 什么是函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段 函数能提高应用的模块性,和代码的重复利用率. 在Python中 ...

  2. 干货!区块链入门、进阶、行业专家观点!1000篇好文帮你破解区块链密码!(中篇)...

    随着区块链概念理论的不断成熟以及强劲技术的不断深耕,区块链已经成为投资圈中备受关注的热点,从区块链1.0时代落地数字货币比特币.莱特币等,打开了区块链通向新弯道的高速路口,到区块链2.0时代开始通过智 ...

  3. python速成要多久2019-8-28_2019最全Python入门学习路线,不是我吹,绝对是最全

    近几年Python的受欢迎程度可谓是扶摇直上,当然了学习的人也是愈来愈多.一些学习Python的小白在学习初期,总希望能够得到一份Python学习路线图,小编经过多方汇总为大家汇总了一份Python学 ...

  4. 自学python推荐书籍2019-2019最全Python入门学习路线,不是我吹,绝对是最全

    近几年Python的受欢迎程度可谓是扶摇直上,当然了学习的人也是愈来愈多.一些学习Python的小白在学习初期,总希望能够得到一份Python学习路线图,小编经过多方汇总为大家汇总了一份Python学 ...

  5. 全栈入门_启动数据栈入门包(2020)

    全栈入门 I advise a lot of people on how to build out their data stack, from tiny startups to enterprise ...

  6. (干货!)Tomcat性能优化

    (干货!)Tomcat性能优化 tomcat启动参数 共享这部分内存 jps tail -f log/catalina.out jmap ps -ef|grep tomcat jmap -heap 3 ...

  7. 软硬结合——写给硬件开发工程师的全栈入门实战

    特别注意: 教程已经迁移到:www.scaugreen.cn 软硬结合--写给硬件开发工程师的全栈入门实战 作者水平 读者水平要求 整个项目简单介绍 教程特点 为什么使用nodejs 开始 提问 提问 ...

  8. 手把手教你写web全栈入门项目—React+Koa+MongoDB(3w字教程,真的很详细,有代码)

    手把手教你写web全栈入门项目-React+Koa+MongoDB

  9. 重磅盘点!过去一年里最受欢迎的技术干货,全在这里了

    导读:过去的一年,大数据DT汇聚了50万+志同道合的小伙伴,推送了1000多篇文章,我们还被一些铁杆粉丝称为「宝藏号」. 数据叔由衷地感谢大家的支持!鼠年第一天,数据叔把过去一年的原创文章做了一个梳理 ...

最新文章

  1. 网站故障排查常用命令
  2. ASP入门(十三)-Server对象
  3. 深入理解XGBoost:分布式实现
  4. c语言程序设计第四次作业——顺序结构
  5. 计算机刚开机时执行的是,电脑刚开机就提示自动关机该怎么办
  6. Python全栈开发之Day02
  7. [转载] 使用Python防止SQL注入攻击
  8. PCA(主成分分析)的简单理解
  9. 归并排序(链表结构)
  10. 微信小程序云开发教程-微信小程序的JS基础-事件响应与视图层数据获取
  11. CE教程第八步之多级指针
  12. VSCODE常用快捷键
  13. 报错invalid operands to binary expression (const char * and const char[9])
  14. sed替换字符串的贪婪匹配和最小匹配
  15. 红警代码开源了 来瞅瞅源码 文内送Win10可联机的红警2标准版游戏
  16. 英特尔、高通等多家科技巨头禁止员工与华为交流!
  17. 企业微信服务商扫码登录
  18. 38个优秀博客站点推荐
  19. 塞尔维亚国家队大名单:马蒂奇领衔,古德利落选
  20. 从FC到Wii 24年来游戏机CPU发展历程回顾

热门文章

  1. Kafka万亿级消息实战解决方案干货
  2. Linux 多网卡bond
  3. Java8新特性总结 - 1.接口新增默认方法和静态方法
  4. Nodejs学习笔记(六)——Mysql模块
  5. C++ 字符串流stringstream(附蓝桥杯2018年第九届真题缩位求和题解)
  6. mysql数据库druid密码加密_Druid数据库密码加密
  7. OSChina 周日乱弹 —— 七哥的北漂日记
  8. 日志,错误日志,成功日志,日志是个好东西。
  9. 私募公司完成对戴尔软件的收购 SonicWall与Quest将分别独立
  10. 面向业务的立体化高可用架构设计