开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

重学 Java 设计模式:实战状态模式「模拟系统营销活动,状

发表于 2020-07-03

作者:小傅哥

博客:bugstack.cn - 原创系列专题文章

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

写好代码三个关键点

如果把写代码想象成家里的软装,你肯定会想到家里需要有一个非常不错格局最好是南北通透的,买回来的家具最好是品牌保证质量的,之后呢是大小合适,不能摆放完了看着别扭。那么把这一过程抽象成写代码就是需要三个核心的关键点;架构(房间的格局)、命名(品牌和质量)、注释(尺寸大小说明书),只有这三个点都做好才能完成出一套赏心悦目的家。

平原走码🐎易放难收

上学期间你写了多少代码?上班一年你能写多少代码?回家自己学习写了多少代码?个人素养的技术栈地基都是一块一块砖码出来的,写的越广越深,根基就越牢固。当根基牢固了以后在再上层建设就变得迎刃而解了,也更容易建设了。往往最难的就是一层一层阶段的突破,突破就像破壳一样,也像夯实地基,短时间看不到成绩,也看不出高度。但以后谁能走的稳,就靠着默默的沉淀。

技术传承的重要性

可能是现在时间节奏太快,一个需求下来恨不得当天就上线(这个需求很简单,怎么实现我不管,明天上线!),导致团队的人都很慌、很急、很累、很崩溃,最终反反复复的人员更替,项目在这个过程中也交接了N次,文档不全、代码混乱、错综复杂,谁在后面接手也都只能修修补补,就像烂尾楼。这个没有传承、没有沉淀的项目,很难跟随业务的发展。最终!根基不牢,一地鸡毛。

二、开发环境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三个,可以通过关注公众号:bugstack虫洞栈,回复源码下载获取(打开获取的链接,找到序号18)
工程 描述
itstack-demo-design-19-00 场景模拟工程;模拟营销活动操作服务(查询、审核)
itstack-demo-design-19-01 使用一坨代码实现业务需求
itstack-demo-design-19-02 通过设计模式优化改造代码,产生对比性从而学习

三、状态模式介绍

状态模式,图片来自 refactoringguru.cn

状态模式描述的是一个行为下的多种状态变更,比如我们最常见的一个网站的页面,在你登录与不登录下展示的内容是略有差异的(不登录不能展示个人信息),而这种登录与不登录就是我们通过改变状态,而让整个行为发生了变化。

收音机&放音机&磁带机

至少80后、90后的小伙伴基本都用过这种磁带放音机(可能没有这个好看),它的上面是一排按钮,当放入磁带后,通过上面的按钮就可以让放音机播放磁带上的内容(listen to 英语听力考试),而且有些按钮是互斥的,当在某个状态下才可以按另外的按钮(这在设计模式里也是一个关键的点)。

四、案例场景模拟

场景模拟;营销活动审核状态流转

在本案例中我们模拟营销活动审核状态流转场景(一个活动的上线是多个层级审核上线的)

在上图中也可以看到我们的流程节点中包括了各个状态到下一个状态扭转的关联条件,比如;审核通过才能到活动中,而不能从编辑中直接到活动中,而这些状态的转变就是我们要完成的场景处理。

大部分程序员基本都开发过类似的业务场景,需要对活动或者一些配置需要审核后才能对外发布,而这个审核的过程往往会随着系统的重要程度而设立多级控制,来保证一个活动可以安全上线,避免造成资损。

当然有时候会用到一些审批流的过程配置,也是非常方便开发类似的流程的,也可以在配置中设定某个节点的审批人员。但这不是我们主要体现的点,在本案例中我们主要是模拟学习对一个活动的多个状态节点的审核控制。

1. 场景模拟工程

1
2
3
4
5
6
7
8
复制代码itstack-demo-design-19-00
└── src
└── main
└── java
└── org.itstack.demo.design
├── ActivityInfo.java
├── Status.java
└── ActivityService.java
  • 在这个模拟工程里我们提供了三个类,包括;状态枚举(Status)、活动对象(ActivityInfo)、活动服务(ActivityService),三个服务类。
  • 接下来我们就分别介绍三个类包括的内容。

2. 代码实现

2.1 基本活动信息

1
2
3
4
5
6
7
8
9
10
复制代码public class ActivityInfo {

private String activityId; // 活动ID
private String activityName; // 活动名称
private Enum<Status> status; // 活动状态
private Date beginTime; // 开始时间
private Date endTime; // 结束时间

// ...get/set
}
  • 一些基本的活动信息;活动ID、活动名称、活动状态、开始时间、结束时间。

2.2 活动枚举状态

1
2
3
4
5
6
复制代码public enum Status {

// 1创建编辑、2待审核、3审核通过(任务扫描成活动中)、4审核拒绝(可以撤审到编辑状态)、5活动中、6活动关闭、7活动开启(任务扫描成活动中)
Editing, Check, Pass, Refuse, Doing, Close, Open

}
  • 活动的枚举;1创建编辑、2待审核、3审核通过(任务扫描成活动中)、4审核拒绝(可以撤审到编辑状态)、5活动中、6活动关闭、7活动开启(任务扫描成活动中)

2.3 活动服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
复制代码public class ActivityService {

private static Map<String, Enum<Status>> statusMap = new ConcurrentHashMap<String, Enum<Status>>();

public static void init(String activityId, Enum<Status> status) {
// 模拟查询活动信息
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.setActivityId(activityId);
activityInfo.setActivityName("早起学习打卡领奖活动");
activityInfo.setStatus(status);
activityInfo.setBeginTime(new Date());
activityInfo.setEndTime(new Date());
statusMap.put(activityId, status);
}

/**
* 查询活动信息
*
* @param activityId 活动ID
* @return 查询结果
*/
public static ActivityInfo queryActivityInfo(String activityId) {
// 模拟查询活动信息
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.setActivityId(activityId);
activityInfo.setActivityName("早起学习打卡领奖活动");
activityInfo.setStatus(statusMap.get(activityId));
activityInfo.setBeginTime(new Date());
activityInfo.setEndTime(new Date());
return activityInfo;
}

/**
* 查询活动状态
*
* @param activityId 活动ID
* @return 查询结果
*/
public static Enum<Status> queryActivityStatus(String activityId) {
return statusMap.get(activityId);
}

/**
* 执行状态变更
*
* @param activityId 活动ID
* @param beforeStatus 变更前状态
* @param afterStatus 变更后状态 b
*/
public static synchronized void execStatus(String activityId, Enum<Status> beforeStatus, Enum<Status> afterStatus) {
if (!beforeStatus.equals(statusMap.get(activityId))) return;
statusMap.put(activityId, afterStatus);
}

}
  • 在这个静态类中提供了活动的查询和状态变更接口;queryActivityInfo、queryActivityStatus、execStatus。
  • 同时使用Map的结构来记录活动ID和状态变化信息,另外还有init方法来初始化活动数据。实际的开发中这类信息基本都是从数据库或者Redis中获取。

五、用一坨坨代码实现

这里我们先使用最粗暴的方式来实现功能

对于这样各种状态的变更,最让我们直接想到的就是使用if和else进行判断处理。每一个状态可以流转到下一个什么状态,都可以使用嵌套的if实现。

1. 工程结构

1
2
3
4
5
6
7
复制代码itstack-demo-design-19-01
└── src
└── main
└── java
└── org.itstack.demo.design
├── ActivityExecStatusController.java
└── Result.java
  • 整个实现的工程结构比较简单,只包括了两个类;ActivityExecStatusController、Result,一个是处理流程状态,另外一个是返回的对象。

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
复制代码public class ActivityExecStatusController {

/**
* 活动状态变更
* 1. 编辑中 -> 提审、关闭
* 2. 审核通过 -> 拒绝、关闭、活动中
* 3. 审核拒绝 -> 撤审、关闭
* 4. 活动中 -> 关闭
* 5. 活动关闭 -> 开启
* 6. 活动开启 -> 关闭
*
* @param activityId 活动ID
* @param beforeStatus 变更前状态
* @param afterStatus 变更后状态
* @return 返回结果
*/
public Result execStatus(String activityId, Enum<Status> beforeStatus, Enum<Status> afterStatus) {

// 1. 编辑中 -> 提审、关闭
if (Status.Editing.equals(beforeStatus)) {
if (Status.Check.equals(afterStatus) || Status.Close.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

// 2. 审核通过 -> 拒绝、关闭、活动中
if (Status.Pass.equals(beforeStatus)) {
if (Status.Refuse.equals(afterStatus) || Status.Doing.equals(afterStatus) || Status.Close.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

// 3. 审核拒绝 -> 撤审、关闭
if (Status.Refuse.equals(beforeStatus)) {
if (Status.Editing.equals(afterStatus) || Status.Close.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

// 4. 活动中 -> 关闭
if (Status.Doing.equals(beforeStatus)) {
if (Status.Close.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

// 5. 活动关闭 -> 开启
if (Status.Close.equals(beforeStatus)) {
if (Status.Open.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

// 6. 活动开启 -> 关闭
if (Status.Open.equals(beforeStatus)) {
if (Status.Close.equals(afterStatus)) {
ActivityService.execStatus(activityId, beforeStatus, afterStatus);
return new Result("0000", "变更状态成功");
} else {
return new Result("0001", "变更状态拒绝");
}
}

return new Result("0001", "非可处理的活动状态变更");

}

}
  • 这里我们只需要看一下代码实现的结构即可。从上到下是一整篇的ifelse,基本这也是大部分初级程序员的开发方式。
  • 这样的面向过程式开发方式,对于不需要改动代码,也不需要二次迭代的,还是可以使用的(但基本不可能不迭代)。而且随着状态和需求变化,会越来越难以维护,后面的人也不好看懂并且很容易填充其他的流程进去。越来越乱就是从点滴开始的

3. 测试验证

3.1 编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Test
public void test() {
// 初始化数据
String activityId = "100001";
ActivityService.init(activityId, Status.Editing);

ActivityExecStatusController activityExecStatusController = new ActivityExecStatusController();
Result resultRefuse = activityExecStatusController.execStatus(activityId, Status.Editing, Status.Refuse);
logger.info("测试结果(编辑中To审核拒绝):{}", JSON.toJSONString(resultRefuse));

Result resultCheck = activityExecStatusController.execStatus(activityId, Status.Editing, Status.Check);
logger.info("测试结果(编辑中To提交审核):{}", JSON.toJSONString(resultCheck));
}
  • 我们的测试代码包括了两个功能的验证,一个是从编辑中到审核拒绝,另外一个是从编辑中到提交审核。
  • 因为从我们的场景流程中可以看到,编辑中的活动是不能直接到审核拒绝的,这中间还需要提审。

3.2 测试结果

1
2
3
4
复制代码23:24:30.774 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To审核拒绝):{"code":"0001","info":"变更状态拒绝"}
23:24:30.778 [main] INFO org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To提交审核):{"code":"0000","info":"变更状态成功"}

Process finished with exit code 0
  • 从测试结果和我们的状态流程的流转中可以看到,是符合测试结果预期的。除了不好维护外,这样的开发过程还是蛮快的,但不建议这么搞!

六、状态模式重构代码

接下来使用状态模式来进行代码优化,也算是一次很小的重构。

重构的重点往往是处理掉ifelse,而想处理掉ifelse基本离不开接口与抽象类,另外还需要重新改造代码结构。

1. 工程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码itstack-demo-design-19-02
└── src
└── main
└── java
└── org.itstack.demo.design
├── event
│ ├── CheckState.java
│ └── CloseState.java
│ └── DoingState.java
│ └── EditingState.java
│ └── OpenState.java
│ └── PassState.java
│ └── RefuseState.java
├── Result.java
├── State.java
└── StateHandler.java

状态模式模型结构

状态模式模型结构

  • 以上是状态模式的整个工程结构模型,State是一个抽象类,定义了各种操作接口(提审、审核、拒审等)。
  • 右侧的不同颜色状态与我们场景模拟中的颜色保持一致,是各种状态流程流转的实现操作。这里的实现有一个关键点就是每一种状态到下一个状态,都分配到各个实现方法中控制,也就不需要if语言进行判断了。
  • 最后是StateHandler对状态流程的统一处理,里面提供Map结构的各项服务接口调用,也就避免了使用if判断各项状态转变的流程。

2. 代码实现

2.1 定义状态抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
57
58
59
60
61
62
63
64
65
66
复制代码public abstract class State {

/**
* 活动提审
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result arraignment(String activityId, Enum<Status> currentStatus);

/**
* 审核通过
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result checkPass(String activityId, Enum<Status> currentStatus);

/**
* 审核拒绝
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result checkRefuse(String activityId, Enum<Status> currentStatus);

/**
* 撤审撤销
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result checkRevoke(String activityId, Enum<Status> currentStatus);

/**
* 活动关闭
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result close(String activityId, Enum<Status> currentStatus);

/**
* 活动开启
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result open(String activityId, Enum<Status> currentStatus);

/**
* 活动执行
*
* @param activityId 活动ID
* @param currentStatus 当前状态
* @return 执行结果
*/
public abstract Result doing(String activityId, Enum<Status> currentStatus);

}
  • 在整个接口中提供了各项状态流转服务的接口,例如;活动提审、审核通过、审核拒绝、撤审撤销等7个方法。
  • 在这些方法中所有的入参都是一样的,activityId(活动ID)、currentStatus(当前状态),只有他们的具体实现是不同的。

2.2 部分状态流转实现

编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码public class EditingState extends State {

public Result arraignment(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Check);
return new Result("0000", "活动提审成功");
}

public Result checkPass(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "编辑中不可审核通过");
}

public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "编辑中不可审核拒绝");
}

@Override
public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "编辑中不可撤销审核");
}

public Result close(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Close);
return new Result("0000", "活动关闭成功");
}

public Result open(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "非关闭活动不可开启");
}

public Result doing(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "编辑中活动不可执行活动中变更");
}

}

