Skip to content

Commit 254e26a

Browse files
committed
优化内容
1 parent 53ed462 commit 254e26a

7 files changed

Lines changed: 1091 additions & 42 deletions
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
> 通过startup.sh启动Tomcat后会发生什么呢?
2+
3+
![](https://img-blog.csdnimg.cn/20210720163627669.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)
4+
1. Tomcat也是Java程序,因此startup.sh脚本会启动一个JVM运行Tomcat的启动类Bootstrap
5+
2. Bootstrap主要负责初始化Tomcat的类加载器,并创建Catalina
6+
3. Catalina是个启动类,解析server.xml、创建相应组件,并调用Server#start
7+
4. Server组件负责管理Service组件,会调用Service#start
8+
5. Service组件负责管理连接器和顶层容器Engine,因此会调用连接器和Engine的start()
9+
10+
这些启动类或组件不处理具体的请求,它们主要是“管理”,管理下层组件的生命周期,并给下层组件分配任务,即路由请求到应负责的组件。
11+
# Catalina
12+
主要负责创建Server,并非直接new个Server实例就完事了,而是:
13+
- 解析server.xml,将里面配的各种组件创建出来
14+
- 接着调用Server组件的init、start方法,这样整个Tomcat就启动起来了
15+
16+
Catalina还需要处理各种“异常”,比如当通过“Ctrl + C”关闭Tomcat时,
17+
18+
> Tomcat会如何优雅停止并清理资源呢?
19+
20+
因此Catalina在JVM中注册一个 **关闭钩子**
21+
```java
22+
public void start() {
23+
// 1. 如果持有的Server实例为空,就解析server.xml创建出来
24+
if (getServer() == null) {
25+
load();
26+
}
27+
// 2. 如果创建失败,报错退出
28+
if (getServer() == null) {
29+
log.fatal(sm.getString("catalina.noServer"));
30+
return;
31+
}
32+
33+
// 3.启动Server
34+
try {
35+
getServer().start();
36+
} catch (LifecycleException e) {
37+
return;
38+
}
39+
40+
// 创建并注册关闭钩子
41+
if (useShutdownHook) {
42+
if (shutdownHook == null) {
43+
shutdownHook = new CatalinaShutdownHook();
44+
}
45+
Runtime.getRuntime().addShutdownHook(shutdownHook);
46+
}
47+
48+
// 监听停止请求
49+
if (await) {
50+
await();
51+
stop();
52+
}
53+
}
54+
```
55+
## 关闭钩子
56+
若需在JVM关闭时做一些清理,比如:
57+
- 将缓存数据刷盘
58+
- 清理一些临时文件
59+
60+
就可以向JVM注册一个关闭钩子,其实就是个线程,JVM在停止之前会尝试执行该线程的run()。
61+
62+
Tomcat的**关闭钩子** 就是CatalinaShutdownHook:
63+
![](https://img-blog.csdnimg.cn/eeb4c65673154a25876ec027c1f488df.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)
64+
65+
Tomcat的“关闭钩子”实际上就执行了Server#stop,会释放和清理所有资源。
66+
# Server组件
67+
Server组件具体实现类StandardServer。
68+
69+
Server继承了LifecycleBase,它的生命周期被统一管理
70+
![](https://img-blog.csdnimg.cn/20210720165154608.png)
71+
它的子组件是Service,因此它还需要管理Service的生命周期,即在启动时调用Service组件的启动方法,在停止时调用它们的停止方法。Server在内部维护了若干Service组件,它是以数组来保存的,那Server是如何添加一个Service到数组中的呢?
72+
```java
73+
@Override
74+
public void addService(Service service) {
75+
76+
service.setServer(this);
77+
78+
synchronized (servicesLock) {
79+
// 长度+1的数组并没有一开始就分配一个很长的数组
80+
// 而是在添加的过程中动态地扩展数组长度,当添加一个新的Service实例时
81+
// 会创建一个新数组并把原来数组内容复制到新数组,节省内存
82+
Service results[] = new Service[services.length + 1];
83+
84+
// 复制老数据
85+
System.arraycopy(services, 0, results, 0, services.length);
86+
results[services.length] = service;
87+
services = results;
88+
89+
// 启动Service组件
90+
if (getState().isAvailable()) {
91+
try {
92+
service.start();
93+
} catch (LifecycleException e) {
94+
// Ignore
95+
}
96+
}
97+
98+
// 触发监听事件
99+
support.firePropertyChange("service", null, service);
100+
}
101+
102+
}
103+
```
104+
105+
Server组件还需要启动一个Socket来监听停止端口,所以才能通过shutdown命令关闭Tomcat。
106+
上面Catalina的启动方法最后一行代码就是调用Server#await。
107+
108+
在await方法里会创建一个Socket监听8005端口,并在一个死循环里接收Socket上的连接请求,如果有新的连接到来就建立连接,然后从Socket中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入stop流程。
109+
110+
# Service组件
111+
Service组件的具体实现类StandardService
112+
```java
113+
public class StandardService extends LifecycleBase implements Service {
114+
//名字
115+
private String name = null;
116+
117+
//Server实例
118+
private Server server = null;
119+
120+
//连接器数组
121+
protected Connector connectors[] = new Connector[0];
122+
private final Object connectorsLock = new Object();
123+
124+
//对应的Engine容器
125+
private Engine engine = null;
126+
127+
//映射器及其监听器
128+
protected final Mapper mapper = new Mapper();
129+
protected final MapperListener mapperListener = new MapperListener(this);
130+
```
131+
132+
StandardService继承了LifecycleBase抽象类,此外StandardService中还有一些我们熟悉的组件,比如ServerConnectorEngineMapper
133+
134+
Tomcat支持热部署,当Web应用的部署发生变化,Mapper中的映射信息也要跟着变化,MapperListener就是监听器,监听容器的变化,并把信息更新到Mapper
135+
136+
## Service启动方法
137+
```java
138+
protected void startInternal() throws LifecycleException {
139+
140+
// 1. 触发启动监听器
141+
setState(LifecycleState.STARTING);
142+
143+
// 2. 先启动Engine,Engine会启动它子容器
144+
if (engine != null) {
145+
synchronized (engine) {
146+
engine.start();
147+
}
148+
}
149+
150+
// 3. 再启动Mapper监听器
151+
mapperListener.start();
152+
153+
// 4.最后启动连接器,连接器会启动它子组件,比如Endpoint
154+
synchronized (connectorsLock) {
155+
for (Connector connector: connectors) {
156+
if (connector.getState() != LifecycleState.FAILED) {
157+
connector.start();
158+
}
159+
}
160+
}
161+
}
162+
```
163+
Service先后启动EngineMapper监听器、连接器。
164+
内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而Mapper也依赖容器组件,容器组件启动好了才能监听它们的变化,因此MapperMapperListener在容器组件之后启动。
165+
# Engine组件
166+
最后我们再来看看顶层的容器组件Engine具体是如何实现的。Engine本质是一个容器,因此它继承了ContainerBase基类,并且实现了Engine接口。
167+
168+
```java
169+
public class StandardEngine extends ContainerBase implements Engine {
170+
}
171+
```
172+
Engine的子容器是Host,所以它持有了一个Host容器的数组,这些功能都被抽象到了ContainerBaseContainerBase中有这样一个数据结构:
173+
```java
174+
protected final HashMap<String, Container> children = new HashMap<>();
175+
```
176+
ContainerBaseHashMap保存了它的子容器,并且ContainerBase还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如ContainerBase会用专门的线程池来启动子容器。
177+
```java
178+
for (int i = 0; i < children.length; i++) {
179+
results.add(startStopExecutor.submit(new StartChild(children[i])));
180+
}
181+
```
182+
所以Engine在启动Host子容器时就直接重用了这个方法。
183+
## Engine自己做了什么?
184+
容器组件最重要的功能是处理请求,而Engine容器对请求的“处理”,其实就是把请求转发给某一个Host子容器来处理,具体是通过Valve来实现的。
185+
186+
每个容器组件都有一个Pipeline,而Pipeline中有一个基础阀(Basic Valve)。
187+
Engine容器的基础阀定义如下:
188+
189+
```java
190+
final class StandardEngineValve extends ValveBase {
191+
192+
public final void invoke(Request request, Response response)
193+
throws IOException, ServletException {
194+
195+
// 拿到请求中的Host容器
196+
Host host = request.getHost();
197+
if (host == null) {
198+
return;
199+
}
200+
201+
// 调用Host容器中的Pipeline中的第一个Valve
202+
host.getPipeline().getFirst().invoke(request, response);
203+
}
204+
205+
}
206+
```
207+
把请求转发到Host容器。
208+
处理请求的Host容器对象是从请求中拿到的,请求对象中怎么会有Host容器?
209+
因为请求到达Engine容器前,Mapper组件已对请求进行路由处理,Mapper组件通过请求URL定位了相应的容器,并且把容器对象保存到请求对象。
210+
211+
所以当我们在设计这样的组件时,需考虑:
212+
- 用合适的数据结构来保存子组件,比如
213+
Server用数组来保存Service组件,并且采取动态扩容的方式,这是因为数组结构简单,占用内存小
214+
ContainerBaseHashMap来保存子容器,虽然Map占用内存会多一点,但是可以通过Map来快速的查找子容器
215+
- 根据子组件依赖关系来决定它们的启动和停止顺序,以及如何优雅的停止,防止异常情况下的资源泄漏。
216+
217+
# 总结
218+
- Server 组件, 实现类 StandServer
219+
- 继承了 LifeCycleBase
220+
- 子组件是 Service, 需要管理其生命周期(调用其 LifeCycle 的方法), 用数组保存多个 Service 组件, 动态扩容数组来添加组件
221+
- 启动一个 socket Listen停止端口, Catalina 启动时, 调用 Server await 方法, 其创建 socket Listen 8005 端口, 并在死循环中等连接, 检查到 shutdown 命令, 调用 stop 方法
222+
- Service 组件, 实现类 StandService
223+
- 包含 Server, Connector, Engine 和 Mapper 组件的成员变量
224+
- 还包含 MapperListener 成员变量, 以支持热部署, 其Listen容器变化, 并更新 Mapper, 是观察者模式
225+
- 需注意各组件启动顺序, 根据其依赖关系确定
226+
- 先启动 Engine, 再启动 Mapper Listener, 最后启动连接器, 而停止顺序相反.
227+
- Engine 组件, 实现类 StandEngine 继承 ContainerBase
228+
- ContainerBase 实现了维护子组件的逻辑, 用 HaspMap 保存子组件, 因此各层容器可重用逻辑
229+
- ContainerBase 用专门线程池启动子容器, 并负责子组件启动/停止, "增删改查"
230+
- 请求到达 Engine 之前, Mapper 通过 URL 定位了容器, 并存入 Request 中. Engine 从 Request 取出 Host 子容器, 并调用其 pipeline 的第一个 valve
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
OOP三大特性最重要的:多态。
2+
3+
很多程序员虽然在用支持OOP的语言,但却从未用过多态。
4+
5+
- 只使用封装、继承的编程方式,称为基于对象(Object Based)编程
6+
- 只有加入多态,才能称为OOP
7+
没写过多态,就是没写过OO代码。
8+
9+
正是有了多态,软件设计才有更大弹性,更好拥抱变化。
10+
# 如何理解多态?
11+
多态,即一个接口,多种形态。
12+
13+
一个draw方法,以正方形调用,则画正方形;以圆形调用,则画圆形:
14+
![](https://img-blog.csdnimg.cn/26163eccb42440899cbf633925961e08.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)
15+
继承的两种方式之一的实现继承,请尽可能用组合替代。而接口继承,主要是给多态用的。
16+
17+
因为重点在于继承体系的使用者,主要考虑父类,而非子类。
18+
如下代码段,不必考虑具体形状是啥,仅需调用它的draw方法
19+
![](https://img-blog.csdnimg.cn/986e35f6e6134ec897c497265a6b23f5.png)
20+
优势在于,一旦有新变化,比如将正方形换成圆,除了变量初始化,其它代码不需要动。
21+
22+
> 既然多态这么好,为什么很多人感觉无法在项目中自如地多态?
23+
24+
多态需构建抽象。
25+
26+
# 构建抽象
27+
找出不同事物的共同点,这是最具挑战的。令人懵逼的也往往是眼中的不同之处。在很多人眼里,鸡就是鸡,鸭就是鸭。
28+
29+
寻找共同点,根基还是**分离关注点**
30+
当你能看出鸡、鸭都有羽毛,都养在家里,你才可能识别“家禽”。
31+
32+
> 构建出的抽象会以接口(此处接口不一定是个语法,而是一个类型的约束)体现。所以,本文讨论的多态范畴内,接口、抽象类、父类等概念等价,统一称为接口。
33+
34+
## 接口的意义
35+
### 接口隔离了变化部分、不变部分
36+
- 不变部分
37+
接口的约定
38+
- 变化部分
39+
子类各自的实现
40+
41+
最影响程序的就是各种变化。有时需求来了,你的代码就得跟着改,一个可能的原因就是各种代码混在了一起。
42+
比如,一个通信协议的调整,你要改业务逻辑,这明显不合理。
43+
所以识别出变化与不变,是区分程序员水平的一大标准。
44+
45+
### 接口是边界
46+
清晰界定系统内不同模块的职责很关键,而模块间彼此通信最重要的就是通信协议,对应到代码中的接口。
47+
48+
很多程序员在接口中添加方法很随意,因为他们眼里,不存在实现者和使用者的角色差异,导致没有清晰边界,后果就是模块定义随意,彼此之间互相耦合,最终玩死自己。
49+
50+
所以,理解多态在于理解接口,理解接口在于谨慎选择接口中的方法。
51+
52+
面向接口编程的价值就源于多态。
53+
54+
这些原则你可能都听说过,但写代码时,就会忽略细节。
55+
比如:
56+
![](https://img-blog.csdnimg.cn/491f7d59c37849fb8790c5a3e42edf3d.png)
57+
这显然没有面向接口编程,推荐写法:
58+
![](https://img-blog.csdnimg.cn/855dad01a2bb4dc08f4a2cb9b7d6474b.png)
59+
差别就在于变量类型,是面向一个接口,还是面向一个具体实现类。
60+
61+
多态对程序员的要求更高,需要你能感知未来变化!
62+
63+
# 实现多态
64+
**OOP会限制使用函数指针,它是对程序控制权的间接转移施加了约束。**
65+
理解这句话,就要理解多态如何实现的。
66+
67+
Linux文件系统用C实现了OOP,就是用了函数指针:
68+
![](https://img-blog.csdnimg.cn/140c9d5229104bd2b859041322e29283.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)
69+
70+
即可这样赋值:
71+
![](https://img-blog.csdnimg.cn/d58e3218f6934aea98b1958a7abc2094.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)
72+
给该结构体赋不同值,就能实现不同文件系统。
73+
但这样非常不安全。既然是个结构体字段,就可能改写它:
74+
![](https://img-blog.csdnimg.cn/28209b781f58425a80d0dc49cd5e391b.png)
75+
本该在hellofs_read运行的代码,跑进了sillyfs_read,程序崩溃。对于C这种灵活语言,你无法禁止这种操作,只能靠人为规定和代码检查。
76+
77+
到了OOP 语言,这种做法由一种编程结构变成一种语法。给函数指针赋值的操作下沉到了运行时去实现。运行时的实现,就是个查表过程:
78+
![](https://img-blog.csdnimg.cn/deb97483fcb544aa94513061a2f97ff7.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16)
79+
一个类在编译时,会给其中的函数在虚拟函数表中找个位置,把函数指针地址写进去,不同子类对应不同虚拟表。
80+
当用接口去调用对应函数时,实际上完成的就是在对应虚拟函数表的一个偏移,不管现在面对哪个子类,都可找到相应实现函数。
81+
82+
C++这种注重运行时消耗的语言:
83+
- 只有virtual函数会出现在虚拟函数表
84+
- 普通函数就是直接的函数调用,以此减少消耗
85+
86+
对于Java程序员,可通过给无需改写的方法添加final帮助运行时优化。
87+
88+
当多态成为语法,就限制了函数指针的使用,犯错率大大降低!
89+
90+
# 没有继承的多态
91+
封装,多态。至于继承,却不是必然选项。只要能够遵循相同接口,即可表现出多态,所以,多态并不一定要依赖继承。
92+
93+
动态语言中一个常见说法 - Duck Typing,若走起来像鸭子,叫起来像鸭子,那它就是鸭子。
94+
两个类可不在同一继承体系下,但只要有相同接口,就是一种多态。
95+
96+
如下代码段:Duck和FakeDuck不在一棵继承树上,但make_quack调用时,它们俩都可传进去。
97+
![](https://img-blog.csdnimg.cn/5ead17c395794be4a5123eba5095ff47.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_10,color_FFFFFF,t_70,g_se,x_16)
98+
99+
很多软件都有插件能力,而插件结构本身就是多态。
100+
比如,著名的开源图形处理软件GIMP,它自身是用C开发的,为它编写插件就需要按照它规定的结构去编写代码:
101+
102+
```c
103+
struct GimpPlugInInfo
104+
{
105+
/* GIMP 应用初始启动时调用 */
106+
GimpInitProc init_proc;
107+
108+
/* GIMP 应用退出时调用 */
109+
GimpQuitProc quit_proc;
110+
111+
/* GIMP 查询插件能力时调用 */
112+
GimpQueryProc query_proc;
113+
114+
/* 插件安装之后,开始运行时调用*/
115+
GimpRunProc run_proc;
116+
};
117+
```
118+
119+
我们所需做的就是按照这个结构声明出PLUG_IN_INFO,这是隐藏的名字,将插件的能力注册给GIMP这个应用:
120+
121+
```c
122+
GimpPlugInInfo PLUG_IN_INFO = {
123+
init,
124+
quit,
125+
query,
126+
run
127+
};
128+
```
129+
这里用的C语言,但依然能表现多态。
130+
131+
多态依赖于继承,这只是某些程序设计语言自身的特点。在面向对象本身的体系中,封装和多态才是重中之重,而继承则很尴尬。
132+
133+
**一定要跳出单一语言的局限,这样,才能对各种编程思想有更本质的认识。**
134+
135+
OOP三大特点的地位:
136+
- 封装是面向对象的根基,软件就是靠各种封装好的对象逐步组合出来的
137+
- 继承给了继承体系内的所有对象一个约束,让它们有了统一的行为
138+
- 多态让整个体系能够更好地应对未来的变化。
139+
140+
# FAQ
141+
某系统需要对普通用户增删改查,后来加了超级管理员用户也需要增删改查。把用户的操作抽象成接口方法,让普通用户和管理员用户实现接口方法…… 那么问题来了,这些接口方法的出入参没法完全共用,比如查询用户信息接口,普通用户和超级管理员用户的返回体信息字段不同。所以没法抽象,请问一下老师这种应不应该抽象呢?如果应该做成抽象需要怎么分离变的部分呢
142+
143+
应该分,因为管理员和普通用户的关注点是不同的。管理员和普通用户可以分别提供接口,分别提供相应的内容。
144+
如果说非要二者共用,可以考虑在服务层共用,在接口层面分开,在接口层去适配不同的接口。
145+
# 总结
146+
多态是基于对象和面向对象的分水岭。多态就是接口一样,实现不同。
147+
**建立起恰当抽象,面向接口编程。**

0 commit comments

Comments
 (0)