This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
<audio id="audio" title="05 | Tomcat系统架构 连接器是如何设计的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d1/bf/d1e496ba509e03ccb784ec2b940929bf.mp3"></audio>
在面试时我们可能经常被问到你做的XX项目的架构是如何设计的请讲一下实现的思路。对于面试官来说可以通过你对复杂系统设计的理解了解你的技术水平以及处理复杂问题的思路。
今天咱们就来一步一步分析Tomcat的设计思路看看Tomcat的设计者们当时是怎么回答这个问题的。一方面我们可以学到Tomcat的总体架构学会从宏观上怎么去设计一个复杂系统怎么设计顶层模块以及模块之间的关系另一方面也为我们深入学习Tomcat的工作原理打下基础。
## Tomcat总体架构
我们知道如果要设计一个系统首先是要了解需求。通过专栏前面的文章我们已经了解了Tomcat要实现2个核心功能
<li>
处理Socket连接负责网络字节流与Request和Response对象的转化。
</li>
<li>
加载和管理Servlet以及具体处理Request请求。
</li>
**因此Tomcat设计了两个核心组件连接器Connector和容器Container来分别做这两件事情。连接器负责对外交流容器负责内部处理。**
所以连接器和容器可以说是Tomcat架构里最重要的两部分需要你花些精力理解清楚。这两部分内容我会分成两期今天我来分析连接器是如何设计的下一期我会介绍容器的设计。
在开始讲连接器前我先铺垫一下Tomcat支持的多种I/O模型和应用层协议。
Tomcat支持的I/O模型有
<li>
NIO非阻塞I/O采用Java NIO类库实现。
</li>
<li>
NIO.2异步I/O采用JDK 7最新的NIO.2类库实现。
</li>
<li>
APR采用Apache可移植运行库实现是C/C++编写的本地库。
</li>
Tomcat支持的应用层协议有
<li>
HTTP/1.1这是大部分Web应用采用的访问协议。
</li>
<li>
AJP用于和Web服务器集成如Apache
</li>
<li>
HTTP/2HTTP 2.0大幅度的提升了Web性能。
</li>
Tomcat为了实现支持多种I/O模型和应用层协议一个容器可能对接多个连接器就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务需要把它们组装起来才能工作组装后这个整体叫作Service组件。这里请你注意Service本身没有做什么重要的事情只是在连接器和容器外面多包了一层把它们组装在一起。Tomcat内可能有多个Service这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
到此我们得到这样一张关系图:
<img src="https://static001.geekbang.org/resource/image/ee/d6/ee880033c5ae38125fa91fb3c4f8cad6.jpg" alt="">
从图上你可以看到最顶层是Server这里的Server指的就是一个Tomcat实例。一个Server中有一个或者多个Service一个Service中有多个连接器和一个容器。连接器与容器之间通过标准的ServletRequest和ServletResponse通信。
## 连接器
连接器对Servlet容器屏蔽了协议及I/O模型等的区别无论是HTTP还是AJP在容器中获取到的都是一个标准的ServletRequest对象。
我们可以把连接器的功能需求进一步细化,比如:
<li>
监听网络端口。
</li>
<li>
接受网络连接请求。
</li>
<li>
读取网络请求字节流。
</li>
<li>
根据具体应用层协议HTTP/AJP解析字节流生成统一的Tomcat Request对象。
</li>
<li>
将Tomcat Request对象转成标准的ServletRequest。
</li>
<li>
调用Servlet容器得到ServletResponse。
</li>
<li>
将ServletResponse转成Tomcat Response对象。
</li>
<li>
将Tomcat Response转成网络字节流。
</li>
<li>
将响应字节流写回给浏览器。
</li>
需求列清楚后,我们要考虑的下一个问题是,连接器应该有哪些子模块?优秀的模块化设计应该考虑**高内聚、低耦合**。
<li>
**高内聚**是指相关度比较高的功能要尽可能集中,不要分散。
</li>
<li>
**低耦合**是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
</li>
通过分析连接器的详细功能列表我们发现连接器需要完成3个**高内聚**的功能:
<li>
网络通信。
</li>
<li>
应用层协议解析。
</li>
<li>
Tomcat Request/Response与ServletRequest/ServletResponse的转化。
</li>
因此Tomcat的设计者设计了3个组件来实现这3个功能分别是Endpoint、Processor和Adapter。
组件之间通过抽象接口交互。这样做还有一个好处是**封装变化。**这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。
网络通信的I/O模型是变化的可能是非阻塞I/O、异步I/O或者APR。应用层协议也是变化的可能是HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。
但是整体的处理逻辑是不变的Endpoint负责提供字节流给ProcessorProcessor负责提供Tomcat Request对象给AdapterAdapter负责提供ServletRequest对象给容器。
如果要支持新的I/O方案、新的应用层协议只需要实现相关的具体子类上层通用的处理逻辑是不变的。
由于I/O模型和应用层协议可以自由组合比如NIO + HTTP或者NIO.2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑设计了一个叫ProtocolHandler的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如Http11NioProtocol和AjpNioProtocol。
除了这些变化点系统也存在一些相对稳定的部分因此Tomcat设计了一系列抽象基类来**封装这些稳定的部分**抽象基类AbstractProtocol实现了ProtocolHandler接口。每一种应用层协议有自己的抽象基类比如AbstractAjpProtocol和AbstractHttp11Protocol具体协议的实现类扩展了协议层抽象基类。下面我整理一下它们的继承关系。
<img src="https://static001.geekbang.org/resource/image/13/55/13850ee56c3f09cbabe9892e84502155.jpg" alt="">
通过上面的图你可以清晰地看到它们的继承和层次关系这样设计的目的是尽量将稳定的部分放到抽象基类同时每一种I/O模型和协议的组合都有相应的具体实现类我们在使用时可以自由选择。
小结一下连接器模块用三个核心组件Endpoint、Processor和Adapter来分别做三件事情其中Endpoint和Processor放在一起抽象成了ProtocolHandler组件它们的关系如下图所示。
<img src="https://static001.geekbang.org/resource/image/6e/ce/6eeaeb93839adcb4e76c15ee93f545ce.jpg" alt="">
下面我来详细介绍这两个顶层组件ProtocolHandler和Adapter。
**ProtocolHandler组件**
由上文我们知道连接器用ProtocolHandler来处理网络连接和应用层协议包含了2个重要部件Endpoint和Processor下面我来详细介绍它们的工作原理。
- Endpoint
Endpoint是通信端点即通信监听的接口是具体的Socket接收和发送处理器是对传输层的抽象因此Endpoint是用来实现TCP/IP协议的。
Endpoint是一个接口对应的抽象实现类是AbstractEndpoint而AbstractEndpoint的具体子类比如在NioEndpoint和Nio2Endpoint中有两个重要的子组件Acceptor和SocketProcessor。
其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求它实现Runnable接口在run方法里调用协议处理组件Processor进行处理。为了提高处理能力SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器Executor)我在后面的专栏会详细介绍Tomcat如何扩展原生的Java线程池。
- Processor
如果说Endpoint是用来实现TCP/IP协议的那么Processor用来实现HTTP协议Processor接收来自Endpoint的Socket读取字节流解析成Tomcat Request和Response对象并通过Adapter将其提交到容器处理Processor是对应用层协议的抽象。
Processor是一个接口定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属性进行封装没有对方法进行实现。具体的实现有AjpProcessor、Http11Processor等这些具体实现类实现了特定协议的解析方法和请求处理方式。
我们再来看看连接器的组件图:
<img src="https://static001.geekbang.org/resource/image/30/cf/309cae2e132210489d327cf55b284dcf.jpg" alt="">
从图中我们看到Endpoint接收到Socket连接后生成一个SocketProcessor任务提交到线程池去处理SocketProcessor的run方法会调用Processor组件去解析应用层协议Processor通过解析生成Request对象后会调用Adapter的Service方法。
到这里我们学习了ProtocolHandler的总体架构和工作原理关于Endpoint的详细设计后面我还会专门介绍Endpoint是如何最大限度地利用Java NIO的非阻塞以及NIO.2的异步特性,来实现高并发。
**Adapter组件**
我在前面说过由于协议不同客户端发过来的请求信息也不尽相同Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest也就意味着不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter这是适配器模式的经典运用连接器调用CoyoteAdapter的sevice方法传入的是Tomcat Request对象CoyoteAdapter负责将Tomcat Request转成ServletRequest再调用容器的service方法。
## 本期精华
Tomcat的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流容器负责内部处理。连接器用ProtocolHandler接口来封装通信协议和I/O模型的差异ProtocolHandler内部又分为Endpoint和Processor模块Endpoint负责底层Socket通信Processor负责应用层协议解析。连接器通过适配器Adapter调用容器。
通过对Tomcat整体架构的学习我们可以得到一些设计复杂系统的基本思路。首先要分析需求根据高内聚低耦合的原则确定子模块然后找出子模块中的变化点和不变点用接口和抽象基类去封装不变点在抽象基类中定义模板方法让子类自行实现抽象方法也就是具体子类去实现变化点。
## 课后思考
回忆一下你在工作中曾经独立设计过的系统,或者你碰到过的设计类面试题,结合今天专栏的内容,你有没有一些新的思路?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,139 @@
<audio id="audio" title="06 | Tomcat系统架构聊聊多层容器的设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/93/a8/932fcee9871bd36583c78261ee0832a8.mp3"></audio>
专栏上一期我们学完了连接器的设计今天我们一起来看一下Tomcat的容器设计。先复习一下上期我讲到了Tomcat有两个核心组件连接器和容器其中连接器负责外部交流容器负责内部处理。具体来说就是连接器处理Socket通信和应用层协议的解析得到Servlet请求而容器则负责处理Servlet请求。我们通过下面这张图来回忆一下。
<img src="https://static001.geekbang.org/resource/image/ee/d6/ee880033c5ae38125fa91fb3c4f8cad6.jpg" alt="">
容器顾名思义就是用来装载东西的器具在Tomcat里容器就是用来装载Servlet的。那Tomcat的Servlet容器是如何设计的呢
## 容器的层次结构
Tomcat设计了4种容器分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系而是父子关系。下面我画了一张图帮你理解它们的关系。
<img src="https://static001.geekbang.org/resource/image/cc/ed/cc968a11925591df558da0e7393f06ed.jpg" alt="">
你可能会问,为什么要设计成这么多层次的容器,这不是增加了复杂度吗?其实这背后的考虑是,**Tomcat通过一种分层的架构使得Servlet容器具有很好的灵活性。**
Context表示一个Web应用程序Wrapper表示一个Servlet一个Web应用程序中可能会有多个ServletHost代表的是一个虚拟主机或者说一个站点可以给Tomcat配置多个虚拟主机地址而一个虚拟主机下可以部署多个Web应用程序Engine表示引擎用来管理多个虚拟站点一个Service最多只能有一个Engine。
你可以再通过Tomcat的`server.xml`配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计它的构成组件都是可配置的其中最外层的是Server其他组件按照一定的格式要求配置在这个顶层容器中。
<img src="https://static001.geekbang.org/resource/image/82/66/82b3f97aab5152dd5fe74e947db2a266.jpg" alt="">
那么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应用通常有多个ServletTomcat还会在每个Context容器里创建多个Wrapper子容器。每个容器都有对应的访问路径你可以通过下面这张图来帮助你理解。
<img src="https://static001.geekbang.org/resource/image/be/96/be22494588ca4f79358347468cd62496.jpg" alt="">
假如有用户访问一个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路径找到WrapperServlet。**
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。我还是通过一张图来解释。
<img src="https://static001.geekbang.org/resource/image/b0/ca/b014ecce1f64b771bd58da62c05162ca.jpg" alt="">
整个调用过程由连接器中的Adapter触发的它会调用Engine的第一个Valve
```
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
```
Wrapper容器的最后一个Valve会创建一个Filter链并调用doFilter方法最终会调到Servlet的service方法。
你可能会问前面我们不是讲到了Filter似乎也有相似的功能那Valve和Filter有什么区别吗它们的区别是
<li>
Valve是Tomcat的私有机制与Tomcat的基础架构/API是紧耦合的。Servlet API是公有的标准所有的Web容器包括Jetty都支持Filter机制。
</li>
<li>
另一个重要的区别是Valve工作在Web容器级别拦截所有应用的请求而Servlet Filter工作在应用级别只能拦截某个Web应用的所有请求。如果想做整个Web容器的拦截器必须通过Valve来实现。
</li>
## 本期精华
今天我们学习了Tomcat容器的层次结构、根据请求定位Servlet的过程以及请求在容器中的调用过程。Tomcat设计了多层容器是为了灵活性的考虑灵活性具体体现在一个Tomcat实例Server可以有多个Service每个Service通过多个连接器监听不同的端口而一个Service又可以支持多个虚拟主机。一个URL网址可以用不同的主机名、不同的端口和不同的路径来访问特定的Servlet实例。
请求的链式调用是基于Pipeline-Valve责任链来完成的这样的设计使得系统具有良好的可扩展性如果需要扩展容器本身的功能只需要增加相应的Valve即可。
## 课后思考
Tomcat内的Context组件跟Servlet规范中的ServletContext接口有什么区别跟Spring中的ApplicationContext又有什么关系
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,152 @@
<audio id="audio" title="07 | Tomcat如何实现一键式启停" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/54/dd/54966edd8fee48887fcf1015a61dffdd.mp3"></audio>
通过前面的学习相信你对Tomcat的架构已经有所了解知道了Tomcat都有哪些组件组件之间是什么样的关系以及Tomcat是怎么处理一个HTTP请求的。下面我们通过一张简化的类图来回顾一下从图上你可以看到各种组件的层次关系图中的虚线表示一个请求在Tomcat中流转的过程。
<img src="https://static001.geekbang.org/resource/image/12/9b/12ad9ddc3ff73e0aacf2276bcfafae9b.png" alt="">
上面这张图描述了组件之间的静态关系如果想让一个系统能够对外提供服务我们需要创建、组装并启动这些组件在服务停止的时候我们还需要释放资源销毁这些组件因此这是一个动态的过程。也就是说Tomcat需要动态地管理这些组件的生命周期。
在我们实际的工作中,如果你需要设计一个比较大的系统或者框架时,你同样也需要考虑这几个问题:如何统一管理组件的创建、初始化、启动、停止和销毁?如何做到代码逻辑清晰?如何方便地添加或者删除组件?如何做到组件启动和停止不遗漏、不重复?
今天我们就来解决上面的问题,在这之前,先来看看组件之间的关系。如果你仔细分析过这些组件,可以发现它们具有两层关系。
<li>
第一层关系是组件有大有小大组件管理小组件比如Server管理ServiceService又管理连接器和容器。
</li>
<li>
第二层关系是组件有外有内,外层组件控制内层组件,比如连接器是外层组件,负责对外交流,外层组件调用内层组件完成业务功能。也就是说,**请求的处理过程是由外层组件来驱动的。**
</li>
这两层关系决定了系统在创建组件时应该遵循一定的顺序。
<li>
第一个原则是先创建子组件,再创建父组件,子组件需要被“注入”到父组件中。
</li>
<li>
第二个原则是先创建内层组件,再创建外层组件,内层组件需要被“注入”到外层组件。
</li>
因此,最直观的做法就是将图上所有的组件按照先小后大、先内后外的顺序创建出来,然后组装在一起。不知道你注意到没有,这个思路其实很有问题!因为这样不仅会造成代码逻辑混乱和组件遗漏,而且也不利于后期的功能扩展。
为了解决这个问题,我们希望找到一种通用的、统一的方法来管理组件的生命周期,就像汽车“一键启动”那样的效果。
## 一键式启停Lifecycle接口
我在前面说到过,设计就是要找到系统的变化点和不变点。这里的不变点就是每个组件都要经历创建、初始化、启动这几个过程,这些状态以及状态的转化是不变的。而变化点是每个具体组件的初始化方法,也就是启动方法是不一样的。
因此我们把不变点抽象出来成为一个接口这个接口跟生命周期有关叫作Lifecycle。Lifecycle接口里应该定义这么几个方法init、start、stop和destroy每个具体的组件去实现这些方法。
理所当然在父组件的init方法里需要创建子组件并调用子组件的init方法。同样在父组件的start方法里也需要调用子组件的start方法因此调用者可以无差别的调用各组件的init方法和start方法这就是**组合模式**的使用并且只要调用最顶层组件也就是Server组件的init和start方法整个Tomcat就被启动起来了。下面是Lifecycle接口的定义。
<img src="https://static001.geekbang.org/resource/image/a1/5c/a1fcba6105f4235486bdba350d58bb5c.png" alt="">
## 可扩展性Lifecycle事件
我们再来考虑另一个问题那就是系统的可扩展性。因为各个组件init和start方法的具体实现是复杂多变的比如在Host容器的启动方法里需要扫描webapps目录下的Web应用创建相应的Context容器如果将来需要增加新的逻辑直接修改start方法这样会违反开闭原则那如何解决这个问题呢开闭原则说的是为了扩展系统的功能你不能直接修改系统中已有的类但是你可以定义新的类。
我们注意到组件的init和start调用是由它的父组件的状态变化触发的上层组件的初始化会触发子组件的初始化上层组件的启动会触发子组件的启动因此我们把组件的生命周期定义成一个个状态把状态的转变看作是一个事件。而事件是有监听器的在监听器里可以实现一些逻辑并且监听器也可以方便的添加和删除这就是典型的**观察者模式**。
具体来说就是在Lifecycle接口里加入两个方法添加监听器和删除监听器。除此之外我们还需要定义一个Enum来表示组件有哪些状态以及处在什么状态会触发什么样的事件。因此Lifecycle接口和LifecycleState就定义成了下面这样。
<img src="https://static001.geekbang.org/resource/image/dd/c0/dd0ce38fdff06dcc6d40714f39fc4ec0.png" alt="">
从图上你可以看到组件的生命周期有NEW、INITIALIZING、INITIALIZED、STARTING_PREP、STARTING、STARTED等而一旦组件到达相应的状态就触发相应的事件比如NEW状态表示组件刚刚被实例化而当init方法被调用时状态就变成INITIALIZING状态这个时候就会触发BEFORE_INIT_EVENT事件如果有监听器在监听这个事件它的方法就会被调用。
## 重用性LifecycleBase抽象基类
有了接口,我们就要用类去实现接口。一般来说实现类不止一个,不同的类在实现接口时往往会有一些相同的逻辑,如果让各个子类都去实现一遍,就会有重复代码。那子类如何重用这部分逻辑呢?其实就是定义一个基类来实现共同的逻辑,然后让各个子类去继承它,就达到了重用的目的。
而基类中往往会定义一些抽象方法,所谓的抽象方法就是说基类不会去实现这些方法,而是调用这些方法来实现骨架逻辑。抽象方法是留给各个子类去实现的,并且子类必须实现,否则无法实例化。
比如宝马和荣威的底盘和骨架其实是一样的,只是发动机和内饰等配套是不一样的。底盘和骨架就是基类,宝马和荣威就是子类。仅仅有底盘和骨架还不是一辆真正意义上的车,只能算是半成品,因此在底盘和骨架上会留出一些安装接口,比如安装发动机的接口、安装座椅的接口,这些就是抽象方法。宝马或者荣威上安装的发动机和座椅是不一样的,也就是具体子类对抽象方法有不同的实现。
回到Lifecycle接口Tomcat定义一个基类LifecycleBase来实现Lifecycle接口把一些公共的逻辑放到基类中去比如生命状态的转变与维护、生命事件的触发以及监听器的添加和删除等而子类就负责实现自己的初始化、启动和停止等方法。为了避免跟基类中的方法同名我们把具体子类的实现方法改个名字在后面加上Internal叫initInternal、startInternal等。我们再来看引入了基类LifecycleBase后的类图
<img src="https://static001.geekbang.org/resource/image/67/d9/6704bf8a3e10e1d4cfb35ba11e6de5d9.png" alt="">
从图上可以看到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) {
...
}
}
```
这个方法逻辑比较清楚,主要完成了四步:
第一步检查状态的合法性比如当前状态必须是NEW然后才能进行初始化。
第二步触发INITIALIZING事件的监听器
```
setStateInternal(LifecycleState.INITIALIZING, null, false);
```
在这个setStateInternal方法里会调用监听器的业务方法。
第三步调用具体子类实现的抽象方法initInternal方法。我在前面提到过为了实现一键式启动具体组件在实现initInternal方法时又会调用它的子组件的init方法。
第四步子组件初始化后触发INITIALIZED事件的监听器相应监听器的业务方法就会被调用。
```
setStateInternal(LifecycleState.INITIALIZED, null, false);
```
总之LifecycleBase调用了抽象方法来实现骨架逻辑。讲到这里 你可能好奇LifecycleBase负责触发事件并调用监听器的方法那是什么时候、谁把监听器注册进来的呢
分为两种情况:
<li>
Tomcat自定义了一些监听器这些监听器是父组件在创建子组件的过程中注册到子组件的。比如MemoryLeakTrackingListener监听器用来检测Context容器中的内存泄漏这个监听器是Host容器在创建Context容器时注册到Context中的。
</li>
<li>
我们还可以在`server.xml`中定义自己的监听器Tomcat在启动时会解析`server.xml`,创建监听器并注册到容器组件。
</li>
## 生周期管理总体类图
通过上面的学习我相信你对Tomcat组件的生命周期的管理有了深入的理解我们再来看一张总体类图继续加深印象。
<img src="https://static001.geekbang.org/resource/image/de/90/de55ad3475e714acbf883713ee077690.png" alt="">
这里请你注意图中的StandardServer、StandardService等是Server和Service组件的具体实现类它们都继承了LifecycleBase。
StandardEngine、StandardHost、StandardContext和StandardWrapper是相应容器组件的具体实现类因为它们都是容器所以继承了ContainerBase抽象基类而ContainerBase实现了Container接口也继承了LifecycleBase类它们的生命周期管理接口和功能接口是分开的这也符合设计中**接口分离的原则**。
## 本期精华
Tomcat为了实现一键式启停以及优雅的生命周期管理并考虑到了可扩展性和可重用性将面向对象思想和设计模式发挥到了极致分别运用了**组合模式、观察者模式、骨架抽象类和模板方法**。
如果你需要维护一堆具有父子关系的实体,可以考虑使用组合模式。
观察者模式听起来“高大上”,其实就是当一个事件发生后,需要执行一连串更新操作。传统的实现方式是在事件响应代码里直接加更新逻辑,当更新逻辑加多了之后,代码会变得臃肿,并且这种方式是紧耦合的、侵入式的。而观察者模式实现了低耦合、非侵入式的通知与更新机制。
而模板方法在抽象基类中经常用到,用来实现通用逻辑。
## 课后思考
从文中最后的类图上你会看到所有的容器组件都扩展了ContainerBase跟LifecycleBase一样ContainerBase也是一个骨架抽象类请你思考一下各容器组件有哪些“共同的逻辑”需要ContainerBase由来实现呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,267 @@
<audio id="audio" title="08 | Tomcat的“高层们”都负责做什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/50/66/50a5b1e7769fe9d4e37d84dc695b0666.mp3"></audio>
使用过Tomcat的同学都知道我们可以通过Tomcat的`/bin`目录下的脚本`startup.sh`来启动Tomcat那你是否知道我们执行了这个脚本后发生了什么呢你可以通过下面这张流程图来了解一下。
<img src="https://static001.geekbang.org/resource/image/57/4d/578edfe9c06856324084ee193243694d.png" alt="">
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的启动就算完成了。下面我来详细介绍一下上面这个启动过程中提到的几个非常关键的启动类和组件。
你可以把Bootstrap看作是上帝它初始化了类加载器也就是创造万物的工具。
如果我们把Tomcat比作是一家公司那么Catalina应该是公司创始人因为Catalina负责组建团队也就是创建Server以及它的子组件。
Server是公司的CEO负责管理多个事业群每个事业群就是一个Service。
Service是事业群总经理它管理两个职能部门一个是对外的市场部也就是连接器组件另一个是对内的研发部也就是容器组件。
Engine则是研发部经理因为Engine是最顶层的容器组件。
你可以看到这些启动类或者组件不处理具体请求它们的任务主要是“管理”管理下层组件的生命周期并且给下层组件分配任务也就是把请求路由到负责“干活儿”的组件。因此我把它们比作Tomcat的“高层”。
今天我们就来看看这些“高层”的实现细节目的是让我们逐步理解Tomcat的工作原理。另一方面软件系统中往往都有一些起管理作用的组件你可以学习和借鉴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(&quot;catalina.noServer&quot;));
return;
}
//3.启动Server
try {
getServer().start();
} catch (LifecycleException e) {
return;
}
//创建并注册关闭钩子
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
//用await方法监听停止请求
if (await) {
await();
stop();
}
}
```
那什么是“关闭钩子”它又是做什么的呢如果我们需要在JVM关闭时做一些清理工作比如将缓存数据刷到磁盘上或者清理一些临时文件可以向JVM注册一个“关闭钩子”。“关闭钩子”其实就是一个线程JVM在停止之前会尝试执行这个线程的run方法。下面我们来看看Tomcat的“关闭钩子”CatalinaShutdownHook做了些什么。
```
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
Catalina.this.stop();
}
} catch (Throwable ex) {
...
}
}
}
```
从这段代码中你可以看到Tomcat的“关闭钩子”实际上就执行了Server的stop方法Server的stop方法会释放和清理所有的资源。
## Server组件
Server组件的具体实现类是StandardServer我们来看下StandardServer具体实现了哪些功能。Server继承了LifecycleBase它的生命周期被统一管理并且它的子组件是Service因此它还需要管理Service的生命周期也就是说在启动时调用Service组件的启动方法在停止时调用它们的停止方法。Server在内部维护了若干Service组件它是以数组来保存的那Server是如何添加一个Service到数组中的呢
```
@Override
public void addService(Service service) {
service.setServer(this);
synchronized (servicesLock) {
//创建一个长度+1的新数组
Service results[] = new Service[services.length + 1];
//将老的数据复制过去
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;
//启动Service组件
if (getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException e) {
// Ignore
}
}
//触发监听事件
support.firePropertyChange(&quot;service&quot;, null, service);
}
}
```
从上面的代码你能看到它并没有一开始就分配一个很长的数组而是在添加的过程中动态地扩展数组长度当添加一个新的Service实例时会创建一个新数组并把原来数组内容复制到新数组这样做的目的其实是为了节省内存空间。
除此之外Server组件还有一个重要的任务是启动一个Socket来监听停止端口这就是为什么你能通过shutdown命令来关闭Tomcat。不知道你留意到没有上面Catalina的启动方法的最后一行代码就是调用了Server的await方法。
在await方法里会创建一个Socket监听8005端口并在一个死循环里接收Socket上的连接请求如果有新的连接到来就建立连接然后从Socket中读取数据如果读到的数据是停止命令“SHUTDOWN”就退出循环进入stop流程。
## Service组件
Service组件的具体实现类是StandardService我们先来看看它的定义以及关键的成员变量。
```
public class StandardService extends LifecycleBase implements Service {
//名字
private String name = null;
//Server实例
private Server server = null;
//连接器数组
protected Connector connectors[] = new Connector[0];
private final Object connectorsLock = new Object();
//对应的Engine容器
private Engine engine = null;
//映射器及其监听器
protected final Mapper mapper = new Mapper();
protected final MapperListener mapperListener = new MapperListener(this);
```
StandardService继承了LifecycleBase抽象类此外StandardService中还有一些我们熟悉的组件比如Server、Connector、Engine和Mapper。
那为什么还有一个MapperListener这是因为Tomcat支持热部署当Web应用的部署发生变化时Mapper中的映射信息也要跟着变化MapperListener就是一个监听器它监听容器的变化并把信息更新到Mapper中这是典型的观察者模式。
作为“管理”角色的组件最重要的是维护其他组件的生命周期。此外在启动各种组件时要注意它们的依赖关系也就是说要注意启动的顺序。我们来看看Service启动方法
```
protected void startInternal() throws LifecycleException {
//1. 触发启动监听器
setState(LifecycleState.STARTING);
//2. 先启动EngineEngine会启动它子容器
if (engine != null) {
synchronized (engine) {
engine.start();
}
}
//3. 再启动Mapper监听器
mapperListener.start();
//4.最后启动连接器连接器会启动它子组件比如Endpoint
synchronized (connectorsLock) {
for (Connector connector: connectors) {
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}
```
从启动方法可以看到Service先启动了Engine组件再启动Mapper监听器最后才是启动连接器。这很好理解因为内层组件启动好了才能对外提供服务才能启动外层的连接器组件。而Mapper也依赖容器组件容器组件启动好了才能监听它们的变化因此Mapper和MapperListener在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的也是基于它们的依赖关系。
## Engine组件
最后我们再来看看顶层的容器组件Engine具体是如何实现的。Engine本质是一个容器因此它继承了ContainerBase基类并且实现了Engine接口。
```
public class StandardEngine extends ContainerBase implements Engine {
}
```
我们知道Engine的子容器是Host所以它持有了一个Host容器的数组这些功能都被抽象到了ContainerBase中ContainerBase中有这样一个数据结构
```
protected final HashMap&lt;String, Container&gt; children = new HashMap&lt;&gt;();
```
ContainerBase用HashMap保存了它的子容器并且ContainerBase还实现了子容器的“增删改查”甚至连子组件的启动和停止都提供了默认实现比如ContainerBase会用专门的线程池来启动子容器。
```
for (int i = 0; i &lt; children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}
```
所以Engine在启动Host子容器时就直接重用了这个方法。
那Engine自己做了什么呢我们知道容器组件最重要的功能是处理请求而Engine容器对请求的“处理”其实就是把请求转发给某一个Host子容器来处理具体是通过Valve来实现的。
通过专栏前面的学习我们知道每一个容器组件都有一个Pipeline而Pipeline中有一个基础阀Basic Valve而Engine容器的基础阀定义如下
```
final class StandardEngineValve extends ValveBase {
public final void invoke(Request request, Response response)
throws IOException, ServletException {
//拿到请求中的Host容器
Host host = request.getHost();
if (host == null) {
return;
}
// 调用Host容器中的Pipeline中的第一个Valve
host.getPipeline().getFirst().invoke(request, response);
}
}
```
这个基础阀实现非常简单就是把请求转发到Host容器。你可能好奇从代码中可以看到处理请求的Host容器对象是从请求中拿到的请求对象中怎么会有Host容器呢这是因为请求到达Engine容器中之前Mapper组件已经对请求进行了路由处理Mapper组件通过请求的URL定位了相应的容器并且把容器对象保存到了请求对象中。
## 本期精华
今天我们学习了Tomcat启动过程具体是由启动类和“高层”组件来完成的它们都承担着“管理”的角色负责将子组件创建出来并把它们拼装在一起同时也掌握子组件的“生杀大权”。
所以当我们在设计这样的组件时,需要考虑两个方面:
首先要选用合适的数据结构来保存子组件比如Server用数组来保存Service组件并且采取动态扩容的方式这是因为数组结构简单占用内存小再比如ContainerBase用HashMap来保存子容器虽然Map占用内存会多一点但是可以通过Map来快速的查找子容器。因此在实际的工作中我们也需要根据具体的场景和需求来选用合适的数据结构。
其次还需要根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。这正是“管理者”应该考虑的事情。
## 课后思考
Server组件的在启动连接器和容器时都分别加了锁这是为什么呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,226 @@
<audio id="audio" title="09 | 比较Jetty架构特点之Connector组件" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/aa/86daf3559544dcc83b1de4e4e4c650aa.mp3"></audio>
经过专栏前面几期的学习相信你对Tomcat的整体架构和工作原理有了基本了解。但是Servlet容器并非只有Tomcat一家还有别的架构设计思路吗今天我们就来看看Jetty的设计特点。
Jetty是Eclipse基金会的一个开源项目和Tomcat一样Jetty也是一个“HTTP服务器 + Servlet容器”并且Jetty和Tomcat在架构设计上有不少相似的地方。但同时Jetty也有自己的特点主要是更加小巧更易于定制化。Jetty作为一名后起之秀应用范围也越来越广比如Google App Engine就采用了Jetty来作为Web容器。Jetty和Tomcat各有特点所以今天我会和你重点聊聊Jetty在哪些地方跟Tomcat不同。通过比较它们的差异一方面希望可以继续加深你对Web容器架构设计的理解另一方面也让你更清楚它们的设计区别并根据它们的特点来选用这两款Web容器。
## 鸟瞰Jetty整体架构
简单来说Jetty Server就是由多个Connector连接器、多个Handler处理器以及一个线程池组成。整体结构请看下面这张图。
<img src="https://static001.geekbang.org/resource/image/95/b6/95b908af86695af107fd3877a02190b6.jpg" alt="">
跟Tomcat一样Jetty也有HTTP服务器和Servlet容器的功能因此Jetty中的Connector组件和Handler组件分别来实现这两个功能而这两个组件工作时所需要的线程资源都直接从一个全局线程池ThreadPool中获取。
Jetty Server可以有多个Connector在不同的端口上监听客户请求而对于请求处理的Handler组件也可以根据具体场景使用不同的Handler。这样的设计提高了Jetty的灵活性需要支持Servlet则可以使用ServletHandler需要支持Session则再增加一个SessionHandler。也就是说我们可以不使用Servlet或者Session只要不配置这个Handler就行了。
为了启动和协调上面的核心组件工作Jetty提供了一个Server类来做这个事情它负责创建并初始化Connector、Handler、ThreadPool组件然后调用start方法启动它们。
我们对比一下Tomcat的整体架构图你会发现Tomcat在整体上跟Jetty很相似它们的第一个区别是Jetty中没有Service的概念Tomcat中的Service包装了多个连接器和一个容器组件一个Tomcat实例可以配置多个Service不同的Service通过不同的连接器监听不同的端口而Jetty中Connector是被所有Handler共享的。
<img src="https://static001.geekbang.org/resource/image/7b/f2/7b100cb1c7399a366157b97b77f042f2.jpg" alt="">
它们的第二个区别是在Tomcat中每个连接器都有自己的线程池而在Jetty中所有的Connector共享一个全局的线程池。
讲完了Jetty的整体架构接下来我来详细分析Jetty的Connector组件的设计下一期我将分析Handler组件的设计。
## Connector组件
跟Tomcat一样Connector的主要功能是对I/O模型和应用层协议的封装。I/O模型方面最新的Jetty 9版本只支持NIO因此Jetty的Connector设计有明显的Java NIO通信模型的痕迹。至于应用层协议方面跟Tomcat的Processor一样Jetty抽象出了Connection组件来封装应用层协议的差异。
Java NIO早已成为程序员的必备技能并且也经常出现在面试题中。接下来我们一起来看看Jetty是如何实现NIO模型的以及它是怎么**用**Java NIO的。
**Java NIO回顾**
关于Java NIO编程如果你还不太熟悉可以先学习这一[系列文章](http://ifeve.com/java-nio-all/)。Java NIO的核心组件是Channel、Buffer和Selector。Channel表示一个连接可以理解为一个Socket通过它可以读取和写入数据但是并不能直接操作数据需要通过Buffer来中转。
Selector可以用来检测Channel上的I/O事件比如读就绪、写就绪、连接就绪一个Selector可以同时处理多个Channel因此单个线程可以监听多个Channel这样会大量减少线程上下文切换的开销。下面我们通过一个典型的服务端NIO程序来回顾一下如何使用这些组件。
首先创建服务端Channel绑定监听端口并把Channel设置为非阻塞方式。
```
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(port));
server.configureBlocking(false);
```
然后创建Selector并在Selector中注册Channel感兴趣的事件OP_ACCEPT告诉Selector如果客户端有新的连接请求到这个端口就通知我。
```
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
```
接下来Selector会在一个死循环里不断地调用select去查询I/O状态select会返回一个SelectionKey列表Selector会遍历这个列表看看是否有“客户”感兴趣的事件如果有就采取相应的动作。
比如下面这个例子如果有新的连接请求就会建立一个新的连接。连接建立后再注册Channel的可读事件到Selector中告诉Selector我对这个Channel上是否有新的数据到达感兴趣。
```
while (true) {
selector.select();//查询I/O事件
for (Iterator&lt;SelectionKey&gt; i = selector.selectedKeys().iterator(); i.hasNext();) {
SelectionKey key = i.next();
i.remove();
if (key.isAcceptable()) {
// 建立一个新连接
SocketChannel client = server.accept();
client.configureBlocking(false);
//连接建立后告诉Selector我现在对I/O可读事件感兴趣
client.register(selector, SelectionKey.OP_READ);
}
}
}
```
简单回顾完服务端NIO编程之后你会发现服务端在I/O通信上主要完成了三件事情**监听连接、I/O事件查询以及数据读写**。因此Jetty设计了**Acceptor、SelectorManager和Connection来分别做这三件事情**,下面我分别来说说这三个组件。
**Acceptor**
顾名思义Acceptor用于接受请求跟Tomcat一样Jetty也有独立的Acceptor线程组用于处理连接请求。在Connector的实现类ServerConnector中有一个`_acceptors`的数组在Connector启动的时候, 会根据`_acceptors`数组的长度创建对应数量的Acceptor而Acceptor的个数可以配置。
```
for (int i = 0; i &lt; _acceptors.length; i++)
{
Acceptor a = new Acceptor(i);
getExecutor().execute(a);
}
```
Acceptor是ServerConnector中的一个内部类同时也是一个RunnableAcceptor线程是通过getExecutor得到的线程池来执行的前面提到这是一个全局的线程池。
Acceptor通过阻塞的方式来接受连接这一点跟Tomcat也是一样的。
```
public void accept(int acceptorID) throws IOException
{
ServerSocketChannel serverChannel = _acceptChannel;
if (serverChannel != null &amp;&amp; serverChannel.isOpen())
{
// 这里是阻塞的
SocketChannel channel = serverChannel.accept();
// 执行到这里时说明有请求进来了
accepted(channel);
}
}
```
接受连接成功后会调用accepted函数accepted函数中会将SocketChannel设置为非阻塞模式然后交给Selector去处理因此这也就到了Selector的地界了。
```
private void accepted(SocketChannel channel) throws IOException
{
channel.configureBlocking(false);
Socket socket = channel.socket();
configure(socket);
// _manager是SelectorManager实例里面管理了所有的Selector实例
_manager.accept(channel);
}
```
**SelectorManager**
Jetty的Selector由SelectorManager类管理而被管理的Selector叫作ManagedSelector。SelectorManager内部有一个ManagedSelector数组真正干活的是ManagedSelector。咱们接着上面分析看看在SelectorManager在accept方法里做了什么。
```
public void accept(SelectableChannel channel, Object attachment)
{
//选择一个ManagedSelector来处理Channel
final ManagedSelector selector = chooseSelector();
//提交一个任务Accept给ManagedSelector
selector.submit(selector.new Accept(channel, attachment));
}
```
SelectorManager从本身的Selector数组中选择一个Selector来处理这个Channel并创建一个任务Accept交给ManagedSelectorManagedSelector在处理这个任务主要做了两步
第一步调用Selector的register方法把Channel注册到Selector上拿到一个SelectionKey。
```
_key = _channel.register(selector, SelectionKey.OP_ACCEPT, this);
```
第二步创建一个EndPoint和Connection并跟这个SelectionKeyChannel绑在一起
```
private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException
{
//1. 创建EndPoint
EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey);
//2. 创建Connection
Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment());
//3. 把EndPoint、Connection和SelectionKey绑在一起
endPoint.setConnection(connection);
selectionKey.attach(endPoint);
}
```
上面这两个过程是什么意思呢打个比方你到餐厅吃饭先点菜注册I/O事件服务员ManagedSelector给你一个单子SelectionKey等菜做好了I/O事件到了服务员根据单子就知道是哪桌点了这个菜于是喊一嗓子某某桌的菜做好了调用了绑定在SelectionKey上的EndPoint的方法
这里需要你特别注意的是ManagedSelector并没有调用直接EndPoint的方法去处理数据而是通过调用EndPoint的方法**返回一个Runnable然后把这个Runnable扔给线程池执行**所以你能猜到这个Runnable才会去真正读数据和处理请求。
**Connection**
这个Runnable是EndPoint的一个内部类它会调用Connection的回调方法来处理请求。Jetty的Connection组件类比就是Tomcat的Processor负责具体协议的解析得到Request对象并调用Handler容器进行处理。下面我简单介绍一下它的具体实现类HttpConnection对请求和响应的处理过程。
**请求处理**HttpConnection并不会主动向EndPoint读取数据而是向在EndPoint中注册一堆回调方法
```
getEndPoint().fillInterested(_readCallback);
```
这段代码就是告诉EndPoint数据到了你就调我这些回调方法`_readCallback`有点异步I/O的感觉也就是说Jetty在应用层面模拟了异步I/O模型。
而在回调方法`_readCallback`会调用EndPoint的接口去读数据读完后让HTTP解析器去解析字节流HTTP解析器会将解析后的数据包括请求行、请求头相关信息存到Request对象里。
**响应处理**Connection调用Handler进行业务处理Handler会通过Response对象来操作响应流向流里面写入数据HttpConnection再通过EndPoint把数据写到Channel这样一次响应就完成了。
到此你应该了解了Connector的工作原理下面我画张图再来回顾一下Connector的工作流程。
<img src="https://static001.geekbang.org/resource/image/b5/83/b526a90be6ee4c3e45c94e122c9c1e83.jpg" alt="">
1.Acceptor监听连接请求当有连接请求到达时就接受连接一个连接对应一个ChannelAcceptor将Channel交给ManagedSelector来处理。
2.ManagedSelector把Channel注册到Selector上并创建一个EndPoint和Connection跟这个Channel绑定接着就不断地检测I/O事件。
3.I/O事件到了就调用EndPoint的方法拿到一个Runnable并扔给线程池执行。
4.线程池中调度某个线程执行Runnable。
5.Runnable执行时调用回调函数这个回调函数是Connection注册到EndPoint中的。
6.回调函数内部实现其实就是调用EndPoint的接口方法来读数据。
7.Connection解析读到的数据生成请求对象并交给Handler组件去处理。
## 本期精华
Jetty Server就是由多个Connector、多个Handler以及一个线程池组成在设计上简洁明了。
Jetty的Connector只支持NIO模型跟Tomcat的NioEndpoint组件一样它也是通过Java的NIO API实现的。我们知道Java NIO编程有三个关键组件Channel、Buffer和Selector而核心是Selector。为了方便使用Jetty在原生Selector组件的基础上做了一些封装实现了ManagedSelector组件。
在线程模型设计上Tomcat的NioEndpoint跟Jetty的Connector是相似的都是用一个Acceptor数组监听连接用一个Selector数组侦测I/O事件用一个线程池执行请求。它们的不同点在于Jetty使用了一个全局的线程池所有的线程资源都是从线程池来分配。
Jetty Connector设计中的一大特点是使用了回调函数来模拟异步I/O比如Connection向EndPoint注册了一堆回调函数。它的本质**将函数当作一个参数来传递**,告诉对方,你准备好了就调这个回调函数。
## 课后思考
Jetty的Connector主要完成了三件事件接收连接、I/O事件查询以及数据读写。因此Jetty设计了Acceptor、SelectorManager和Connection来做这三件事情。今天的思考题是为什么要把这些组件跑在不同的线程里呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,123 @@
<audio id="audio" title="10 | 比较Jetty架构特点之Handler组件" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/88/7247d03fdc73836019655db81219ba88.mp3"></audio>
在专栏上一期我们学习了Jetty的整体架构。先来回顾一下Jetty 就是由多个Connector连接器、多个Handler处理器以及一个线程池组成整体结构图如下。
<img src="https://static001.geekbang.org/resource/image/66/6a/66e55e89fd621c0eba6321471da2016a.png" alt="">
上一期我们分析了Jetty Connector组件的设计Connector会将Servlet请求交给Handler去处理那Handler又是如何处理请求的呢
Jetty的Handler在设计上非常有意思可以说是Jetty的灵魂Jetty通过Handler实现了高度可定制化那具体是如何实现的呢我们能从中学到怎样的设计方法呢接下来我就来聊聊这些问题。
## Handler是什么
**Handler就是一个接口它有一堆实现类**Jetty的Connector组件调用这些接口来处理Servlet请求我们先来看看这个接口定义成什么样子。
```
public interface Handler extends LifeCycle, Destroyable
{
//处理请求的方法
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException;
//每个Handler都关联一个Server组件被Server管理
public void setServer(Server server);
public Server getServer();
//销毁方法相关的资源
public void destroy();
}
```
你会看到Handler接口的定义非常简洁主要就是用handle方法用来处理请求跟Tomcat容器组件的service方法一样它有ServletRequest和ServletResponse两个参数。除此之外这个接口中还有setServer和getServer方法因为任何一个Handler都需要关联一个Server组件也就是说Handler需要被Server组件来管理。一般来说Handler会加载一些资源到内存因此通过设置destroy方法来销毁。
**Handler继承关系**
Handler只是一个接口完成具体功能的还是它的子类。那么Handler有哪些子类呢它们的继承关系又是怎样的这些子类是如何实现Servlet容器功能的呢
Jetty中定义了一些默认Handler类并且这些Handler类之间的继承关系比较复杂我们先通过一个全景图来了解一下。为了避免让你感到不适我对类图进行了简化。
<img src="https://static001.geekbang.org/resource/image/3a/64/3a7b3fbf16bb79594ec23620507c5c64.png" alt="">
从图上你可以看到Handler的种类和层次关系还是比较复杂的
Handler接口之下有抽象类AbstractHandler这一点并不意外因为有接口一般就有抽象实现类。
在AbstractHandler之下有AbstractHandlerContainer为什么需要这个类呢这其实是个过渡为了实现链式调用一个Handler内部必然要有其他Handler的引用所以这个类的名字里才有Container意思就是这样的Handler里包含了其他Handler的引用。
理解了上面的AbstractHandlerContainer我们就能理解它的两个子类了HandlerWrapper和HandlerCollection。简单来说就是HandlerWrapper和HandlerCollection都是Handler但是这些Handler里还包括其他Handler的引用。不同的是HandlerWrapper只包含一个其他Handler的引用而HandlerCollection中有一个Handler数组的引用。
<img src="https://static001.geekbang.org/resource/image/f8/c1/f89a1e88a78a7e6860d69af3572467c1.png" alt="">
接着来看左边的HandlerWrapper它有两个子类Server和ScopedHandler。Server比较好理解它本身是Handler模块的入口必然要将请求传递给其他Handler来处理为了触发其他Handler的调用所以它是一个HandlerWrapper。
再看ScopedHandler它也是一个比较重要的Handler实现了“具有上下文信息”的责任链调用。为什么我要强调“具有上下文信息”呢那是因为Servlet规范规定Servlet在执行过程中是有上下文的。那么这些Handler在执行过程中如何访问这个上下文呢这个上下文又存在什么地方呢答案就是通过ScopedHandler来实现的。
而ScopedHandler有一堆的子类这些子类就是用来实现Servlet规范的比如ServletHandler、ContextHandler、SessionHandler、ServletContextHandler和WebAppContext。接下来我会详细介绍它们但我们先把总体类图看完。
请看类图的右边跟HandlerWrapper对等的还有HandlerCollectionHandlerCollection其实维护了一个Handler数组。你可能会问为什么要发明一个这样的Handler这是因为Jetty可能需要同时支持多个Web应用如果每个Web应用有一个Handler入口那么多个Web应用的Handler就成了一个数组比如Server中就有一个HandlerCollectionServer会根据用户请求的URL从数组中选取相应的Handler来处理就是选择特定的Web应用来处理请求。
**Handler的类型**
虽然从类图上看Handler有很多但是本质上这些Handler分成三种类型
- 第一种是**协调Handler**这种Handler负责将请求路由到一组Handler中去比如上图中的HandlerCollection它内部持有一个Handler数组当请求到来时它负责将请求转发到数组中的某一个Handler。
- 第二种是**过滤器Handler**这种Handler自己会处理请求处理完了后再把请求转发到下一个Handler比如图上的HandlerWrapper它内部持有下一个Handler的引用。需要注意的是所有继承了HandlerWrapper的Handler都具有了过滤器Handler的特征比如ContextHandler、SessionHandler和WebAppContext等。
- 第三种是**内容Handler**说白了就是这些Handler会真正调用Servlet来处理请求生成响应的内容比如ServletHandler。如果浏览器请求的是一个静态资源也有相应的ResourceHandler来处理这个请求返回静态页面。
## 如何实现Servlet规范
上文提到ServletHandler、ContextHandler以及WebAppContext等它们实现了Servlet规范那具体是怎么实现的呢为了帮助你理解在这之前我们还是来看看如何使用Jetty来启动一个Web应用。
```
//新建一个WebAppContextWebAppContext是一个Handler
WebAppContext webapp = new WebAppContext();
webapp.setContextPath(&quot;/mywebapp&quot;);
webapp.setWar(&quot;mywebapp.war&quot;);
//将Handler添加到Server中去
server.setHandler(webapp);
//启动Server
server.start();
server.join();
```
上面的过程主要分为两步:
第一步创建一个WebAppContext接着设置一些参数到这个Handler中就是告诉WebAppContext你的WAR包放在哪Web应用的访问路径是什么。
第二步就是把新创建的WebAppContext添加到Server中然后启动Server。
WebAppContext对应一个Web应用。我们回忆一下Servlet规范中有Context、Servlet、Filter、Listener和Session等Jetty要支持Servlet规范就需要有相应的Handler来分别实现这些功能。因此Jetty设计了3个组件ContextHandler、ServletHandler和SessionHandler来实现Servlet规范中规定的功能而**WebAppContext本身就是一个ContextHandler**另外它还负责管理ServletHandler和SessionHandler。
我们再来看一下什么是ContextHandler。ContextHandler会创建并初始化Servlet规范里的ServletContext对象同时ContextHandler还包含了一组能够让你的Web应用运行起来的Handler可以这样理解Context本身也是一种Handler它里面包含了其他的Handler这些Handler能处理某个特定URL下的请求。比如ContextHandler包含了一个或者多个ServletHandler。
再来看ServletHandler它实现了Servlet规范中的Servlet、Filter和Listener的功能。ServletHandler依赖FilterHolder、ServletHolder、ServletMapping、FilterMapping这四大组件。FilterHolder和ServletHolder分别是Filter和Servlet的包装类每一个Servlet与路径的映射会被封装成ServletMapping而Filter与拦截URL的映射会被封装成FilterMapping。
SessionHandler从名字就知道它的功能用来管理Session。除此之外WebAppContext还有一些通用功能的Handler比如SecurityHandler和GzipHandler同样从名字可以知道这些Handler的功能分别是安全控制和压缩/解压缩。
WebAppContext会将这些Handler构建成一个执行链通过这个链会最终调用到我们的业务Servlet。我们通过一张图来理解一下。
<img src="https://static001.geekbang.org/resource/image/5f/c1/5f1404567deec36ac68c36e44bb06cc1.jpg" alt="">
通过对比Tomcat的架构图你可以看到Jetty的Handler组件和Tomcat中的容器组件是大致是对等的概念Jetty中的WebAppContext相当于Tomcat的Context组件都是对应一个Web应用而Jetty中的ServletHandler对应Tomcat中的Wrapper组件它负责初始化和调用Servlet并实现了Filter的功能。
对于一些通用组件比如安全和解压缩在Jetty中都被做成了Handler这是Jetty Handler架构的特点。
因此对于Jetty来说请求处理模块就被抽象成Handler不管是实现了Servlet规范的Handler还是实现通用功能的Handler比如安全、解压缩等我们可以任意添加或者裁剪这些“功能模块”从而实现高度的可定制化。
## 本期精华
Jetty Server就是由多个Connector、多个Handler以及一个线程池组成。
Jetty的Handler设计是它的一大特色Jetty本质就是一个Handler管理器Jetty本身就提供了一些默认Handler来实现Servlet容器的功能你也可以定义自己的Handler来添加到Jetty中这体现了“**微内核 + 插件**”的设计思想。
## 课后思考
通过今天的学习我们知道各种Handler都会对请求做一些处理再将请求传给下一个Handler而Servlet也是用来处理请求的那Handler跟Servlet有什么区别呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,54 @@
<audio id="audio" title="11 | 总结从Tomcat和Jetty中提炼组件化设计规范" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6e/42/6ea42a9860fc77edb204200e5fd49742.mp3"></audio>
在当今的互联网时代,我们每个人获取信息的机会基本上都是平等的,但是为什么有些人对信息理解得更深,并且有自己独到的见解呢?我认为是因为他们养成了思考和总结的好习惯。当我们学习一门技术的时候,如果可以勤于思考、善于总结,可以帮助我们看到现象背后更本质的东西,让我们在成长之路上更快“脱颖而出”。
我们经常谈敏捷、快速迭代和重构这些都是为了应对需求的快速变化也因此我们在开始设计一个系统时就要考虑可扩展性。那究竟该怎样设计才能适应变化呢或者要设计成什么样后面才能以最小的成本进行重构呢今天我来总结一些Tomcat和Jetty组件化的设计思想或许从中我们可以得到一些启发。
## 组件化及可配置
Tomcat和Jetty的整体架构都是基于组件的你可以通过XML文件或者代码的方式来配置这些组件比如我们可以在server.xml配置Tomcat的连接器以及容器组件。相应的你也可以在jetty.xml文件里组装Jetty的Connector组件以及各种Handler组件。也就是说**Tomcat和Jetty提供了一堆积木怎么搭建这些积木由你来决定**你可以根据自己的需要灵活选择组件来搭建你的Web容器并且也可以自定义组件这样的设计为Web容器提供了深度可定制化。
那Web容器如何实现这种组件化设计呢我认为有两个要点
- 第一个是面向接口编程。我们需要对系统的功能按照“高内聚、低耦合”的原则进行拆分,每个组件都有相应的接口,组件之间通过接口通信,这样就可以方便地替换组件了。比如我们可以选择不同连接器类型,只要这些连接器组件实现同一个接口就行。
- 第二个是Web容器提供一个载体把组件组装在一起工作。组件的工作无非就是处理请求因此容器通过责任链模式把请求依次交给组件去处理。对于用户来说我只需要告诉Web容器由哪些组件来处理请求。把组件组织起来需要一个“管理者”这就是为什么Tomcat和Jetty都有一个Server的概念Server就是组件的载体Server里包含了连接器组件和容器组件容器还需要把请求交给各个子容器组件去处理Tomcat和Jetty都是责任链模式来实现的。
用户通过配置来组装组件跟Spring中Bean的依赖注入相似。Spring的用户可以通过配置文件或者注解的方式来组装BeanBean与Bean的依赖关系完全由用户自己来定义。这一点与Web容器**不同**Web容器中组件与组件之间的关系是固定的比如Tomcat中Engine组件下有Host组件、Host组件下有Context组件等但你不能在Host组件里“注入”一个Wrapper组件这是由于Web容器本身的功能来决定的。
## 组件的创建
由于组件是可以配置的Web容器在启动之前并不知道要创建哪些组件也就是说不能通过硬编码的方式来实例化这些组件而是需要通过反射机制来动态地创建。具体来说Web容器不是通过new方法来实例化组件对象的而是通过Class.forName来创建组件。无论哪种方式在实例化一个类之前Web容器需要把组件类加载到JVM这就涉及一个类加载的问题Web容器设计了自己类加载器我会在专栏后面的文章详细介绍Tomcat的类加载器。
Spring也是通过反射机制来动态地实例化Bean那么它用到的类加载器是从哪里来的呢Web容器给每个Web应用创建了一个类加载器Spring用到的类加载器是Web容器传给它的。
## 组件的生命周期管理
不同类型的组件具有父子层次关系父组件处理请求后再把请求传递给某个子组件。你可能会感到疑惑Jetty的中Handler不是一条链吗看上去像是平行关系其实不然Jetty中的Handler也是分层次的比如WebAppContext中包含ServletHandler和SessionHandler。因此你也可以把ContextHandler和它所包含的Handler看作是父子关系。
而Tomcat通过容器的概念把小容器放到大容器来实现父子关系其实它们的本质都是一样的。这其实涉及如何统一管理这些组件如何做到一键式启停。
Tomcat和Jetty都采用了类似的办法来管理组件的生命周期主要有两个要点一是父组件负责子组件的创建、启停和销毁。这样只要启动最上层组件整个Web容器就被启动起来了也就实现了一键式启停二是Tomcat和Jetty都定义了组件的生命周期状态并且把组件状态的转变定义成一个事件一个组件的状态变化会触发子组件的变化比如Host容器的启动事件里会触发Web应用的扫描和加载最终会在Host容器下创建相应的Context容器而Context组件的启动事件又会触发Servlet的扫描进而创建Wrapper组件。那么如何实现这种联动呢答案是观察者模式。具体来说就是创建监听器去监听容器的状态变化在监听器的方法里去实现相应的动作这些监听器其实是组件生命周期过程中的“扩展点”。
Spring也采用了类似的设计Spring给Bean生命周期状态提供了很多的“扩展点”。这些扩展点被定义成一个个接口只要你的Bean实现了这些接口Spring就会负责调用这些接口这样做的目的就是当Bean的创建、初始化和销毁这些控制权交给Spring后Spring让你有机会在Bean的整个生命周期中执行你的逻辑。下面我通过一张图帮你理解Spring Bean的生命周期过程
<img src="https://static001.geekbang.org/resource/image/7f/3d/7f87b5f06cef33af6266ae7f6dcf203d.png" alt="">
## 组件的骨架抽象类和模板模式
具体到组件的设计的与实现Tomcat和Jetty都大量采用了骨架抽象类和模板模式。比如说Tomcat中ProtocolHandler接口ProtocolHandler有抽象基类AbstractProtocol它实现了协议处理层的骨架和通用逻辑而具体协议也有抽象基类比如HttpProtocol和AjpProtocol。对于Jetty来说Handler接口之下有AbstractHandlerConnector接口之下有AbstractConnector这些抽象骨架类实现了一些通用逻辑并且会定义一些抽象方法这些抽象方法由子类实现抽象骨架类调用抽象方法来实现骨架逻辑。
这是一个通用的设计规范不管是Web容器还是Spring甚至JDK本身都到处使用这种设计比如Java集合中的AbstractSet、AbstractMap等。 值得一提的是从Java 8开始允许接口有default方法这样我们可以把抽象骨架类的通用逻辑放到接口中去。
## 本期精华
今天我总结了Tomcat和Jetty的组件化设计我们可以通过搭积木的方式来定制化自己的Web容器。Web容器为了支持这种组件化设计遵循了一些规范比如面向接口编程用“管理者”去组装这些组件用反射的方式动态的创建组件、统一管理组件的生命周期并且给组件生命状态的变化提供了扩展点组件的具体实现一般遵循骨架抽象类和模板模式。
通过今天的学习你会发现Tomcat和Jetty有很多共同点并且Spring框架的设计也有不少相似的的地方这正好说明了Web开发中有一些本质的东西是相通的只要你深入理解了一个技术也就是在一个点上突破了深度再扩展广度就不是难事。并且我建议在学习一门技术的时候可以回想一下之前学过的东西是不是有相似的地方有什么不同的地方通过对比理解它们的本质这样我们才能真正掌握这些技术背后的精髓。
## 课后思考
在我们的实际项目中,可能经常遇到改变需求,那如果采用组件化设计,当需求更改时是不是会有一些帮助呢?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="12 | 实战优化并提高Tomcat启动速度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1c/28/1cd234a8fa1e16e291ed04a55a944928.mp3"></audio>
到目前为止我们学习了Tomcat和Jetty的整体架构还知道了Tomcat是如何启动起来的今天我们来聊一个比较轻松的话题如何优化并提高Tomcat的启动速度。
我们在使用Tomcat时可能会碰到启动比较慢的问题比如我们的系统发布新版本上线时可能需要重启服务这个时候我们希望Tomcat能快速启动起来提供服务。其实关于如何让Tomcat启动变快官方网站有专门的[文章](https://wiki.apache.org/tomcat/HowTo/FasterStartUp)来介绍这个话题。下面我也针对Tomcat 8.5和9.0版本,给出几条非常明确的建议,可以现学现用。
## 清理你的Tomcat
**1. 清理不必要的Web应用**
首先我们要做的是删除掉webapps文件夹下不需要的工程一般是host-manager、example、doc等这些默认的工程可能还有以前添加的但现在用不着的工程最好把这些全都删除掉。如果你看过Tomcat的启动日志可以发现每次启动Tomcat都会重新布署这些工程。
**2. 清理XML配置文件**
我们知道Tomcat在启动的时候会解析所有的XML配置文件但XML解析的代价可不小因此我们要尽量保持配置文件的简洁需要解析的东西越少速度自然就会越快。
**3. 清理JAR文件**
我们还可以删除所有不需要的JAR文件。JVM的类加载器在加载类时需要查找每一个JAR文件去找到所需要的类。如果删除了不需要的JAR文件查找的速度就会快一些。这里请注意**Web应用中的lib目录下不应该出现Servlet API或者Tomcat自身的JAR**这些JAR由Tomcat负责提供。如果你是使用Maven来构建你的应用对Servlet API的依赖应该指定为`&lt;scope&gt;provided&lt;/scope&gt;`
**4. 清理其他文件**
及时清理日志删除logs文件夹下不需要的日志文件。同样还有work文件夹下的catalina文件夹它其实是Tomcat把JSP转换为Class文件的工作目录。有时候我们也许会遇到修改了代码重启了Tomcat但是仍没效果这时候便可以删除掉这个文件夹Tomcat下次启动的时候会重新生成。
## 禁止Tomcat TLD扫描
Tomcat为了支持JSP在应用启动的时候会扫描JAR包里面的TLD文件加载里面定义的标签库所以在Tomcat的启动日志里你可能会碰到这种提示
>
At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
Tomcat的意思是我扫描了你Web应用下的JAR包发现JAR包里没有TLD文件。我建议配置一下Tomcat不要去扫描这些JAR包这样可以提高Tomcat的启动速度并节省JSP编译时间。
那如何配置不去扫描这些JAR包呢这里分两种情况
- 如果你的项目没有使用JSP作为Web页面模板而是使用Velocity之类的模板引擎你完全可以把TLD扫描禁止掉。方法是找到Tomcat的`conf/`目录下的`context.xml`文件在这个文件里Context标签下加上**JarScanner**和**JarScanFilter**子标签,像下面这样。
<img src="https://static001.geekbang.org/resource/image/b9/6e/b9c09507c546c6ff349270cd992ff66e.jpg" alt="">
- 如果你的项目使用了JSP作为Web页面模块意味着TLD扫描无法避免但是我们可以通过配置来告诉Tomcat只扫描那些包含TLD文件的JAR包。方法是找到Tomcat的`conf/`目录下的`catalina.properties`文件在这个文件里的jarsToSkip配置项中加上你的JAR包。
```
tomcat.util.scan.StandardJarScanFilter.jarsToSkip=xxx.jar
```
## 关闭WebSocket支持
Tomcat会扫描WebSocket注解的API实现比如`@ServerEndpoint`注解的类。我们知道注解扫描一般是比较慢的如果不需要使用WebSocket就可以关闭它。具体方法是找到Tomcat的`conf/`目录下的`context.xml`文件给Context标签加一个**containerSciFilter**的属性,像下面这样。
<img src="https://static001.geekbang.org/resource/image/fb/fc/fb25c3f5e44521ec47046fafec11e0fc.jpg" alt="">
更进一步如果你不需要WebSocket这个功能你可以把Tomcat lib目录下的`websocket-api.jar``tomcat-websocket.jar`这两个JAR文件删除掉进一步提高性能。
## 关闭JSP支持
跟关闭WebSocket一样如果你不需要使用JSP可以通过类似方法关闭JSP功能像下面这样。
<img src="https://static001.geekbang.org/resource/image/0e/c4/0ef2d6f508babe62960909c6c881c4c4.jpg" alt="">
我们发现关闭JSP用的也是**containerSciFilter**属性如果你想把WebSocket和JSP都关闭那就这样配置
<img src="https://static001.geekbang.org/resource/image/eb/5d/ebd41a1156f28e92257f4c0130a8125d.jpg" alt="">
## 禁止Servlet注解扫描
Servlet 3.0引入了注解ServletTomcat为了支持这个特性会在Web应用启动时扫描你的类文件因此如果你没有使用Servlet注解这个功能可以告诉Tomcat不要去扫描Servlet注解。具体配置方法是在你的Web应用的`web.xml`文件中,设置`&lt;web-app&gt;`元素的属性`metadata-complete="true"`,像下面这样。
<img src="https://static001.geekbang.org/resource/image/9b/60/9b54b6eebfe23017f4e90e7a16c97760.jpg" alt="">
`metadata-complete`的意思是,`web.xml`里配置的Servlet是完整的不需要再去库类中找Servlet的定义。
## 配置Web-Fragment扫描
Servlet 3.0还引入了“Web模块部署描述符片段”的`web-fragment.xml`,这是一个部署描述文件,可以完成`web.xml`的配置功能。而这个`web-fragment.xml`文件必须存放在JAR文件的`META-INF`目录下而JAR包通常放在`WEB-INF/lib`目录下因此Tomcat需要对JAR文件进行扫描才能支持这个功能。
你可以通过配置`web.xml`里面的`&lt;absolute-ordering&gt;`元素直接指定了哪些JAR包需要扫描`web fragment`,如果`&lt;absolute-ordering/&gt;`元素是空的, 则表示不需要扫描,像下面这样。
<img src="https://static001.geekbang.org/resource/image/ff/8f/ff715ef5e61959bb8a17abd15681fc8f.jpg" alt="">
## 随机数熵源优化
这是一个比较有名的问题。Tomcat 7以上的版本依赖Java的SecureRandom类来生成随机数比如Session ID。而JVM 默认使用阻塞式熵源(`/dev/random` 在某些情况下就会导致Tomcat启动变慢。当阻塞时间较长时 你会看到这样一条警告日志:
>
<p>`&lt;DATE&gt;` org.apache.catalina.util.SessionIdGenerator createSecureRandom<br>
INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [8152] milliseconds.</p>
这其中的原理我就不展开了,你可以阅读[资料](https://stackoverflow.com/questions/28201794/slow-startup-on-tomcat-7-0-57-because-of-securerandom)获得更多信息。解决方案是通过设置让JVM使用非阻塞式的熵源。
我们可以设置JVM的参数
```
-Djava.security.egd=file:/dev/./urandom
```
或者是设置`java.security`文件,位于`$JAVA_HOME/jre/lib/security`目录之下: `securerandom.source=file:/dev/./urandom`
这里请你注意,`/dev/./urandom`中间有个`./`的原因是Oracle JRE中的BugJava 8里面的 SecureRandom类已经修正这个Bug。 阻塞式的熵源(`/dev/random`)安全性较高, 非阻塞式的熵源(`/dev/./urandom`)安全性会低一些,因为如果你对随机数的要求比较高, 可以考虑使用硬件方式生成熵源。
## 并行启动多个Web应用
Tomcat启动的时候默认情况下Web应用都是一个一个启动的等所有Web应用全部启动完成Tomcat才算启动完毕。如果在一个Tomcat下你有多个Web应用为了优化启动速度你可以配置多个应用程序并行启动可以通过修改`server.xml`中Host元素的startStopThreads属性来完成。startStopThreads的值表示你想用多少个线程来启动你的Web应用如果设成0表示你要并行启动Web应用像下面这样的配置。
<img src="https://static001.geekbang.org/resource/image/50/ed/50e0c10a38c5e4a5e7a28ca1885698ed.jpg" alt="">
这里需要注意的是Engine元素里也配置了这个参数这意味着如果你的Tomcat配置了多个Host虚拟主机Tomcat会以并行的方式启动多个Host。
## 本期精华
今天我讲了不少提高优化Tomcat启动速度的小贴士现在你就可以把它们用在项目中了。不管是在开发环境还是生产环境你都可以打开Tomcat的启动日志看看目前你们的应用启动需要多长时间然后尝试去调优再看看Tomcat的启动速度快了多少。
如果你是用嵌入式的方式运行Tomcat比如Spring Boot你也可以通过Spring Boot的方式去修改Tomcat的参数调优的原理都是一样的。
## 课后思考
在Tomcat启动速度优化上你都遇到了哪些问题或者你还有自己的“独门秘籍”欢迎把它们分享给我和其他同学。
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,76 @@
<audio id="audio" title="13 | 热点问题答疑1如何学习源码" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/a2/beea7e78875c4dd8e30fc1ee337e72a2.mp3"></audio>
不知道你有没有留意到,不少高端开发岗位在招聘要求里往往会写这么一条:研究过框架和中间件源码的优先考虑。这是因为一切秘密都藏在源码之中,阅读源码会让我们对框架或者中间件的理解更加深刻。有时候即使你阅读了大量原理性的文档,但如果不看源码,可能仍然会觉得还没有理解透。另外如果你能深入源码,招聘者从侧面也能感觉到你的学习热情和探索精神。
今天我们就来聊聊源码学习这个话题。对于Java后端开发来说有不少经典的开源框架和中间件下面我帮你按照后端的分层架构整理出来供你参考。
- **服务接入层**反向代理NginxAPI网关Node.js。
- **业务逻辑层**Web容器Tomcat、Jetty应用层框架Spring、Spring MVC和Spring BootORM框架MyBatis
- **数据缓存层**内存数据库Redis消息中间件Kafka。
- **数据存储层**关系型数据库MySQL非关系型数据库MongoDB文件存储HDFS搜索分析引擎Elasticsearch。
这其中每一层都要支持水平扩展和高可用比如业务层普遍采用微服务架构微服务之间需要互相调用于是就出现了RPC框架Spring Cloud和Dubbo。
除此之外还有两个非常重要的基础组件Netty和ZooKeeper其中Netty用于网络通信ZooKeeper用于分布式协调。其实很多中间件都用到了这两个基础组件并且ZooKeeper的网络通信模块也是通过Netty来实现的。
而这些框架或者中间件并不是凭空产生的,它们是在互联网的演化过程中,为了解决各种具体业务的痛点,一点一点积累进化而来的。很多时候我们把这些“零件”按照成熟的模式组装在一起,就能搭建出一个互联网后台系统。一般来说大厂都会对这些框架或者中间件进行改造,或者完全靠自己来实现。这就对后台程序员提出了更高的要求。
那这么多中间件和框架从哪里入手呢先学哪个后学哪个呢我觉得可以先学一些你熟悉的或者相对来说比较简单的树立起信心后再学复杂的。比如可以先学Tomcat、Jetty和Spring核心容器弄懂了这些以后再扩展到Spring的其他组件。
在这个过程中,我们就会积累一些通用的技术,比如网络编程、多线程、反射和类加载技术等,这些通用的技术在不少中间件和框架中会用到。
先说**网络通信**在分布式环境下信息要在各个实体之间流动到处都是网络通信的场景比如浏览器要将HTTP请求发给Web容器一个微服务要调用另一个微服务Web应用读写缓存服务器、消息队列或者数据库等都需要网络通信。
尽管网络通信的场景很多,但无外乎都要考虑这么几个问题:
- I/O模型同步还是异步是阻塞还是非阻塞
- 通信协议是二进制gRPC还是文本HTTP
- 数据怎么序列化是JSON还是Protocol Buffer
此外服务端的线程模型也是一个重点。我们知道多线程可以把要做的事情“并行化”提高并发度和吞吐量但是线程可能会阻塞一旦阻塞线程资源就闲置了并且会有线程上下文切换的开销浪费CPU资源。而有些任务执行会发生阻塞有些则不会阻塞因此线程模型就是要决定哪几件事情放到一个线程来做哪几件事情放到另一个线程来做并设置合理的线程数量目的就是要让CPU忙起来并且不是白忙活也就是不做无用功。
我们知道服务端处理一个网络连接的过程是:
accept、select、read、decode、process、encode、send。
一般来说服务端程序有几个角色Acceptor、Selector和Processor。
- Acceptor负责接收新连接也就是accept
- Selector负责检测连接上的I/O事件也就是select
- Processor负责数据读写、编解码和业务处理也就是read、decode、process、encode、send。
Acceptor在接收连接时可能会阻塞为了不耽误其他工作一般跑在单独的线程里而Selector在侦测I/O事件时也可能阻塞但是它一次可以检测多个Channel连接其实就是用阻塞它一个来换取大量业务线程的不阻塞那Selector检测I/O事件到了是用同一个线程来执行Processor还是另一个线程来执行呢不同的场景又有相应的策略。
比如Netty通过EventLoop将Selector和Processor跑在同一个线程。一个EventLoop绑定了一个线程并且持有一个Selector。而Processor的处理过程被封装成一个个任务一个EventLoop负责处理多个Channel上的所有任务而一个Channel只能由一个EventLoop来处理这就保证了任务执行的线程安全并且用同一个线程来侦测I/O事件和读写数据可以充分利用CPU缓存。我们通过一张图来理解一下
<img src="https://static001.geekbang.org/resource/image/f6/f4/f6741dc09985d5e08934ad77cd8c96f4.png" alt="">
请你注意这要求Processor中的任务能在短时间完成否则会阻塞这个EventLoop上其他Channel的处理。因此在Netty中可以设置业务处理和I/O处理的时间比率超过这个比率则将任务扔到专门的业务线程池来执行这一点跟Jetty的EatWhatYouKill线程策略有异曲同工之妙。
而Kafka把Selector和Processor跑在不同的线程里因为Kafka的业务逻辑大多涉及与磁盘读写处理时间不确定所以Kafka有专门的业务处理线程池来运行Processor。与此类似Tomcat也采用了这样的策略同样我们还是通过一张图来理解一下。
<img src="https://static001.geekbang.org/resource/image/74/e4/74f7742142740ae55bcda997a73a37e4.png" alt="">
我们再来看看**Java反射机制**几乎所有的框架都用到了反射和类加载技术这是为了保证框架的通用性需要根据配置文件在运行时加载不同的类并调用其方法。比如Web容器Tomcat和Jetty通过反射来加载Servlet、Filter和Listener而Spring的两大核心功能IOC和AOP都用到了反射技术再比如MyBatis将数据从数据库读出后也是通过反射机制来创建Java对象并设置对象的值。
因此你会发现通过学习一个中间件熟悉了这些通用的技术以后再学习其他的中间件或者框架就容易多了。比如学透了Tomcat的I/O线程模型以及高并发高性能设计思路再学Netty的源码就轻车熟路了Tomcat的组件化设计和类加载机制理解透彻了再学Spring容器的源码就会轻松很多。
接下来我再来聊聊具体如何学习源码,有很多同学在专栏里问这个问题,我在专栏的留言中也提到过,但我觉得有必要展开详细讲讲我是如何学习源码的。
学习的第一步首先我们要弄清楚中间件的核心功能是什么我以专栏所讲的Tomcat为例。Tomcat的核心功能是HTTP服务器和Servlet容器因此就抓住请求处理这条线通过什么样的方式接收连接接收到连接后以什么样的方式来读取数据读到数据后怎么解析数据HTTP协议请求数据解析出来后怎么调用Servlet容器Servlet容器又怎么调到Spring中的业务代码。
为了完成这些功能Tomcat中有一些起骨架作用的核心类其他类都是在这个骨架上进行扩展或补充细节来实现。因此在学习前期就要紧紧抓住这些类先不要深入到其他细节你可以先画出一张骨架类图。
<img src="https://static001.geekbang.org/resource/image/12/9b/12ad9ddc3ff73e0aacf2276bcfafae9b.png" alt="">
在此之后我们还需要将源码跑起来打打断点看看变量的值和调用栈。我建议用内嵌式的方式来启动和调试Tomcat体会一下Spring Boot是如何使用Tomcat的这里有[示例源码](https://github.com/heroku/devcenter-embedded-tomcat)。在源码阅读过程中要充分利用IDE的功能比如通过快捷键查找某个接口的所有实现类、查找某个类或者函数在哪些地方被用到。
我们还要带着问题去学习源码比如你想弄清楚Tomcat如何启停、类加载器是如何设计的、Spring Boot是如何启动Tomcat的、Jetty是如何通过Handler链实现高度定制化的如果要你来设计这些功能会怎么做呢带着这些问题去分析相关的源码效率会更高同时你在寻找答案的过程中也会碰到更多问题等你把这些问题都弄清楚了你获得的不仅仅是知识更重要的是你会树立起攻克难关的信心。同时我还建议在你弄清楚一些细节后要及时记录下来画画流程图或者类图再加上一些关键备注以防遗忘。
当然在这个过程中,你还可以看看产品的官方文档,熟悉一下大概的设计思路。在遇到难题时,你还可以看看网上的博客,参考一下别人的分析。但最终还是需要你自己去实践和摸索,因为网上的分析也不一定对,只有你自己看了源码后才能真正理解它,印象才更加深刻。
今天说了这么多,就是想告诉你如果理解透彻一两个中间件,有了一定的积累,这时再来学一个新的系统,往往你只需要瞧上几眼,就能明白它所用的架构,而且你会自然联想到系统存在哪些角色,以及角色之间的关系,包括静态的依赖关系和动态的协作关系,甚至你会不由自主带着审视的眼光,来发现一些可以改进的地方。如果你现在就是这样的状态,那么恭喜你,你的技术水平已经成长到一个新的层面了。
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。