提审

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码public class CheckState extends State {

public Result arraignment(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "待审核状态不可重复提审");
}

public Result checkPass(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Pass);
return new Result("0000", "活动审核通过完成");
}

public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Refuse);
return new Result("0000", "活动审核拒绝完成");
}

@Override
public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Editing);
return new Result("0000", "活动审核撤销回到编辑中");
}

public Result close(String activityId, Enum<Status> currentStatus) {
ActivityService.execStatus(activityId, currentStatus, Status.Close);
return new Result("0000", "活动审核关闭完成");
}

public Result open(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "非关闭活动不可开启");
}

public Result doing(String activityId, Enum<Status> currentStatus) {
return new Result("0001", "待审核活动不可执行活动中变更");
}

}
  • 这里提供了两个具体实现类的内容,编辑状态和提审状态。
  • 例如在这两个实现类中,checkRefuse这个方法对于不同的类中有不同的实现,也就是不同状态下能做的下一步流转操作已经可以在每一个方法中具体控制了。
  • 其他5个类的操作是类似的具体就不在这里演示了,大部分都是重复代码。可以通过源码进行学习理解。

2.3 状态处理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码public class StateHandler {

private Map<Enum<Status>, State> stateMap = new ConcurrentHashMap<Enum<Status>, State>();

public StateHandler() {
stateMap.put(Status.Check, new CheckState()); // 待审核
stateMap.put(Status.Close, new CloseState()); // 已关闭
stateMap.put(Status.Doing, new DoingState()); // 活动中
stateMap.put(Status.Editing, new EditingState()); // 编辑中
stateMap.put(Status.Open, new OpenState()); // 已开启
stateMap.put(Status.Pass, new PassState()); // 审核通过
stateMap.put(Status.Refuse, new RefuseState()); // 审核拒绝
}

public Result arraignment(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).arraignment(activityId, currentStatus);
}

public Result checkPass(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkPass(activityId, currentStatus);
}

public Result checkRefuse(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkRefuse(activityId, currentStatus);
}

public Result checkRevoke(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).checkRevoke(activityId, currentStatus);
}

public Result close(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).close(activityId, currentStatus);
}

public Result open(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).open(activityId, currentStatus);
}

public Result doing(String activityId, Enum<Status> currentStatus) {
return stateMap.get(currentStatus).doing(activityId, currentStatus);
}

}
  • 这是对状态服务的统一控制中心,可以看到在构造函数中提供了所有状态和实现的具体关联,放到Map数据结构中。
  • 同时提供了不同名称的接口操作类,让外部调用方可以更加容易的使用此项功能接口,而不需要像在itstack-demo-design-19-01例子中还得传两个状态来判断。

3. 测试验证

3.1 编写测试类(Editing2Arraignment)

1
2
3
4
5
6
7
8
9
复制代码@Test
public void test_Editing2Arraignment() {
String activityId = "100001";
ActivityService.init(activityId, Status.Editing);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.arraignment(activityId, Status.Editing);
logger.info("测试结果(编辑中To提审活动):{}", JSON.toJSONString(result));
logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}

测试结果

1
2
3
4
复制代码23:59:20.883 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To提审活动):{"code":"0000","info":"活动提审成功"}
23:59:20.907 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694760892,"endTime":1593694760892,"status":"Check"} 状态:"Check"

Process finished with exit code 0
  • 测试编辑中To提审活动,的状态流转。

3.2 编写测试类(Editing2Open)

1
2
3
4
5
6
7
8
9
复制代码@Test
public void test_Editing2Open() {
String activityId = "100001";
ActivityService.init(activityId, Status.Editing);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.open(activityId, Status.Editing);
logger.info("测试结果(编辑中To开启活动):{}", JSON.toJSONString(result));
logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}

测试结果

1
2
3
4
复制代码23:59:36.904 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果(编辑中To开启活动):{"code":"0001","info":"非关闭活动不可开启"}
23:59:36.914 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694776907,"endTime":1593694776907,"status":"Editing"} 状态:"Editing"

Process finished with exit code 0
  • 测试编辑中To开启活动,的状态流转。

3.3 编写测试类(Refuse2Doing)

1
2
3
4
5
6
7
8
9
复制代码@Test
public void test_Refuse2Doing() {
String activityId = "100001";
ActivityService.init(activityId, Status.Refuse);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.doing(activityId, Status.Refuse);
logger.info("测试结果(拒绝To活动中):{}", JSON.toJSONString(result));
logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}

测试结果

1
2
3
4
复制代码23:59:46.339 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果(拒绝To活动中):{"code":"0001","info":"审核拒绝不可执行活动为进行中"}
23:59:46.352 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694786342,"endTime":1593694786342,"status":"Refuse"} 状态:"Refuse"

Process finished with exit code 0
  • 测试拒绝To活动中,的状态流转。

3.4 编写测试类(Refuse2Revoke)

1
2
3
4
5
6
7
8
9
复制代码@Test
public void test_Refuse2Revoke() {
String activityId = "100001";
ActivityService.init(activityId, Status.Refuse);
StateHandler stateHandler = new StateHandler();
Result result = stateHandler.checkRevoke(activityId, Status.Refuse);
logger.info("测试结果(拒绝To撤审):{}", JSON.toJSONString(result));
logger.info("活动信息:{} 状态:{}", JSON.toJSONString(ActivityService.queryActivityInfo(activityId)), JSON.toJSONString(ActivityService.queryActivityInfo(activityId).getStatus()));
}

测试结果

1
2
3
4
复制代码23:59:50.197 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果(拒绝To撤审):{"code":"0000","info":"撤销审核完成"}
23:59:50.208 [main] INFO org.itstack.demo.design.test.ApiTest - 活动信息:{"activityId":"100001","activityName":"早起学习打卡领奖活动","beginTime":1593694810201,"endTime":1593694810201,"status":"Editing"} 状态:"Editing"

Process finished with exit code 0
  • 测试测试结果(拒绝To撤审),的状态流转。
  • 综上以上四个测试类分别模拟了不同状态之间的有效流转和拒绝流转,不同的状态服务处理不同的服务内容。

七、总结

  • 从以上的两种方式对一个需求的实现中可以看到,在第二种使用设计模式处理后已经没有了ifelse,代码的结构也更加清晰易于扩展。这就是设计模式的好处,可以非常强大的改变原有代码的结构,让以后的扩展和维护都变得容易些。
  • 在实现结构的编码方式上可以看到这不再是面向过程的编程,而是面向对象的结构。并且这样的设计模式满足了单一职责和开闭原则,当你只有满足这样的结构下才会发现代码的扩展是容易的,也就是增加和修改功能不会影响整体的变化。
  • 但如果状态和各项流转较多像本文的案例中,就会产生较多的实现类。因此可能也会让代码的实现上带来了时间成本,因为如果遇到这样的场景可以按需评估投入回报率。主要点在于看是否经常修改、是否可以做成组件化、抽离业务与非业务功能。

八、推荐阅读

  • 1. 重学 Java 设计模式:实战工厂方法模式「多种类型商品不同接口,统一发奖服务搭建场景」
  • 2. 重学 Java 设计模式:实战原型模式「上机考试多套试,每人题目和答案乱序排列场景」
  • 3. 重学 Java 设计模式:实战桥接模式「多支付渠道(微信、支付宝)与多支付模式(刷脸、指纹)场景」
  • 4. 重学 Java 设计模式:实战组合模式「营销差异化人群发券,决策树引擎搭建场景」
  • 5. 重学 Java 设计模式:实战外观模式「基于SpringBoot开发门面模式中间件,统一控制接口白名单场景」
  • 6. 重学 Java 设计模式:实战享元模式「基于Redis秒杀,提供活动与库存信息查询场景」
  • 7. 重学 Java 设计模式:实战备忘录模式「模拟互联网系统上线过程中,配置文件回滚场景」

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

体验了一把线上CPU100%及应用OOM的排查和解决过程 ⛽

发表于 2020-07-02

⛽zipkin2.reporter.InMemoryReporterMetrics导致服务器CPU100%及应用OOM问题排查和解决

下面是我遇到的问题,以及一些简单的排查思路,如有不对的地方,欢迎留言讨论。
如果你已经遇到 InMemoryReporterMetrics 导致的OOM问题,并已经解决,则可忽略此文。若你对CPU100%以及线上问题OOM排查不清楚,可以浏览下本文。

问题现象

【告警通知-应用异常告警】

简单看下告警的信息:拒绝连接,反正就是服务有问题了,请不要太在意马赛克。
环境说明
====

Spring Cloud F版。

项目中默认使用 spring-cloud-sleuth-zipkin 依赖得到 zipkin-reporter。分析的版本发现是 zipkin-reporter版本是 2.7.3 。

1
2
3
4
5
6
复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

版本: 2.0.0.RELEASE

在这里插入图片描述

问题排查

通过告警信息,知道是哪一台服务器的哪个服务出现问题。首先登录服务器进行检查。

1、检查服务状态和验证健康检查URL是否ok

这一步可忽略/跳过,与实际公司的的健康检查相关,不具有通用性。

①查看服务的进程是否存在。

ps -ef | grep 服务名
ps -aux | grep 服务名

②查看对应服务健康检查的地址是否正常,检查 ip port 是否正确

是不是告警服务检查的url配置错了,一般这个不会出现问题

③验证健康检查地址

这个健康检查地址如:http://192.168.1.110:20606/serviceCheck
检查 IP 和 Port 是否正确。

1
2
3
4
5
6
7
复制代码# 服务正常返回结果
curl http://192.168.1.110:20606/serviceCheck
{"appName":"test-app","status":"UP"}

# 服务异常,服务挂掉
curl http://192.168.1.110:20606/serviceCheck
curl: (7) couldn't connect to host

2、查看服务的日志

查看服务的日志是否还在打印,是否有请求进来。查看发现服务OOM了。

在这里插入图片描述

tips:

java.lang.OutOfMemoryError GC overhead limit exceeded
oracle官方给出了这个错误产生的原因和解决方法:
Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded
Cause: The detail message “GC overhead limit exceeded” indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations.
Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.

原因:
大概意思就是说,JVM花费了98%的时间进行垃圾回收,而只得到2%可用的内存,频繁的进行内存回收(最起码已经进行了5次连续的垃圾回收),JVM就会曝出ava.lang.OutOfMemoryError: GC overhead limit exceeded错误。

在这里插入图片描述

上面tips来源:java.lang.OutOfMemoryError GC overhead limit exceeded原因分析及解决方案

3、检查服务器资源占用状况

查询系统中各个进程的资源占用状况,使用 top 命令。够查看出有一个进程为 11441 的进程 CPU 使用率达到300%,如下截图:

在这里插入图片描述

然后 查询这个进程下所有线程的CPU使用情况:

top -H -p pid
保存文件: top -H -n 1 -p pid > /tmp/pid_top.txt

1
2
3
4
5
6
7
复制代码# top -H -p 11441
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
11447 test 20 0 4776m 1.6g 13m R 92.4 20.3 74:54.19 java
11444 test 20 0 4776m 1.6g 13m R 91.8 20.3 74:52.53 java
11445 test 20 0 4776m 1.6g 13m R 91.8 20.3 74:50.14 java
11446 test 20 0 4776m 1.6g 13m R 91.4 20.3 74:53.97 java
....

查看 PID: 11441 下面的线程,发现有几个线程占用cpu较高。

4、保存堆栈数据

1、打印系统负载快照
top -b -n 2 > /tmp/top.txt
top -H -n 1 -p pid > /tmp/pid_top.txt

2、cpu升序打印进程对应线程列表
ps -mp -o THREAD,tid,time | sort -k2r > /tmp/进程号_threads.txt

3、看tcp连接数 (最好多次采样)
lsof -p 进程号 > /tmp/进程号_lsof.txt
lsof -p 进程号 > /tmp/进程号_lsof2.txt

4、查看线程信息 (最好多次采样)
jstack -l 进程号 > /tmp/进程号_jstack.txt
jstack -l 进程号 > /tmp/进程号_jstack2.txt
jstack -l 进程号 > /tmp/进程号_jstack3.txt

5、查看堆内存占用概况
jmap -heap 进程号 > /tmp/进程号_jmap_heap.txt

6、查看堆中对象的统计信息
jmap -histo 进程号 | head -n 100 > /tmp/进程号_jmap_histo.txt

7、查看GC统计信息
jstat -gcutil 进程号 > /tmp/进程号_jstat_gc.txt

8、生产对堆快照Heap dump
jmap -dump:format=b,file=/tmp/进程号_jmap_dump.hprof 进程号

堆的全部数据,生成的文件较大。

jmap -dump:live,format=b,file=/tmp/进程号_live_jmap_dump.hprof 进程号

dump:live,这个参数表示我们需要抓取目前在生命周期内的内存对象,也就是说GC收不走的对象,一般用这个就行。

拿到出现问题的快照数据,然后重启服务。

问题分析

根据上述的操作,已经获取了出现问题的服务的GC信息、线程堆栈、堆快照等数据。下面就进行分析,看问题到底出在哪里。

1、分析cpu占用100%的线程

转换线程ID

从jstack生成的线程堆栈进程分析。

