Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 254e26a

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

7 files changed

+1091
-42
lines changed
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容器的数组,这些功能都被抽象到了ContainerBase,ContainerBase中有这样一个数据结构:
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)

AltStyle によって変換されたページ (->オリジナル) /