将 上面线程ID 为
11447 :0x2cb7
11444 :0x2cb4
11445 :0x2cb5
11446 :0x2cb6
转为 16进制(jstack命令输出文件记录的线程ID是16进制)。
第一种转换方法 :

1
2
复制代码$ printf “0x%x” 11447
“0x2cb7”

第二种转换方法 : 在转换的结果加上 0x即可。

在这里插入图片描述

查找线程堆栈

1
2
3
4
5
复制代码$ cat 11441_jstack.txt | grep "GC task thread"
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f971401e000 nid=0x2cb4 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f9714020000 nid=0x2cb5 runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f9714022000 nid=0x2cb6 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f9714023800 nid=0x2cb7 runnable

发现这些线程都是在做GC操作。

2、分析生成的GC文件

1
2
复制代码  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
0.00 0.00 100.00 99.94 90.56 87.86 875 9.307 3223 5313.139 5322.446
  • S0:幸存1区当前使用比例
  • S1:幸存2区当前使用比例
  • E:Eden Space(伊甸园)区使用比例
  • O:Old Gen(老年代)使用比例
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

FGC 十分频繁。

3、分析生成的堆快照

使用 Eclipse Memory Analyzer 工具。 下载地址: www.eclipse.org/mat/downloa…

分析的结果:

在这里插入图片描述

在这里插入图片描述

看到堆积的大对象的具体内容:
在这里插入图片描述

问题大致原因,InMemoryReporterMetrics 引起的OOM。

zipkin2.reporter.InMemoryReporterMetrics @ 0xc1aeaea8
Shallow Size: 24 B Retained Size: 925.9 MB

也可以使用:Java内存Dump分析 进行分析,如下截图,功能没有MAT强大,有些功能需收费。

在这里插入图片描述

4、原因分析和验证

因为出现了这个问题,查看出现问题的这个服务 zipkin的配置,和其他服务没有区别。发现配置都一样。

然后看在试着对应的 zipkin 的jar包,发现出现问题的这个服务依赖的 zipkin版本较低。

有问题的服务的 zipkin-reporter-2.7.3.jar
其他没有问题的服务 依赖的包 : zipkin-reporter-2.8.4.jar

在这里插入图片描述

将有问题的服务依赖的包版本升级,在测试环境进行验证,查看堆栈快照发现没有此问题了。
原因探索
====

查 zipkin-reporter的 github:搜索 相应的资料
github.com/openzipkin/…
找到此 下面这个issues:
github.com/openzipkin/…

在这里插入图片描述

修复代码和验证代码:
github.com/openzipkin/…

对比两个版本代码的差异:

在这里插入图片描述

简单的DEMO验证:

1
2
3
4
5
6
复制代码// 修复前的代码:
private final ConcurrentHashMap<Throwable, AtomicLong> messagesDropped =
new ConcurrentHashMap<Throwable, AtomicLong>();
// 修复后的代码:
private final ConcurrentHashMap<Class<? extends Throwable>, AtomicLong> messagesDropped =
new ConcurrentHashMap<>();

修复后使用 这个为key : Class<? extends Throwable> 替换 Throwable。

简单验证:

在这里插入图片描述

在这里插入图片描述

解决方案

将zipkin-reporter 版本进行升级即可。使用下面依赖配置,引入的 zipkin-reporter版本为 2.8.4 。

1
2
3
4
5
6
复制代码<!-- zipkin 依赖包 -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave</artifactId>
<version>5.6.4</version>
</dependency>

小建议:配置JVM参数的时候还是加上下面参数,设置内存溢出的时候输出堆栈快照.

1
2
复制代码 -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=path/filename.hprof

参考文章

记一次sleuth发送zipkin异常引起的OOM
:www.jianshu.com/p/f8c74943c…

彩蛋

附上:百度搜索还是有点坑

在这里插入图片描述

推荐阅读 : 一文学会Java死锁和CPU 100% 问题的排查技巧


谢谢你的阅读,如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到!祝你每天开心愉快!


不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

博客首页 : https://aflyun.blog.csdn.net/

Java编程技术乐园:一个分享干货编程技术知识的公众号。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

逐行解读Spring(一) - 面试官:我看你写精通spri

发表于 2020-07-02

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

一、前言

最近在看spring源码,发现之前看的很多细节已经忘了,于是决定在看源码的过程中也把主要的流程用博客记载下来,希望自己能坚持下来吧。

spring已经发展很久,整个体系已经变得很庞大了。为了能更好的把源码看下去,我决定从最基础也是最核心的IOC开始切入,并且从最原始的xml解析开始看。面对这样一个庞大的体系,我认为从最原始的方式开始学习,才能更好的看懂它的设计和实现思路。

这一系列文章会默认你对于spring的使用已经熟悉,并且不抗拒读源码。因为很多的文字会在源码片段上注释-对于源码解析的文章,我暂时也找不到更好的表述方法了。

二、一个简单的示例

首先我们配置一个bean:

1
2
xml复制代码<bean class="com.xiaoxizi.spring.service.AccountServiceImpl" 
id="accountService" scope="singleton" primary="true"/>

对应的类:

1
2
3
4
5
6
java复制代码public class AccountServiceImpl implements AccountService {
@Override
public String queryAccount(String id) {
return null;
}
}

测试类:

1
2
3
4
5
6
java复制代码@Test
public void test1() {
applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService bean = applicationContext.getBean(AccountService.class);
System.out.println(bean);
}

运行结果:

1
shell复制代码com.xiaoxizi.spring.service.AccountServiceImpl@3e78b6a5

三、源码解析

1. beanDefinition注册流程

我们知道,spring容器启动的逻辑在refresh()方法里面。所以,话不多说,直接点进refresh()逻辑,具体位置是 org.springframework.context.support.AbstractApplicationContext#refresh

1
2
3
4
5
6
7
8
java复制代码public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh();
// 本篇博文主要讲这个逻辑
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
prepareBeanFactory(beanFactory);
...
}

本篇博文主要讲xml解析的逻辑,暂时我们只关注 obtainFreshBeanFactory()

1
2
3
4
java复制代码protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}

继续往下跟refreshBeanFactory(),实际上方法位置在org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 创建一个beanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
// 加载所有的BeanDefinitions,实际解析xml的位置
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

继续往下 org.springframework.context.support.AbstractXmlApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 这里使用了委托模式,把BeanDefinition的解析委托给了 BeanDefinitionReader
// 由于我们当前是解析xml,所以是委托给Xml...Reader。合理想象,注解方式将会委托给Anno...Reader
// 需要注意的是,我们把beanFactory引用传递给了Reader,之后会用到
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// ... 为BeanDefinitionReader设置了一些不重要的属性,略过。
// 加载BeanDefinition
loadBeanDefinitions(beanDefinitionReader);
}
// XmlBeanDefinitionReader对应构造器,注意 beanFactory 是作为 BeanDefinitionRegistry 传入的
public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
// 可以看到,是委托给Reader来加载BeanDefinition的
reader.loadBeanDefinitions(configResources);
}
// 配置文件位置实际上就是我们 new ClassPathXmlApplicationContext("xxx") 时传入的
String[] configLocations = getConfigLocations();
if (configLocations != null) {
// 可以看到,是委托给Reader来加载BeanDefinition的
reader.loadBeanDefinitions(configLocations);
}
}

经过一系列解析、包装、加载逻辑之后… org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadBeanDefinitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {

try {
// 配置文件的输入流被加载成了Document --> XML解析知识,详细可搜素 SAX解析
Document doc = doLoadDocument(inputSource, resource);
// 解析并注册BeanDefinitions
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
// 异常处理,省略...
}

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 又来了,熟悉的委托模式,Document的解析被委托给了BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
// 委托documentReader解析注册BeanDefinition,注意这里传入了一个 XmlReaderContext
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
// 可以看到XmlReaderContext的构造器传入了当前类-XmlBeanDefinitionReader
// 而当前类持有BeanDefinitionRegistry,所以XmlReaderContext中持有了一个BeanDefinitionRegistry
public XmlReaderContext createReaderContext(Resource resource) {
return new XmlReaderContext(resource, this.problemReporter, this.eventListener,
this.sourceExtractor, this, getNamespaceHandlerResolver());
}

细看DocumentReader解析过程 org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码protected void doRegisterBeanDefinitions(Element root) {
// 代理的逻辑,我们暂时不看
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// ...
}
// 解析xml之前的钩子,暂时是空实现
preProcessXml(root);
// 解析逻辑
parseBeanDefinitions(root, this.delegate);
// 解析xml之前的钩子,暂时是空实现
postProcessXml(root);
this.delegate = parent;
}

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 判断是否是默认的命名空间
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 解析默认标签
parseDefaultElement(ele, delegate);
}
else {
// 可以看到代理主要进行自定义标签的解析 - CustomElement
delegate.parseCustomElement(ele);
}
}
}
}
else {
// 可以看到代理主要进行自定义标签的解析 - CustomElement
delegate.parseCustomElement(root);
}
}

这里解释一下 自定义标签、自定义命名空间、默认标签、默认命名空间的含义

1
2
3
4
5
6
7
8
9
10
xml复制代码<!-- 标签前面有 xxx:即是spring的自定义标签,我们也可以自己定义一个xiaozize:的标签-之后会讲到 -->
<context:component-scan base-package="com.xiaoxizi.spring"/>
<!-- 该标签对应的命名空间在xml文件头部beans标签中声明 -->
<beans xmlns:context="http://www.springframework.org/schema/context" ... />

<!-- 默认标签没有 xx: 前缀 -->
<bean class="com.xiaoxizi.spring.service.AccountServiceImpl"
id="accountService" scope="singleton" primary="true"/>
<!-- 对应的命名空间也在xml文件头部beans标签中声明 -->
<beans xmlns="http://www.springframework.org/schema/beans" ... />

我们先看默认标签的解析过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// 解析import标签,其实就是一个递归解析import导入的xml的过程
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
// 解析alias标签,一般很少用这个功能,我们不看这个逻辑
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
// 解析bean标签,重头戏
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
// 解析beans标签, 其实就是递归走了一次解析流程
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// 这个方法眼熟吧?实际上我们就是从这个方法跟下来的
doRegisterBeanDefinitions(ele);
}
}

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// 具体的解析过程,将会把bean标签解析并封装到BeanDefinition中
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 对bean标签解析出来的BeanDefinition进行装饰,用的很少
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
// 注册BeanDefinition
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
...
}
}

2. bean标签解析

再正真解析bean标签前,我们先看一下spring的bean标签都有哪些属性和默认子标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码<bean
class="com.xiaoxizi.spring.service.AccountServiceImpl"
id="accountService"
name="aaa"
scope="singleton"
abstract="false"
parent="parent"
autowire="byType"
autowire-candidate="true"
primary="true"
depends-on="depends"
init-method="init"
destroy-method="destroy"
factory-bean="factoryBean"
factory-method="factoryMethod"
lazy-init="false"
>
<description>一些描述</description>
<constructor-arg ref="bean" value="固定值" type="参数类型" name="参数名称" index="索引"/>
<property name="key1" value="固定值" ref="beanRef"/>
<meta key="key1" value="固定值"/>
<qualifier type="bean类型" value="限定的bean的名称"/>
<lookup-method name="方法名" bean="bean名称"/>
<replaced-method name="方法名" replacer="bean名称">
<arg-type>参数类型,用于缺人唯一的方法</arg-type>
</replaced-method>
</bean>

逐一说一下bean标签中属性作用:

属性 作用
class 指明bean所属的类
id bean在ioc容器中的唯一标识,如果不填将取别名中的第一个
name bean的别名
scope bean的scope,一般日常开发都是使用默认的singleton单例,还有prototype多例。事实上web环境还有request和session。而且我们也可以自定义scope(之后会讲到的)
abstract 是否是抽象的,抽象的bean不会被实例化,只能被继承,用的很少
parent 指定父bean,可以结合abstract一起使用,当然parent指向的bean并不一定要是抽象的
autowire 被自动装配的模式,有byType,byName等可选
autowire-candidate 是否能被其他bean自动装配,false的话该bean将不能被其他bean注入,读者可以自行尝试一下
primary 如果自动装配时匹配到多个bean,标记为primary的bean将被优先注入。
depends-on 依赖,依赖的bean将会先被实例化
init-method bean实例化之后将会调用的方法
destroy-method bean销毁时将会调用的方法,需要主要的是,只有单例的bean,IOC容器才持有其引用,IOC容器销毁 –> bean销毁时才会触发这个方法。
factory-bean 工厂bean的名称,需要与factory-method结合使用,创建bean时将会调用factory-bean.factory-method()来获取当前类实例。
factory-method 工厂bean方法,其实@Bean注解就是通过factory-bean、factory-method属性的功能实现的。
lazy-init 是否是懒加载的

bean标签的默认子标签的作用:

子标签 作用
description 没啥作用,就是个描述而已
constructor-arg 构造器注入时使用的标签,标明每个参数需要的值。用得少,因为可能会导致不能处理的循环依赖
property 为bean中的属性注入值,常用
meta bean的元数据信息,业务开发中比较少用,之后跟源码过程中能看到使用的地方
qualifier 与@Qualifier作用一致,当注入时出现多个匹配的bean时,将会注入qualifier限定的bean
lookup-method 可以理解为覆盖/重写方法,把当前bean中的指定方法委托给指定的bean执行(例:把当前beanA.test() 方法委托给 beanB.test(),方法名必须一致),应用场景比较少。
replaced-method 与lookup-method类似,只是该标签多了子标签用来准确定位方法,当待委托的方法有多个重名方法(重载)时可以使用。

继续往下看解析过程org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parseBeanDefinitionElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
String id = ele.getAttribute(ID_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
// 处理别名
List<String> aliases = new ArrayList<>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
// 默认id为beanName
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
// 如果不配置id,将会取第一个别名当做beanName
beanName = aliases.remove(0);
// ...
}

if (containingBean == null) {
// 校验beanName、alias是否重复
// 实际上有一个set用来存所以用过的name,避免重复 BeanDefinitionParserDelegate#usedNames
checkNameUniqueness(beanName, aliases, ele);
}
// 要解析元素了,解析xml获取一个beanDefinition
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
// ... 跳过一些逻辑
String[] aliasesArray = StringUtils.toStringArray(aliases);
// 把beanDefinition封装成Holder
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}

解析方法parseBeanDefinitionElement中我们将把xml bean标签中的信息解析并封装到 一个BeanDefinition中,而之后(初始化流程),我们将会根据BeanDefinition中的属性来创建bean实例。现在,先让我们看一下这个BeanDefinition的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
java复制代码// 默认使用的是 GenericBeanDefinition
public class GenericBeanDefinition extends AbstractBeanDefinition {
// 这个子类中只有一个parentName属性,明显对应bean标签的parent属性
@Nullable
private String parentName;
}
// 找父类,基本可以看到属性和bean标签的内容是一一对应的,不过meta子标签的信息还在父类里面
public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor
implements BeanDefinition, Cloneable {
@Nullable
private volatile Object beanClass;

@Nullable
private String scope = SCOPE_DEFAULT;

private boolean abstractFlag = false;

@Nullable
private Boolean lazyInit;

private int autowireMode = AUTOWIRE_NO;

private int dependencyCheck = DEPENDENCY_CHECK_NONE;

@Nullable
private String[] dependsOn;

private boolean autowireCandidate = true;

private boolean primary = false;
// qualifier子标签信息
private final Map<String, AutowireCandidateQualifier> qualifiers = new LinkedHashMap<>();

@Nullable
private Supplier<?> instanceSupplier;

private boolean nonPublicAccessAllowed = true;

private boolean lenientConstructorResolution = true;

@Nullable
private String factoryBeanName;

@Nullable
private String factoryMethodName;
// constructor-arg 子标签的信息
@Nullable
private ConstructorArgumentValues constructorArgumentValues;
// property子标签的信息
@Nullable
private MutablePropertyValues propertyValues;
// lookup-method、replaced-method子标签的信息
private MethodOverrides methodOverrides = new MethodOverrides();

@Nullable
private String initMethodName;

@Nullable
private String destroyMethodName;

@Nullable
private String description;
// 加载这个beanDefinition的资源 -> 哪个xml
@Nullable
private Resource resource;
}
// BeanMetadataAttributeAccessor extends AttributeAccessorSupport
public abstract class AttributeAccessorSupport implements AttributeAccessor, Serializable {
// 保存beanDefinition的元数据信息
/** Map with String keys and Object values. */
private final Map<String, Object> attributes = new LinkedHashMap<>();
}

好,我们继续往下解析bean标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
java复制代码public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, @Nullable BeanDefinition containingBean) {
// ...
// 获取class属性
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
// 获取parent属性
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}

try {
// 创建了一个BeanDefinition,感兴趣的同学可以跟一下,实际上就是创建了一个
// GenericBeanDefinition 并且把 parent set 进去了
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// 解析bean标签上的属性 scope, autowrite等
// 感兴趣的同学可以跟一下,就是把值从xml中解析出来塞到beanDefinition而已
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
// description属性
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));

// 解析meta子标签
parseMetaElements(ele, bd);
// 解析lockup-method子标签
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
// 解析replaced-method子标签
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
// 解析constructor-arg子标签
parseConstructorArgElements(ele, bd);
// 解析property子标签
parsePropertyElements(ele, bd);
// 解析qualifier子标签
parseQualifierElements(ele, bd);

bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));

return bd;
}
// ...
return null;
}

// 因为解析的流程其实都差不多,这边简单挑几个有代表性的看一下
public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) {
NodeList nl = ele.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 循环所有bean标签的子标签,找到mate标签
if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) {
Element metaElement = (Element) node;
String key = metaElement.getAttribute(KEY_ATTRIBUTE);
String value = metaElement.getAttribute(VALUE_ATTRIBUTE);
// 封装成BeanMetadataAttribute
BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value);
attribute.setSource(extractSource(metaElement));
// 记住我们的beanDefinition是继承BeanMetadataAttributeAccessor的
// 所以这里其实也是放到beanDefinition中了
attributeAccessor.addMetadataAttribute(attribute);
}
}
}

public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 循环所有bean标签的子标签,找到lockup-method标签
if (isCandidateElement(node) && nodeNameEquals(node, LOOKUP_METHOD_ELEMENT)) {
Element ele = (Element) node;
String methodName = ele.getAttribute(NAME_ATTRIBUTE);
String beanRef = ele.getAttribute(BEAN_ELEMENT);
// 封装成LookupOverride
LookupOverride override = new LookupOverride(methodName, beanRef);
override.setSource(extractSource(ele));
overrides.addOverride(override);
}
}
}
// 我们这里看一下MethodOverrides的结构
// 首先MethodOverrides是在beanDefinition创建的时候就初始化的
private MethodOverrides methodOverrides = new MethodOverrides();
public class MethodOverrides {
// 其实就是一个MethodOverride列表
private final Set<MethodOverride> overrides = new CopyOnWriteArraySet<>();
// ...
}
// 继续看看MethodOverride
public abstract class MethodOverride implements BeanMetadataElement {
// 被代理/重写的方法名
private final String methodName;
// 是否是重载的方法 - 重载的方法处理起来要复杂点
private boolean overloaded = true;
// ...
}
// MethodOverride只有两个子类,LookupOverride 和 ReplaceOverride,看名字大家都知道对应哪个标签了
public class LookupOverride extends MethodOverride {
// 提供重写逻辑的bean的名称
private final String beanName;
// 可以看到,这个属性我们在解析xml的时候没有用到。
// 这个应该是用来支持注解@Lockup的,因为这个功能用的很少,我也没去深究,不过字段的含义还是很好理解的
private Method method;
}
public class ReplaceOverride extends MethodOverride {
// 提供重写逻辑的bean的名称
private final String methodReplacerBeanName;
// 之前有说过replaced-method是用于目标方法有重载的情况,这个参数类型列表就是用来区分重载的方法的
// 也是replaced-method标签的子标签arg-type中定义的
private List<String> typeIdentifiers = new LinkedList<>();
}

那么致此,我们的一个默认的bean标签就解析完毕了,并且把所有的信息封装到了一个BeanDefinition实例中,然后这个beanDefinition将会注册到我们的IOC容器中去,为下一步生成实例做准备。org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#processBeanDefinition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// 具体的解析过程,将会把bean标签解析并封装到BeanDefinition中
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 对bean标签解析出来的BeanDefinition进行装饰,用的很少,但此处的是个spi很重要
// 这里下一期再讲
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
// 注册BeanDefinition
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
...
}
}

我们主要看一下注册beanDefinition的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
String beanName = definitionHolder.getBeanName();
// 注册bean
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
// 注册别名
registry.registerAlias(beanName, alias);
}
}
}

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
// ... 这里去除掉了大部分的分支判断和异常处理逻辑,有兴趣的同学可以自行看一下
// registerBeanDefinition的主要逻辑其实是以下两段
// 将当前beanDefinition放入两个容器
this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);
// 这里是把当前bean从 manualSingletonNames 中删除,简单看了下逻辑
// 对于通过 DefaultListableBeanFactory#registerSingleton(String beanName, Object singletonObject)
// 直接注册到IOC容器中的单例bean,因为没有对应的beanDefinition,name相应的beanName会被记录到这个set
// 而如果我们解析xml中获取到了相应的beanDefinition,就会将其从set中移除
// 这个逻辑可以不看,不是主流程
removeManualSingletonName(beanName);
}

四、总结

spring xml bean标签的解析就完整了,其实简单来讲,就是通过一系列手段,拿到xml bean标签中配置的各种属性,封装成一个BeanDefinition对象,然后把这个对象存到我们IOC容器的 beanDefinitionMap、beanDefinitionNames中。这两个容器在之后的bean实例创建的过程中将会用到。

第一次写这种源码类的博客(其实算是第一次写博客?emmm),感觉写的很干,希望自己能坚持下来并且有进步吧。

下一篇将会讲spring 自定义标签的解析以及应用~

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

٩(* ఠO ఠ)=3⁼³₌₃⁼³₌₃⁼³₌₃嘟啦啦啦啦。。。

这里是新人博主小希子,大佬们都看到这了,左上角点个赞再走吧~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux 操作系统!开篇!!!

发表于 2020-07-02

此篇文章主要会带你介绍 Linux 操作系统,包括 Linux 本身、Linux 如何使用、以及系统调用和 Linux 是如何工作的。

Linux 简介

UNIX 是一个交互式系统,用于同时处理多进程和多用户同时在线。为什么要说 UNIX,那是因为 Linux 是由 UNIX 发展而来的,UNIX 是由程序员设计,它的主要服务对象也是程序员。Linux 继承了 UNIX 的设计目标。从智能手机到汽车,超级计算机和家用电器,从家用台式机到企业服务器,Linux 操作系统无处不在。

大多数程序员都喜欢让系统尽量简单,优雅并具有一致性。举个例子,从最底层的角度来讲,一个文件应该只是一个字节集合。为了实现顺序存取、随机存取、按键存取、远程存取只能是妨碍你的工作。相同的,如果命令

1
复制代码ls A*

意味着只列出以 A 为开头的所有文件,那么命令

1
复制代码rm A*

应该会移除所有以 A 为开头的文件而不是只删除文件名是 A* 的文件。这个特性也是最小吃惊原则(principle of least surprise)

最小吃惊原则一半常用于用户界面和软件设计。它的原型是:该功能或者特征应该符合用户的预期,不应该使用户感到惊讶和震惊。

一些有经验的程序员通常希望系统具有较强的功能性和灵活性。设计 Linux 的一个基本目标是每个应用程序只做一件事情并把他做好。所以编译器只负责编译的工作,编译器不会产生列表,因为有其他应用比编译器做的更好。

很多人都不喜欢冗余,为什么在 cp 就能描述清楚你想干什么时候还使用 copy?这完全是在浪费宝贵的 hacking time。为了从文件中提取所有包含字符串 ard 的行,Linux 程序员应该输入

1
复制代码grep ard f

Linux 接口

Linux 系统是一种金字塔模型的系统,如下所示

应用程序发起系统调用把参数放在寄存器中(有时候放在栈中),并发出 trap 系统陷入指令切换用户态至内核态。因为不能直接在 C 中编写 trap 指令,因此 C 提供了一个库,库中的函数对应着系统调用。有些函数是使用汇编编写的,但是能够从 C 中调用。每个函数首先把参数放在合适的位置然后执行系统调用指令。因此如果你想要执行 read 系统调用的话,C 程序会调用 read 函数库来执行。这里顺便提一下,是由 POSIX 指定的库接口而不是系统调用接口。也就是说,POSIX 会告诉一个标准系统应该提供哪些库过程,它们的参数是什么,它们必须做什么以及它们必须返回什么结果。

除了操作系统和系统调用库外,Linux 操作系统还要提供一些标准程序,比如文本编辑器、编译器、文件操作工具等。直接和用户打交道的是上面这些应用程序。因此我们可以说 Linux 具有三种不同的接口:系统调用接口、库函数接口和应用程序接口

Linux 中的 GUI(Graphical User Interface) 和 UNIX 中的非常相似,这种 GUI 创建一个桌面环境,包括窗口、目标和文件夹、工具栏和文件拖拽功能。一个完整的 GUI 还包括窗口管理器以及各种应用程序。

Linux 上的 GUI 由 X 窗口支持,主要组成部分是 X 服务器、控制键盘、鼠标、显示器等。当在 Linux 上使用图形界面时,用户可以通过鼠标点击运行程序或者打开文件,通过拖拽将文件进行复制等。

Linux 组成部分

事实上,Linux 操作系统可以由下面这几部分构成

  • 引导程序(Bootloader):引导程序是管理计算机启动过程的软件,对于大多数用户而言,只是弹出一个屏幕,但其实内部操作系统做了很多事情
  • 内核(Kernel):内核是操作系统的核心,负责管理 CPU、内存和外围设备等。
  • 初始化系统(Init System):这是一个引导用户空间并负责控制守护程序的子系统。一旦从引导加载程序移交了初始引导,它就是用于管理引导过程的初始化系统。
  • 后台进程(Daemon):后台进程顾名思义就是在后台运行的程序,比如打印、声音、调度等,它们可以在引导过程中启动,也可以在登录桌面后启动
  • 图形服务器(Graphical server):这是在监视器上显示图形的子系统。通常将其称为 X 服务器或 X。
  • 桌面环境(Desktop environment):这是用户与之实际交互的部分,有很多桌面环境可供选择,每个桌面环境都包含内置应用程序,比如文件管理器、Web 浏览器、游戏等
  • 应用程序(Applications):桌面环境不提供完整的应用程序,就像 Windows 和 macOS 一样,Linux 提供了成千上万个可以轻松找到并安装的高质量软件。

Shell

尽管 Linux 应用程序提供了 GUI ,但是大部分程序员仍偏好于使用命令行(command-line interface),称为shell。用户通常在 GUI 中启动一个 shell 窗口然后就在 shell 窗口下进行工作。

shell 命令行使用速度快、功能更强大、而且易于扩展、并且不会带来肢体重复性劳损(RSI)。

下面会介绍一些最简单的 bash shell。当 shell 启动时,它首先进行初始化,在屏幕上输出一个 提示符(prompt),通常是一个百分号或者美元符号,等待用户输入

等用户输入一个命令后,shell 提取其中的第一个词,这里的词指的是被空格或制表符分隔开的一连串字符。假定这个词是将要运行程序的程序名,那么就会搜索这个程序,如果找到了这个程序就会运行它。然后 shell 会将自己挂起直到程序运行完毕,之后再尝试读入下一条指令。shell 也是一个普通的用户程序。它的主要功能就是读取用户的输入和显示计算的输出。shell 命令中可以包含参数,它们作为字符串传递给所调用的程序。比如

1
复制代码cp src dest

会调用 cp 应用程序并包含两个参数 src 和 dest。这个程序会解释第一个参数是一个已经存在的文件名,然后创建一个该文件的副本,名称为 dest。

并不是所有的参数都是文件名,比如下面

1
复制代码head -20 file

第一个参数 -20,会告诉 head 应用程序打印文件的前 20 行,而不是默认的 10 行。控制命令操作或者指定可选值的参数称为标志(flag),按照惯例标志应该使用 - 来表示。这个符号是必要的,比如

1
复制代码head 20 file

是一个完全合法的命令,它会告诉 head 程序输出文件名为 20 的文件的前 10 行,然后输出文件名为 file 文件的前 10 行。Linux 操作系统可以接受一个或多个参数。

为了更容易的指定多个文件名,shell 支持 魔法字符(magic character),也被称为通配符(wild cards)。比如,* 可以匹配一个或者多个可能的字符串

1
复制代码ls *.c

告诉 ls 列举出所有文件名以 .c 结束的文件。如果同时存在多个文件,则会在后面进行并列。

另一个通配符是问号,负责匹配任意一个字符。一组在中括号中的字符可以表示其中任意一个,因此

1
复制代码ls [abc]*

会列举出所有以 a、b 或者 c 开头的文件。

shell 应用程序不一定通过终端进行输入和输出。shell 启动时,就会获取 标准输入、标准输出、标准错误文件进行访问的能力。

标准输出是从键盘输入的,标准输出或者标准错误是输出到显示器的。许多 Linux 程序默认是从标准输入进行输入并从标准输出进行输出。比如

1
复制代码sort

会调用 sort 程序,会从终端读取数据(直到用户输入 ctrl-d 结束),根据字母顺序进行排序,然后将结果输出到屏幕上。

通常还可以重定向标准输入和标准输出,重定向标准输入使用 < 后面跟文件名。标准输出可以通过一个大于号 > 进行重定向。允许一个命令中重定向标准输入和输出。例如命令

1
复制代码sort <in >out

会使 sort 从文件 in 中得到输入,并把结果输出到 out 文件中。由于标准错误没有重定向,所以错误信息会直接打印到屏幕上。从标准输入读入,对其进行处理并将其写入到标准输出的程序称为 过滤器。

考虑下面由三个分开的命令组成的指令

1
复制代码sort <in >temp;head -30 <temp;rm temp

首先会调用 sort 应用程序,从标准输入 in 中进行读取,并通过标准输出到 temp。当程序运行完毕后,shell 会运行 head ,告诉它打印前 30 行,并在标准输出(默认为终端)上打印。最后,temp 临时文件被删除。轻轻的,你走了,你挥一挥衣袖,不带走一片云彩。

命令行中的第一个程序通常会产生输出,在上面的例子中,产生的输出都不 temp 文件接收。然而,Linux 还提供了一个简单的命令来做这件事,例如下面

1
复制代码sort <in | head -30

上面 | 称为竖线符号,它的意思是从 sort 应用程序产生的排序输出会直接作为输入显示,无需创建、使用和移除临时文件。由管道符号连接的命令集合称为管道(pipeline)。例如如下

1
复制代码grep cxuan *.c | sort | head -30 | tail -5 >f00

对任意以 .t 结尾的文件中包含 cxuan 的行被写到标准输出中,然后进行排序。这些内容中的前 30 行被 head 出来并传给 tail ,它又将最后 5 行传递给 foo。这个例子提供了一个管道将多个命令连接起来。

可以把一系列 shell 命令放在一个文件中,然后将此文件作为输入来运行。shell 会按照顺序对他们进行处理,就像在键盘上键入命令一样。包含 shell 命令的文件被称为 shell 脚本(shell scripts)。

推荐一个 shell 命令的学习网站:www.shellscript.sh/

shell 脚本其实也是一段程序,shell 脚本中可以对变量进行赋值,也包含循环控制语句比如 if、for、while 等,shell 的设计目标是让其看起来和 C 相似(There is no doubt that C is father)。由于 shell 也是一个用户程序,所以用户可以选择不同的 shell。

Linux 应用程序

Linux 的命令行也就是 shell,它由大量标准应用程序组成。这些应用程序主要有下面六种

  • 文件和目录操作命令
  • 过滤器
  • 文本程序
  • 系统管理
  • 程序开发工具,例如编辑器和编译器
  • 其他

除了这些标准应用程序外,还有其他应用程序比如 Web 浏览器、多媒体播放器、图片浏览器、办公软件和游戏程序等。

我们在上面的例子中已经见过了几个 Linux 的应用程序,比如 sort、cp、ls、head,下面我们再来认识一下其他 Linux 的应用程序。

我们先从几个例子开始讲起,比如

1
复制代码cp a b

是将 a 复制一个副本为 b ,而

1
复制代码mv a b

是将 a 移动到 b ,但是删除原文件。

上面这两个命令有一些区别,cp 是将文件进行复制,复制完成后会有两个文件 a 和 b;而 mv 相当于是文件的移动,移动完成后就不再有 a 文件。cat 命令可以把多个文件内容进行连接。使用 rm 可以删除文件;使用 chmod 可以允许所有者改变访问权限;文件目录的的创建和删除可以使用 mkdir 和 rmdir 命令;使用 ls 可以查看目录文件,ls 可以显示很多属性,比如大小、用户、创建日期等;sort 决定文件的显示顺序

Linux 应用程序还包括过滤器 grep,grep 从标准输入或者一个或多个输入文件中提取特定模式的行;sort 将输入进行排序并输出到标准输出;head 提取输入的前几行;tail 提取输入的后面几行;除此之外的过滤器还有 cut 和 paste,允许对文本行的剪切和复制;od 将输入转换为 ASCII ;tr 实现字符大小写转换;pr 为格式化打印输出等。

程序编译工具使用 gcc;

make 命令用于自动编译,这是一个很强大的命令,它用于维护一个大的程序,往往这类程序的源码由许多文件构成。典型的,有一些是 header files 头文件,源文件通常使用 include 指令包含这些文件,make 的作用就是跟踪哪些文件属于头文件,然后安排自动编译的过程。

下面列出了 POSIX 的标准应用程序

程序 应用
ls 列出目录
cp 复制文件
head 显示文件的前几行
make 编译文件生成二进制文件
cd 切换目录
mkdir 创建目录
chmod 修改文件访问权限
ps 列出文件进程
pr 格式化打印
rm 删除一个文件
rmdir 删除文件目录
tail 提取文件最后几行
tr 字符集转换
grep 分组
cat 将多个文件连续标准输出
od 以八进制显示文件
cut 从文件中剪切
paste 从文件中粘贴

Linux 内核结构

在上面我们看到了 Linux 的整体结构,下面我们从整体的角度来看一下 Linux 的内核结构

内核直接坐落在硬件上,内核的主要作用就是 I/O 交互、内存管理和控制 CPU 访问。上图中还包括了 中断 和 调度器,中断是与设备交互的主要方式。中断出现时调度器就会发挥作用。这里的低级代码停止正在运行的进程,将其状态保存在内核进程结构中,并启动驱动程序。进程调度也会发生在内核完成一些操作并且启动用户进程的时候。图中的调度器是 dispatcher。

注意这里的调度器是 dispatcher 而不是 scheduler,这两者是有区别的

scheduler 和 dispatcher 都是和进程调度相关的概念,不同的是 scheduler 会从几个进程中随意选取一个进程;而 dispatcher 会给 scheduler 选择的进程分配 CPU。

然后,我们把内核系统分为三部分。

  • I/O 部分负责与设备进行交互以及执行网络和存储 I/O 操作的所有内核部分。

从图中可以看出 I/O 层次的关系,最高层是一个虚拟文件系统,也就是说不管文件是来自内存还是磁盘中,都是经过虚拟文件系统中的。从底层看,所有的驱动都是字符驱动或者块设备驱动。二者的主要区别就是是否允许随机访问。网络驱动设备并不是一种独立的驱动设备,它实际上是一种字符设备,不过网络设备的处理方式和字符设备不同。

上面的设备驱动程序中,每个设备类型的内核代码都不同。字符设备有两种使用方式,有一键式的比如 vi 或者 emacs ,需要每一个键盘输入。其他的比如 shell ,是需要输入一行按回车键将字符串发送给程序进行编辑。

网络软件通常是模块化的,由不同的设备和协议来支持。大多数 Linux 系统在内核中包含一个完整的硬件路由器的功能,但是这个不能和外部路由器相比,路由器上面是协议栈,包括 TCP/IP 协议,协议栈上面是 socket 接口,socket 负责与外部进行通信,充当了门的作用。

磁盘驱动上面是 I/O 调度器,它负责排序和分配磁盘读写操作,以尽可能减少磁头的无用移动。

  • I/O 右边的是内存部件,程序被装载进内存,由 CPU 执行,这里会涉及到虚拟内存的部件,页面的换入和换出是如何进行的,坏页面的替换和经常使用的页面会进行缓存。
  • 进程模块负责进程的创建和终止、进程的调度、Linux 把进程和线程看作是可运行的实体,并使用统一的调度策略来进行调度。

在内核最顶层的是系统调用接口,所有的系统调用都是经过这里,系统调用会触发一个 trap,将系统从用户态转换为内核态,然后将控制权移交给上面的内核部件。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Jetpack 新成员 Hilt 与 Dagger 大不同(

发表于 2020-07-02

在 Google 的 Hilt 文档中 Dependency injection with Hilt 只是简单的告诉我们 Hilt 是 Android 的依赖注入库,它减少了在项目中进行手动依赖,Hilt 是基于 Dagger 基础上进行开发的,为常见的 Android 类提供容器并自动管理它们的生命周期等等。

文档中的概念过于模糊,那么 Hilt 与 Dagger 在使用上有那些区别,并没有一个直观感受,而本文的目的就是详细的分析一下 Hilt 与 Dagger 到底有那些不同之处。

在之前的两篇文章中已经详细的介绍了 Hilt 注解的含义以及用法,并附上详细的案例,在代码中都有详细的注释,为了节省篇幅,本文不会在详细介绍 Hilt 注解的含义,可以点击下方链接前往查看。

  • Jetpack 新成员 Hilt 实践(一)启程过坑记:介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何与 Android 框架类进行绑定,以及他们的生命周期。
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇:分析注解的区别、限定符和作用域注解的使用,以及如何在 ViewModel、App Startup、ContentProvider 中使用等等。
  • 代码已经上传到了 GitHub:AndroidX-Jetpack-Practice 如果对你有帮助,请在仓库右上角帮我点个赞,感谢。

在之前的文章中 放弃 Dagger 拥抱 Koin 分析了 Dagger 和 Koin 编译时间和使用上的不同等等,这篇文章主要从以下几个方面分析 Hilt 与 Dagger 的不同之处。

  • 初始化对比?
  • 与 Android 框架类对比?
  • 与 Room、WorkManager 对比?
  • 与 ViewModule 对比?
  • Hilt 在多模块中的局限性?

初始化对比

无论使用 Hilt 还是使用 Dagger,使用它们之前都需要在 Application 里面进行初始化,这是依赖注入容器的入口。

Dagger

在 Dagger 中我们必须通过 @Module 和 @Component 注解,创建对应的文件,并注入 Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// Component 声明了所有的 modules
// ActivityAllModule 配置了所有的 activity
@Singleton
@Component(modules = arrayOf(
AndroidInjectionModule::class,
ActivitylModule::class))
interface AppCompoment {

fun inject(app: App)

@Component.Builder
interface Builder {
@BindsInstance
fun bindApplication(app: Application): Builder
fun build(): AppCompoment
}

}

然后创建完 modules 和 components 文件之后,需要在 Application 中 初始化 Dagger, 需要实现 HasActivityInjector 接口,用来自动管理 Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码class App : Application(), HasActivityInjector {

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Activity>

override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
}

override fun onCreate() {
super.onCreate()
DaggerAppCompoment.builder()
.bindApplication(this)
.build()
.inject(this)
}

override fun activityInjector(): AndroidInjector<Activity> {
return dispatchingAndroidInjector
}
}

Hilt

在 Hilt 中我们不需要手动指定包含每个模块,在 Application 中添加 @HiltAndroidApp 注解将会触发 Hilt 代码的生成,用作应用程序依赖项容器的基类。

1
2
3
4
5
6
7
8
9
10
11
复制代码@HiltAndroidApp
class HiltApplication : Application() {
/**
* 1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application
* 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
* 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
* 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
* Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
* Application 使用 @HiltAndroidApp 注解
*/
}

所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application,这是依赖注入容器的入口。

Hilt 提供了 @ApplicationContext 和 @ActivityContext 两种预定义限定符,我们可以直接使用,不需要开发人员自己注入 Application。

与 Android 框架类对比

我们来看一下 Dagger 和 Hilt 对于最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 的支持,我们以 Activity 和 Fragment 为例。

Dagger

在 Dagger 中对于每一个 Activity 和 Fragment 都需要告诉 Dagger 如何注入它们,所以我们需要创建对应的 ActivityModule、FragmentModule。

每次有新增的 Fragment 和 Activity 必须添加在对应的 Module 文件中,每次添加 Activity 时都需要添加 @ContributesAndroidInjector 注解,用于自动生成子组件相关代码,帮我们减少重复的模板代码,编译的时候会自动创建的一个类 ActivitylModule_ContributeXXXXActivity,帮我们生成注入的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码// 把所有的 Activity 放到 ActivitylModule 进行统一管理
@Module
abstract class ActivitylModule(){

// ContributesAndroidInjector 用于自动生成子组件相关代码,帮我们减少重复的模板代码
// modules 指定子组件(当前 MainActivity 包含了 2 个 fragment,所以我们需要指定包含的 fragment)
// 每次新建的 activity 都需要在这里手动添加
// 通过注解 @ActivityScope 指定 Activity 的 生命周期
@ActivityScope
@ContributesAndroidInjector(modules = arrayOf(FragmentModule::class))
abstract fun contributeMainActivity():MainActivity

}

// 管理所有的 Fragment
@Module
abstract class FragmentModule {

// 如果当前有新增的 Fragment 需要添加到这个模块中
@ContributesAndroidInjector
abstract fun contributeHomeFragment(): HomeFragment

@ContributesAndroidInjector
abstract fun contributeAboutFragment(): AboutFragment
}

Dagger 提供了 HasSupportFragmentInjector 接口去自动管理 Fragment,所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector,并且需要在 Activity 和 Fragment 中添加 AndroidInjection.inject(this)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码abstract class BaseActivity : AppCompatActivity(),HasSupportFragmentInjector {

@Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
}

override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
}

abstract class BaseFragment : Fragment() {
override fun onAttach(context: Context?) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}
}

Hilt

在 Hilt 中 Android 框架类完全由 Hilt 帮我管理,我们只要在 Activiyt 和 Fragmetn 中添加 @AndroidEntryPoint 即可。

1
2
3
4
5
6
7
8
复制代码@AndroidEntryPoint
class HitAppCompatActivity : AppCompatActivity() {

}

@AndroidEntryPoint
class HiltFragment : Fragment() {
}

Hilt 真做了很多优化工作,相比于 Dagger 而言,删除很多模板代码,不需要开发者手动管理,开发者只需要关注如何进行绑定即可。

与 Room、WorkManager 对比

接下来我们来看一下 Dagger 和 Hilt 对于 Room、WorkManager 在使用上有什么区别,这里以 Room 为例。

Dagger

在 Dagger 中使用 Room 需要使用 @Module 注解创建 RoomModule 文件,然后在 Component 中添加 RoomModule。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码@Module
class RoomModule(val app: Application) {

@Provides
@Singleton
fun providerAppDataBase(): AppDataBase = Room
.databaseBuilder(app, AppDataBase::class.java, "dhl.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()

}

// Component 声明了所有的 modules
// ActivityAllModule 配置了所有的 activity
// RoomModule 和数据库相关的
@Singleton
@Component(modules = arrayOf(
AndroidInjectionModule::class,
RoomModule::class,
ActivitylModule::class))
interface AppCompoment {
fun inject(app: App)

@Component.Builder
interface Builder {
@BindsInstance
fun bindApplication(app: Application): Builder
fun build(): AppCompoment
}

}

在 Dagger 中需要在对应的模块中添加组件对应的生命周期 @Singleton、@ActivityScope 等等。

  • @Singleton 对应的 Application。
  • @ActivityScope 对应的 Activity。

Hilt

在 Hilt 中我们只需要使用注解 @Module 创建 RoomModule 文件即可,不需要自己手动去添加 Module。

使用 @InstallIn 注解指定 module 的生命周期,例如使用 @InstallIn(ApplicationComponent::class) 注解 module 会绑定到 Application 的生命周期上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 RoomModule 绑定到 Application 的生命周期。
object RoomModule {

/**
* @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
* @Singleton 提供单例
*/
@Provides
@Singleton
fun provideAppDataBase(application: Application): AppDataBase {
return Room
.databaseBuilder(application, AppDataBase::class.java, "dhl.db")
.fallbackToDestructiveMigration()
.allowMainThreadQueries()
.build()
}
}

Hilt 提供了以下组件来绑定依赖与对应的 Android 类。

Hilt 提供的组件 对应的 Android 类
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent View annotated with @WithFragmentBindings
ServiceComponent Service

与 ViewModule 对比?

我们在 Android 组件中注入一个 ViewModels 实例,需要通过 ViewModelFactory 来绑定 ViewModels 实例,传统的调用方式如下所示:

1
复制代码ViewModelProviders.of(this).get(DetailViewModel::class.java)

接下来我们来看一下在 Dagger 和 Hilt 中如何使用 ViewModule。

Dagger

在 Dagger 中,对于每一个 ViewModel,需要告诉 Dagger 如何注入它们,所以我们需要创建 ViewModelModule 文件,在 ViewModelModule 中管理所有的 ViewModel。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Module
abstract class ViewModelModule {

@Binds
@IntoMap
@ViewModelKey(DetailViewModel::class)
abstract fun bindDetailViewModel(viewModel: DetailViewModel): ViewModel

@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}

创建完 ViewModelModule 文件之后,需要在 Component 中添加 ViewModelModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码// Component 声明了所有的 modules
// RoomModule 和数据库相关的
// ActivityAllModule 配置了所有的 activity
// ViewModelModule 配置所有的 ViewModel
@Singleton
@Component(modules = arrayOf(
AndroidInjectionModule::class,
RoomModule::class,
ViewModelModule::class,
ActivitylModule::class))
interface AppCompoment {
fun inject(app: App)

@Component.Builder
interface Builder {
@BindsInstance
fun bindApplication(app: Application): Builder
fun build(): AppCompoment
}

}

Hilt

Hilt 为我们提供了 @ViewModelInject 注解来注入 ViewModel 实例,另外 Hilt 为 SavedStateHandle 类提供了 @Assisted 注解来注入 SavedStateHandle 实例。

1
2
3
4
5
复制代码class HiltViewModel @ViewModelInject constructor(
private val tasksRepository: Repository,
//SavedStateHandle 用于进程被终止时,存储和恢复数据
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel()

Hilt 的局限性

  • Hilt 不支持 ContentProvider,如果你在想在 ContentProvider 中获取 Hilt 提供的依赖,需要使用 @EntryPoint 注解。具体如何使用,可以看之前的内容 在 Hilt 不支持的类中执行依赖注入
  • Hilt 在多模块项目中的局限性,多模块项目大概分为两种类型:
+ 分级模块组成的应用程序。
+ 动态功能模块(dynamic feature modules)组成的应用程序。

分级模块组成的应用程序

如果多模块项目是由分级模块组成的应用程序,那么可以使用 Hilt 来完成依赖注入,分级模块依赖如下图所示。图来自 Google。

从上到下层层依赖,这种情况下可以直接使用 Hilt 进行依赖注入,和在单个 App 模块中使用是一样的,这里不再详述了,Hilt 在多模块中的使用的项目示例 HiltWithMultiModuleSimple 已经上传到 GitHub 上了,代码中有详细的注释。

动态功能模块应用程序

如果是模块项目是 dynamic feature modules (动态功能模块)组成的应用程序,那么使用 Hilt 就有些局限性了,dynamic feature modules 简称 DFMs。

在 DFMs 中,模块之间相互依赖的方式是颠倒的,因此 Hilt 无法在动态功能模块中使用,所以在 DFMs 中只能使用 Dagger 完成依赖注入,在 DFMs 中模块依赖如下图所示。图来自 Google。

一个 App 被分割成一个 Base APK 和多个模块 APK。

Base APK: 这个 APK 包含了基本的代码和资源(services, content providers, permissions)等等,其他被分割的模块 APK 都可以访问,当一个用户请求下载你的应用,这个 APK 首先下载和安装。

Configuration APK:每个 APK 包含特定的资源。当用户下载你的应用程序时,他们的设备只下载和安装针对他们设备的 Configuration APK。

Dynamic Feature APK:每个 APK 包含应用程序的某个功能的代码和资源,这些功能在首次安装应用程序时是不需要的。用户可以按需安装 Feature APK,从而为用户提供额外的功能。每个 Feature APK 都依赖于 Base APK。

例如 dynamic-feature1 依赖于 Base APK,所以在 DFMs 中,模块之间相互依赖的方式是颠倒的。

如何解决 Hilt 在 DFMs 中组件依赖问题?

    1. 在 app module 中或者任何其它可以被 Hilt 处理的模块中,定义一个接口并添加 @EntryPoint 注解,然后添加 @InstallIn 注解指定 module 的范围。
1
2
3
4
5
6
7
8
9
复制代码// LoginModuleDependencies.kt - File in the app module.

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface LoginModuleDependencies {

@AuthInterceptorOkHttpClient
fun okHttpClient(): OkHttpClient
}
    1. 在 dynamic-feature1 模块中,使用 Dagger 中的 @Component 注解,创建 Component 文件,并在 dependencies 中指定通过 @EntryPoint 注解声明的接口。
1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Component(dependencies = [LoginModuleDependencies::class])
interface LoginComponent {

fun inject(activity: LoginActivity)

@Component.Builder
interface Builder {
fun context(@BindsInstance context: Context): Builder
fun appDependencies(loginModuleDependencies: LoginModuleDependencies): Builder
fun build(): LoginComponent
}
}

上面步骤完成之后,就可以在 DFMs 中使用 Dagger 完成依赖注入,就跟我们之前介绍的使用 Dagger 方式一样。

  • 在 Application 中 初始化 Dagger,并实现 HasActivityInjector 接口。
  • 对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 Dagger 如何注入它们。
  • 在每个 modules 中添加 Fragments、Activities 和 ViewModels。
  • 所有的 Activity 继承 BaseActivity,我们需要实现 HasSupportFragmentInjector。

总结

关于 Hilt 在多模块中的使用的项目示例 HiltWithMultiModuleSimple 已经上传的 GitHub 可以前去查看。

到这里 Hilt 入门三部曲终于完结了,从入门、进阶、到落地,所有注解的含义以及项目示例、以及和 Jetpack 组件的使用,Hilt 与 Dagger 不同之处,以及在多模块中局限性以及使用,全部都介绍完了。

  • Jetpack 新成员 Hilt 实践(一)启程过坑记:介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何与 Android 框架类进行绑定,以及他们的生命周期。
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇:分析注解的区别、限定符和作用域注解的使用,以及如何在 ViewModel、App Startup、ContentProvider 中使用等等。
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇:Hilt 与 Dagger 不同之处,以及在多模块中局限性以及使用。

对于使用 Dagger 小伙伴们,应该能够感受到从入门到放弃是什么感觉,Dagger 学习的成本是非常高的,如果项目中引入了 Dagger 意味着团队每个人都要学习 Dagger,无疑这个成本是巨大的,而且使用起来非常的复杂。对于每一个 ViewModel、Fragment 和 Activity 我们需要告诉 Dagger 如何注入它们。

而 Hilt 的学习成本相对于 Dagger 而言成本非常低,Hilt 集成了 Jetpack 库和 Android 框架类,并自动管理它们的生命周期,让开发者只需要关注如何进行绑定,而不需要管理所有 Dagger 配置的问题。

正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 如何在项目中封装 Kotlin + Android Databinding
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:小伙子,你给我说一下线程池的线程复用原理吧

发表于 2020-07-01

前言

前两天和粉丝聊天的时候,粉丝问了我一个挺有意思的问题,说他之前在面试的时候被问到线程池的线程复用原理,当时我跟他简单的说了一下,没想到过了几天又来问我这个问题了,说他最近又被问到了这个问题…….想了想,干脆写篇文章把这个东西讲清楚吧,满满的干货都放在下面了

1.什么是线程复用?

在线程池中,通过同一个线程去执行不同的任务,这就是线程复用。

假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class FixedThreadPoolDemo {

static ExecutorService executorService = Executors.newFixedThreadPool(3);

public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "-> 执行");
});
}
// 关闭线程池
executorService.shutdown();
}

}

执行结果:

1
2
3
4
5
6
7
8
9
复制代码pool-1-thread-1-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行
pool-1-thread-3-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行
...

2.线程复用的原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

3.线程池执行流程

这部分内容在 Java 线程池的各个参数的含义 讨论过,这里我们再复习一次,再从中去了解线程复用。

3.1 流程图

3.2 线程创建的流程

当任务提交之后,线程池首先会检查当前线程数,如果当前的线程数小于核心线程数(corePoolSize),比如最开始创建的时候线程数为 0,则新建线程并执行任务。
当提交的任务不断增加,创建的线程数等于核心线程数(corePoolSize),新增的任务会被添加到 workQueue 任务队列中,等待核心线程执行完当前任务后,重新从 workQueue 中获取任务执行。
假设任务非常多,达到了 workQueue 的最大容量,但是当前线程数小于最大线程数(maximumPoolSize),线程池会在核心线程数(corePoolSize)的基础上继续创建线程来执行任务。
假设任务继续增加,线程池的线程数达到最大线程数(maximumPoolSize),如果任务继续增加,这个时候线程池就会采用拒绝策略来拒绝这些任务。
在任务不断增加的过程中,线程池会逐一进行以下 4 个方面的判断

核心线程数(corePoolSize)
任务队列(workQueue)
最大线程数(maximumPoolSize)
拒绝策略

3.3 ThreadPoolExecutor#execute 源码分析

java.util.concurrent.ThreadPoolExecutor#execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码 public void execute(Runnable command) {
// 如果传入的Runnable的空,就抛出异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 线程池中的线程比核心线程数少
if (workerCountOf(c) < corePoolSize) {
// 新建一个核心线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 核心线程已满,但是任务队列未满,添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command))
// 如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果之前的线程已经被销毁完,新建一个非核心线程
addWorker(null, false);
}
// 核心线程池已满,队列已满,尝试创建一个非核心新的线程
else if (!addWorker(command, false))
// 如果创建新线程失败,说明线程池关闭或者线程池满了,拒绝任务
reject(command);
}

3.4 逐行分析

1
2
3
复制代码//如果传入的Runnable的空,就抛出异常        
if (command == null)
throw new NullPointerException();

execute 方法中通过 if 语句判断 command ,也就是 Runnable 任务是否等于 null,如果为 null 就抛出异常。

1
2
3
4
5
复制代码if (workerCountOf(c) < corePoolSize) { 
if (addWorker(command, true))
return;
c = ctl.get();
}

判断当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程。

addWorker 方法的主要作用是在线程池中创建一个线程并执行传入的任务,如果返回 true 代表添加成功,如果返回 false 代表添加失败。

第一个参数表示传入的任务

第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程(核心线程),大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maximumPoolSize,小于则增加新线程(非核心线程),大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增非核心线程的判断

这一段判断相关源码如下

1
2
3
4
5
6
7
8
复制代码    private boolean addWorker(Runnable firstTask, boolean core) {     
...
int wc = workerCountOf(c);//当前工作线程数
//判断当前工作线程数>=最大线程数 或者 >=核心线程数(当core = true)
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
...

最核心的就是 core ? corePoolSize : maximumPoolSize 这个三目运算。

1
2
3
4
5
6
7
8
9
10
11
复制代码      // 核心线程已满,但是任务队列未满,添加到队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command))
// 如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
reject(command);
else if (workerCountOf(recheck) == 0)
// 如果之前的线程已经被销毁完,新建一个非核心线程
addWorker(null, false);
}

如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过

if (isRunning(c) && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就通过 workQueue.offer(command) 将任务放入任务队列中,

任务成功添加到队列以后,再次检查线程池状态,如果线程池不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下:

1
2
3
复制代码            if (! isRunning(recheck) && remove(command))
// 如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
reject(command);

下面我们再来看后一个 else 分支:

1
2
3
复制代码            else if (workerCountOf(recheck) == 0)
// 如果之前的线程已经被销毁完,新建一个非核心线程
addWorker(null, false);

进入这个 else 说明前面判断到线程池状态为 Running,那么当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了),所以此时如果检查当前线程数为 0,也就是 workerCountOf(recheck) == 0,那就执行 addWorker() 方法新建一个非核心线程。

我们再来看最后一部分代码:

1
2
3
4
复制代码        // 核心线程池已满,队列已满,尝试创建一个非核心新的线程
else if (!addWorker(command, false))
// 如果创建新线程失败,说明线程池关闭或者线程池满了,拒绝任务
reject(command);

执行到这里,说明线程池不是 Running 状态,又或者线程数 >= 核心线程数并且任务队列已经满了,根据规则,此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会再次调用 addWorker 方法并将第二个参数传入 false,传入 false 代表增加非核心线程。

addWorker 方法如果返回 true 代表添加成功,如果返回 false 代表任务添加失败,说明当前线程数已经达到 maximumPoolSize,然后执行拒绝策略 reject 方法。

如果执行到这里线程池的状态不是 Running,那么 addWorker 会失败并返回 false,所以也会执行拒绝策略 reject 方法。

4.线程复用源码分析

java.util.concurrent.ThreadPoolExecutor#runWorker
省略掉部分和复用无关的代码之后,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码    final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 释放锁 设置work的state=0 允许中断
boolean completedAbruptly = true;
try {
//一直执行 如果task不为空 或者 从队列中获取的task不为空
while (task != null || (task = getTask()) != null) {
task.run();//执行task中的run方法
}
}
completedAbruptly = false;
} finally {
//1.将 worker 从数组 workers 里删除掉
//2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的 Worker 进数组 workers
processWorkerExit(w, completedAbruptly);
}
}

可以看到,实现线程复用的逻辑主要在一个不停循环的 while 循环体中。

通过获取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务

直接通过 task.run() 来执行具体的任务(而不是新建线程)

在这里,我们找到了线程复用最终的实现,通过取 Worker 的 firstTask 或者 getTask 方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。

总结

这篇关于线程池的线程复用原理的文章就到这里了,大家看完有什么不懂的欢迎在下方留言评论,也可以私信问我,我看到了一般都会回复的!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【奇技淫巧】使用 ProcessLifecycle 优雅地监

发表于 2020-07-01

前言

很高兴见到你,又来到了「奇技淫巧」系列,本系列介绍一些「骚操作」,可能不适合用于生产,但可以开拓思路

前些天在群里看到有人讨论通过维护 activity 栈来监听程序前后台切换的问题。其实单纯监听程序的前后台切换完全不需要维护 activity 栈,而现在比较主流的做法是使用 registerActivityLifecycleCallbacks。而今天我来介绍一下使用 ProcessLifecycleOwner 来实现这一功能

lifecycle-process 库

Android Jetpack Lifecycle 组件有一个可选库:lifecycle-process,它可以为整个 app 进程提供一个 ProcessLifecycleOwner

lifecycle-process 引入

lifecycle-process 引入

该库十分简单,只有四个文件

lifecycle-process

lifecycle-process

ProcessLifecycleOwnerInitializer 借助 ContentProvider 拿到 Context,用于初始化操作

init

init

EmptyActivityLifecycleCallbacks 为 Application.ActivityLifecycleCallbacks 的实现类,内部为空实现

EmptyActivityLifecycleCallbacks

EmptyActivityLifecycleCallbacks

LifecycleDispatcher 通过 ReportFragment 来 hook 宿主的生命周期事件


核心逻辑都在 ProcessLifecycleOwner 中

ProcessLifecycleOwner

ProcessLifecycleOwner

该类提供了整个 app 进程的 lifecycle

可以将其视为所有 activity 的 LifecycleOwner ,其中 Lifecycle.Event.ON_CREATE 只会分发一次,而 Lifecycle.Event.ON_DESTROY 则永远不会分发

其它的生命周期事件将按以下规则分发:

ProcessLifecycleOwner 会分发 Lifecycle.Event.ON_START 和 Lifecycle.Event.ON_RESUME 事件(在第一个 activity 移动到这些事件时)

Lifecycle.Event.ON_PAUSE 与 Lifecycle.Event.ON_STOP 会在最后一个 activity 移动到这些状态后 延迟 分发,该延迟足够长,以确保由于配置更改等操作重建 activity 后不会分发任何事件

对于监听应用在前后台切换且不需要毫秒级的精度的场景,这十分有用

ProcessLifecycleOwner 源码解析

根据上图我们得知 ProcessLifecycleOwner 实现了 LifecycleOwner 接口

由于在 ProcessLifecycleOwnerInitializer 中初始化时传入了 Context,因此 ProcessLifecycleOwner 在 attach 方法中借助 Context 拿到了 Application 实例,并调用了 registerActivityLifecycleCallbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码void attach(Context context) {
mHandler = new Handler();
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
Application app = (Application) context.getApplicationContext();
app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks()
@RequiresApi(29)
@Override
public void onActivityPreCreated(@NonNull Activity activity,
@Nullable Bundle savedInstanceState) {
//我们需要 ProcessLifecycleOwner 刚好在第一个 activity 的 LifecycleOwner started/resumed 之前获取 ON_START 和 ON_RESUME。
//activity 的 LifecycleOwner 通过在 onCreate() 中添加 activity 注册的 callback 来获取 started/resumed 状态。
//通过在 onActivityPreCreated() 中添加我们自己的 activity 注册的 callback,我们首先获得了回调,同时与 Activity 的 onStart()/ onResume()回调相比仍具有正确的相对顺序

activity.registerActivityLifecycleCallbacks(new EmptyActivityLifecycl
@Override
public void onActivityPostStarted(@NonNull Activity activity) {
activityStarted();
}
@Override
public void onActivityPostResumed(@NonNull Activity activity) {
activityResumed();
}
});
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceStat
//仅在API 29 之前使用 ReportFragment,在此之后,我们可以使用在 onActivityPreCreated() 中注册的 onActivityPostStarted 和 onActivityPostResumed 回调
if (Build.VERSION.SDK_INT < 29) {
ReportFragment.get(activity).setProcessListener(mInitializationLi
}
}
@Override
public void onActivityPaused(Activity activity) {
activityPaused();
}
@Override
public void onActivityStopped(Activity activity) {
activityStopped();
}
});
}

内部维护了 Started 和 Resumed 的数量

1
2
3
4
复制代码private int mStartedCounter = 0;
private int mResumedCounter = 0;
private boolean mPauseSent = true;
private boolean mStopSent = true;

并在 activityStarted 和 activityResumed 方法中对 这两个数值进行 ++,并更改 lifecycle 状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码void activityStarted() {
mStartedCounter++;
if (mStartedCounter == 1 && mStopSent) {
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
mStopSent = false;
}
}
void activityResumed() {
mResumedCounter++;
if (mResumedCounter == 1) {
if (mPauseSent) {
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
mPauseSent = false;
} else {
mHandler.removeCallbacks(mDelayedPauseRunnable);
}
}
}

在 activityPaused 和 activityStopped 方法对这两个数值进行 –

1
2
3
4
5
6
7
8
9
10
复制代码void activityPaused() {
mResumedCounter--;
if (mResumedCounter == 0) {
mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);
}
}
void activityStopped() {
mStartedCounter--;
dispatchStopIfNeeded();
}

而在这里我们看到了上文提到的延迟操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码// 使用 handler 进行延迟操作
mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);

// 延迟 700 ms
static final long TIMEOUT_MS = 700; //mls

private Runnable mDelayedPauseRunnable = new Runnable() {
@Override
public void run() {
// 根据需要分发事件
dispatchPauseIfNeeded();
dispatchStopIfNeeded();
}
};

void dispatchPauseIfNeeded() {
if (mResumedCounter == 0) {
mPauseSent = true;
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}
}
void dispatchStopIfNeeded() {
if (mStartedCounter == 0 && mPauseSent) {
mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
mStopSent = true;
}
}

源码就解析到这里,接下来我们看看如何使用吧

使用

首先引入该库

1
复制代码implementation "androidx.lifecycle:lifecycle-process:2.3.0-alpha05"

由于我们要自定义 lifecycleObserver,因此还需引入

1
复制代码implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-alpha05"

首先创建 ProcessLifecycleObserver 类,实现 DefaultLifecycleObserver 接口,在相应的生命周期中打印 log

接着在自定义 Application 中加入


这样便完成了!

演示

演示

Demo 在这里

系列文章

  • 【奇技淫巧】AndroidStudio Nexus3.x 搭建 Maven 私服遇到问题及解决方案
  • 【奇技淫巧】什么?项目里 gradle 代码超过 200 行了!你可能需要 Kotlin+buildSrc Plugin
  • 【奇技淫巧】gradle 依赖查找太麻烦?这个插件可能帮到你
  • 【奇技淫巧】Android 组件化不使用 Router 如何实现组件间 activity 跳转
  • 【奇技淫巧】新的图片加载库?基于 Kotlin 协程的图片加载库——Coil
  • 【奇技淫巧】使用 Navigation + Dynamic Feature Module 实现模块化
  • 【奇技淫巧】除了 buildSrc 还能这样统一配置依赖版本?巧用 includeBuild
  • 【奇技淫巧】巧用 kotlin 扩展函数和 typealias 封装 带网络状态和解决「粘性」事件的 LiveData

关于我

我是 Flywith24,我的博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉

  • 掘金
  • 简书
  • Github

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

redis的zset有多牛?请把耳朵递过来

发表于 2020-07-01

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

本篇文章很短,但信息量很大,是关于redis的zset。我来分享一点遇到过的线上数据,或许对你的决策有帮助。

redis支持一个数据结构,叫做 zset,也就是有序的列表。当然redis也不能滥用,可以看我以前的规范文章:
《这可能是最中肯的Redis规范了》

忘了zset是个啥的同学可以看这张gif图。

通过它,可以实现游戏排行榜一类的功能,或者实现Topx这样的需求,也能精准的让用户在海量数据中找到自己的位置。

zset的底层结构是跳跃表,而与之类似的Java中的有序Set是TreeSet,使用红黑树实现的。

concurrent包里面,还有一个类叫做ConcurrentSkipListMap,从它的名字就可以看出来,也是用跳跃表实现的,这个和zset最像。

好了,这是前提。广度面试的时候我也会这么问。

我们的问题是:zset中能存放多少条记录?线上有没有有说服力的数据?

先笼统的回答一下,zset理论上支持的元素最多是2^32-1个,约42亿,如果你的内存够大,放下国人绰绰有余。

使用redis-benchmark去测这个效果,不是很可信,测试用例写起来也比较费劲。测完了也不一定信,那就让线上流量去冲击吧。

为了应付产品的需求,我把用户按照省市进行了划分(geohash),结果,用户分布最大的就是广东省,非常棒。

在广东省的zset里,存放了接近6千万的数据,我们就要算在这6千万内任何人的排行。zcard、zrank等一系列操作,easy实现。

运行一段时间后,内存直接飙升到了8G左右。这是由于跳表的特殊结构所引起的,额外的辅助信息会占用更多的内存。

以下是经验值:

  1. 最高TPS写入量1k/秒。
  2. 同时最高QPS查询量5k/秒。
  3. 平均耗时5ms左右。
  4. 百分之95的请求都在10ms以内返回。
  5. 长尾请求超过100ms的不超过100条。

也就是说,在保持高写入和高查询的同时,zset能够保证较低的响应耗时。

你要说再多,我就不知道了,看这些数据,或许还能够再升一把。但要让服务要尽量的稳,压力尽量的分散,就不能太过苛刻,对这个数据我已经很满意了。

这只是一个省份的数据。如果综合起来,上层的业务,就需要承载10w/s的请求。这是非常容易的,但也没有意义,许多高并发经验都是这么吹上去的,要不要去改改简历?

复杂业务高并发才有价值,10w/s请求,给我两台redis就够了,没必要拿来吹。

但也是被zset的性能震惊了一把。跳表的结构,也了解一些,没想到在高并发大数据量场景下,能这么快。

测试数据?没有。本文只是分享一个经验值。对了,redis几乎不占用CPU,你只需要一台2core16gb的服务器就可以了。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用了 springboot + rabbitmq 消息确认机

发表于 2020-07-01

本文收录在个人博客:www.chengxy-nds.top,技术资源共享,一起进步

最近部门号召大伙多组织一些技术分享会,说是要活跃公司的技术氛围,但早就看穿一切的我知道,这 T M 就是为了刷KPI。不过,话说回来这的确是件好事,与其开那些没味的扯皮会,多做技术交流还是很有助于个人成长的。

于是乎我主动报名参加了分享,咳咳咳~ ,真的不是为了那点KPI,就是想和大伙一起学习学习!

在这里插入图片描述

在这里插入图片描述

这次我分享的是 springboot + rabbitmq 如何实现消息确认机制,以及在实际开发中的一点踩坑经验,其实整体的内容比较简单,有时候事情就是这么神奇,越是简单的东西就越容易出错。

可以看到使用了 RabbitMQ 以后,我们的业务链路明显变长了,虽然做到了系统间的解耦,但可能造成消息丢失的场景也增加了。例如:

  • 消息生产者 - > rabbitmq服务器(消息发送失败)
  • rabbitmq服务器自身故障导致消息丢失
  • 消息消费者 - > rabbitmq服务(消费消息失败)

在这里插入图片描述
所以说能不使用中间件就尽量不要用,如果为了用而用只会徒增烦恼。开启消息确认机制以后,尽管很大程度上保证了消息的准确送达,但由于频繁的确认交互,rabbitmq 整体效率变低,吞吐量下降严重,不是非常重要的消息真心不建议你用消息确认机制。


下边我们先来实现springboot + rabbitmq消息确认机制,再对遇到的问题做具体分析。

一、准备环境

1、引入 rabbitmq 依赖包

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2、修改 application.properties 配置

配置中需要开启 发送端和 消费端 的消息确认。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

# 发送者开启 confirm 确认机制
spring.rabbitmq.publisher-confirms=true
# 发送者开启 return 确认机制
spring.rabbitmq.publisher-returns=true
####################################################
# 设置消费端手动 ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 是否支持重试
spring.rabbitmq.listener.simple.retry.enabled=true

3、定义 Exchange 和 Queue

定义交换机 confirmTestExchange 和队列 confirm_test_queue ,并将队列绑定在交换机上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码@Configuration
public class QueueConfig {

@Bean(name = "confirmTestQueue")
public Queue confirmTestQueue() {
return new Queue("confirm_test_queue", true, false, false);
}

@Bean(name = "confirmTestExchange")
public FanoutExchange confirmTestExchange() {
return new FanoutExchange("confirmTestExchange");
}

@Bean
public Binding confirmTestFanoutExchangeAndQueue(
@Qualifier("confirmTestExchange") FanoutExchange confirmTestExchange,
@Qualifier("confirmTestQueue") Queue confirmTestQueue) {
return BindingBuilder.bind(confirmTestQueue).to(confirmTestExchange);
}
}

rabbitmq 的消息确认分为两部分:发送消息确认 和 消息接收确认。

在这里插入图片描述

在这里插入图片描述

二、消息发送确认

发送消息确认:用来确认生产者 producer 将消息发送到 broker ,broker 上的交换机 exchange 再投递给队列 queue的过程中,消息是否成功投递。

消息从 producer 到 rabbitmq broker有一个 confirmCallback 确认模式。

消息从 exchange 到 queue 投递失败有一个 returnCallback 退回模式。

我们可以利用这两个Callback来确保消的100%送达。

1、 ConfirmCallback确认模式

消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {

if (!ack) {
log.error("消息发送异常!");
} else {
log.info("发送者爸爸已经收到确认,correlationData={} ,ack={}, cause={}", correlationData.getId(), ack, cause);
}
}
}

实现接口 ConfirmCallback ,重写其confirm()方法,方法内有三个参数correlationData、ack、cause。

  • correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
  • ack:消息投递到broker 的状态,true表示成功。
  • cause:表示投递失败的原因。

但消息被 broker 接收到只能表示已经到达 MQ服务器,并不能保证消息一定会被投递到目标 queue 里。所以接下来需要用到 returnCallback 。

2、 ReturnCallback 退回模式

如果消息未能投递到目标 queue 里将触发回调 returnCallback ,一旦向 queue 投递消息未成功,这里一般会记录下当前消息的详细投递数据,方便后续做重发或者补偿等操作。

1
2
3
4
5
6
7
8
9
复制代码@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {

@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("returnedMessage ===> replyCode={} ,replyText={} ,exchange={} ,routingKey={}", replyCode, replyText, exchange, routingKey);
}
}

实现接口ReturnCallback,重写 returnedMessage() 方法,方法有五个参数message(消息体)、replyCode(响应code)、replyText(响应内容)、exchange(交换机)、routingKey(队列)。

下边是具体的消息发送,在rabbitTemplate中设置 Confirm 和 Return 回调,我们通过setDeliveryMode()对消息做持久化处理,为了后续测试创建一个 CorrelationData对象,添加一个id 为10000000000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
复制代码@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private ConfirmCallbackService confirmCallbackService;

@Autowired
private ReturnCallbackService returnCallbackService;

public void sendMessage(String exchange, String routingKey, Object msg) {

/**
* 确保消息发送失败后可以重新返回到队列中
* 注意:yml需要配置 publisher-returns: true
*/
rabbitTemplate.setMandatory(true);

/**
* 消费者确认收到消息后,手动ack回执回调处理
*/
rabbitTemplate.setConfirmCallback(confirmCallbackService);

/**
* 消息投递到队列失败回调处理
*/
rabbitTemplate.setReturnCallback(returnCallbackService);

/**
* 发送消息
*/
rabbitTemplate.convertAndSend(exchange, routingKey, msg,
message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
},
new CorrelationData(UUID.randomUUID().toString()));
}

三、消息接收确认

消息接收确认要比消息发送确认简单一点,因为只有一个消息回执(ack)的过程。使用@RabbitHandler注解标注的方法要增加 channel(信道)、message 两个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码@Slf4j
@Component
@RabbitListener(queues = "confirm_test_queue")
public class ReceiverMessage1 {

@RabbitHandler
public void processHandler(String msg, Channel channel, Message message) throws IOException {

try {
log.info("小富收到消息:{}", msg);

//TODO 具体业务

channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

} catch (Exception e) {

if (message.getMessageProperties().getRedelivered()) {

log.error("消息已重复处理失败,拒绝再次接收...");

channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
} else {

log.error("消息即将再次返回队列处理...");

channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}

消费消息有三种回执方法,我们来分析一下每种方法的含义。

1、basicAck

basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。

1
复制代码void basicAck(long deliveryTag, boolean multiple)

deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。

multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。

举个栗子: 假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。

2、basicNack

basicNack :表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。

1
复制代码void basicNack(long deliveryTag, boolean multiple, boolean requeue)

deliveryTag:表示消息投递序号。

multiple:是否批量确认。

requeue:值为 true 消息将重新入队列。

3、basicReject

basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。

1
复制代码void basicReject(long deliveryTag, boolean requeue)

deliveryTag:表示消息投递序号。

requeue:值为 true 消息将重新入队列。

四、测试

发送消息测试一下消息确认机制是否生效,从执行结果上看发送者发消息后成功回调,消费端成功的消费了消息。
在这里插入图片描述
用抓包工具Wireshark 观察一下rabbitmq amqp协议交互的变化,也多了 ack 的过程。
在这里插入图片描述

五、踩坑日志

1、不消息确认

这是一个非常没技术含量的坑,但却是非常容易犯错的地方。

开启消息确认机制,消费消息别忘了channel.basicAck,否则消息会一直存在,导致重复消费。
在这里插入图片描述

2、消息无限投递

在我最开始接触消息确认机制的时候,消费端代码就像下边这样写的,思路很简单:处理完业务逻辑后确认消息, int a = 1 / 0 发生异常后将消息重新投入队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@RabbitHandler
public void processHandler(String msg, Channel channel, Message message) throws IOException {

try {
log.info("消费者 2 号收到:{}", msg);

int a = 1 / 0;

channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);

} catch (Exception e) {

channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}

但是有个问题是,业务代码一旦出现 bug 99.9%的情况是不会自动修复,一条消息会被无限投递进队列,消费端无限执行,导致了死循环。

在这里插入图片描述

在这里插入图片描述

本地的CPU被瞬间打满了,大家可以想象一下当时在生产环境导致服务死机,我是有多慌。

在这里插入图片描述
而且rabbitmq management 只有一条未被确认的消息。

在这里插入图片描述

在这里插入图片描述

经过测试分析发现,当消息重新投递到消息队列时,这条消息不会回到队列尾部,仍是在队列头部。

消费者会立刻消费这条消息,业务处理再抛出异常,消息再重新入队,如此反复进行。导致消息队列处理出现阻塞,导致正常消息也无法运行。

而我们当时的解决方案是,先将消息进行应答,此时消息队列会删除该条消息,同时我们再次发送该消息到消息队列,异常消息就放在了消息队列尾部,这样既保证消息不会丢失,又保证了正常业务的进行。

1
2
3
4
5
复制代码channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// 重新发送消息到队尾
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
message.getMessageProperties().getReceivedRoutingKey(), MessageProperties.PERSISTENT_TEXT_PLAIN,
JSON.toJSONBytes(msg));

但这种方法并没有解决根本问题,错误消息还是会时不时报错,后面优化设置了消息重试次数,达到了重试上限以后,手动确认,队列删除此消息,并将消息持久化入MySQL并推送报警,进行人工处理和定时任务做补偿。

3、重复消费

如何保证 MQ 的消费是幂等性,这个需要根据具体业务而定,可以借助MySQL、或者redis 将消息持久化,通过再消息中的唯一性属性校验。

demo的 GitHub 地址 https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot-rabbitmq-confirm


原创不易,燃烧秀发输出内容,如果有一丢丢收获,点个再看鼓励一下吧!

整理了几百本各类技术电子书,送给小伙伴们。关注公号回复【666】自行领取。和一些小伙伴们建了一个技术交流群,一起探讨技术、分享技术资料,旨在共同学习进步,如果感兴趣就扫码加入我们吧!


本文使用 mdnice 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

被面试官问懵B了,十亿级数据ES搜索怎么优化?

发表于 2020-06-30

面试题

es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?

面试官心理分析

这个问题是肯定要问的,说白了,就是看你有没有实际干过 es,因为啥?其实 es 性能并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能你会懵逼的发现,跑个搜索怎么一下 510s,坑爹了。第一次搜索的时候,是510s,后面反而就快了,可能就几百毫秒。

你就很懵,每个用户第一次访问都会比较慢,比较卡么?所以你要是没玩儿过 es,或者就是自己玩玩儿 demo,被问到这个问题容易懵逼,显示出你对 es 确实玩儿的不怎么样?

面试题剖析

说实话,es 性能优化是没有什么银弹的,啥意思呢?就是不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。

性能优化的杀手锏——filesystem cache

你往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去。

es 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

性能差距究竟可以有多大?我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1秒、5秒、10秒。但如果是走 filesystem cache,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。

这里有个真实的案例。某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 64 * 3 = 192G。每台机器给 es jvm heap 是 32G,那么剩下来留给 filesystem cache的就是每台机器才 32G,总共集群里给 filesystem cache 的就是 32 * 3 = 96G 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,es 数据量是 1T,那么每台机器的数据量是 300G。这样性能好吗?filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。

归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。

根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 es 中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。

比如说你现在有一行数据。id,name,age …. 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase 这么一个架构。

hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。

写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。然后你从 es 检索可能就花费 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。

数据预热

假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。

其实可以做数据预热。

举个例子,拿微博来说,你可以把一些大V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。

document 模型设计

对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。

最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。

document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。

分页性能优化

es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。

分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你必须得从每个 shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。你翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。

我们之前也是遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。

有什么解决方案吗?

不允许深度分页(默认深度分页性能很差)

跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。

类似于 app 里的推荐商品不断下拉出来一页一页的

类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api,关于如何使用,自行上网搜索。

scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。但是,唯一的一点就是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。也就是说,你不能先进入第 10 页,然后去第 120 页,然后又回到第 58 页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,app,也有一些网站,做的就是你只能往下拉,一页一页的翻。

初始化时必须指定 scroll 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。

除了用 scroll api,你也可以用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。

来源 | 8rr.co/5Csc

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…798799800…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%