2. 报警系统QuickAlarm之报警执行器的设计与实现

文章目录
  1. I. AlarmExecute接口定义
    1. 1. 基础知识
    2. 2. 接口定义
    3. 3. 额外说明
  2. II. AlarmExecute的加载
    1. 1. 问题分析
      1. 1. 开放一个注册接口
      2. 2. 抽象工厂
      3. 3. 借助Spring容器来加载
      4. 4. SPI加载方式
    2. 2. 实现
  3. III. AlarmExecute内部实现
  4. IV. 小结
  5. V. 其他
    1. 相关博文
    2. 项目
    3. 个人博客: Z+|blog
    4. 声明
    5. 扫描关注,java分享

根据前面一篇总纲的博文,将整体结构划分为了四大块,本文则主要目标集中在第一块,报警执行器(AlarmExecute)的设计与加载上了

主要的关注点无外乎 定义-》加载-》实现逻辑三块了:

  • AlarmExecute 的接口定义
  • 如何加载用户自定义的AlarmExecute
  • AlarmExecute的内部实现

I. AlarmExecute接口定义

在定义接口之前,先来根据几个问题来加深下这个概念的理解:

1. 基础知识

  1. 说一下这个报警执行器到底是干嘛的?
  • 执行具体的报警逻辑(感觉说了依据废话)
  • 因此不同的报警方式,可以选择不同的实现,这个强业务关联的逻辑可以交由适用方自己来把控
  1. 多个alarmExecute之间如何区分?
  • 给一个类似身份证的标识,将标识与alarmExecute绑定,则可以报警规则中,用这个标识来表示对应的报警执行器
  • 标识要求全局唯一,否则就没法找到对应的执行器

2. 接口定义

根据上面的基础知识,那么很容易给出接口的定义了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface IExecute {

/**
* 报警的具体实现
*
* @param users 报警用户,支持批量
* @param title 报警信息的title
* @param msg 报警的主题信息
*/
void sendMsg(List<String> users, String title, String msg);


/**
* 获取报警单元唯一标识
*
* @return name 要求全局唯一
*/
default String getName() {
return ExecuteNameGenerator.genExecuteName(this.getClass());
}
}
  • 第一个方法sendMsg也就是需要使用者来实现的具体执行报警代码的核心模块了,比较清晰,其中用户是列表,因此,支持同时报警给多个用户(但是报警内容都是相同的)
  • 第二个方法getName表示获取标识,默认给了一个实现,规则如下
    • 获取类的 SimpleName
    • 干掉类名后面的 Execute (如果不是以这个结尾的就不需要了)
    • 剩下的全部转大写
    • 实例: SmsExecute -> SMS; LogExecute -> LOG;

3. 额外说明

上面接口定义中的sendMsg中,支持给多个用户发送报警信息,如果要求每个报警信息都不同,比如最常见的是:

  • 发送一段文本,其中通知人地方根据报警人来替换,其他的不变

当然这样的场景完全可以自己在实现中来做

  • 传入的content作为一个话术模板
  • 然后利用 String#format() 来实现参数代替

当然更激进一点就是,穿进来的title或者content作为一个key,然后我可以通过这个key,到其他的地方(如db,缓存等)获取报警内容,甚至我连传进来的报警人都不care,直接从其他地方来获取

简单来说,这个实现委托给用户自己实现,你完全可以随意的控制,做任何你想做的事情

II. AlarmExecute的加载

1. 问题分析

加载AlarmExecut,貌似没有什么特别复杂的东西,一般的思路是创建一个简单工厂类,然后实例化对应的Executor返回,(再多一点确保只有一个实例对象,加以缓存)

这样有什么问题?

很简单的实现,但是我们需要加载用户自定义的执行器,要怎么支持呢?

几种可行的解决手段

1. 开放一个注册接口

这个可算是最容易想到的了,直接让用户把自己的Executor实例,主动的扔进来

2. 抽象工厂

将前面说的简单工厂,改成抽象工厂类,让后具体的加载委托给用户自己来做

3. 借助Spring容器来加载

如果所有的AlarmExecute都委托给Spring容器来管理,那么就很简单了,直接通过ApplicationContext#getBean来获取所有的执行器即可

4. SPI加载方式

通过JDK的spi机制来实现(详细后面来说)

针对上面的几个手段,首先排除掉前面两个,因为不满足我们的设计目标一:

  • 简单 (只有报警这个接口进行交互,不需要额外的接口调用)

然后也排除掉spring容器,因为我们希望这个东西,可以较独立的被引用到java工程中,后面可以看情况实现一个spring版

从使用来讲,由spring容器来托管的方式,对使用者而言,是最简单,成本最低的,因为不需要额外添加SPI配置


2. 实现

我们采用SPI方式来实现加载,对于SPI是什么东西,这里不详细展看,有兴趣的童鞋可以看我之前的一个系类博文:自定义SPI框架设计

实现方式,可说是非常简单了

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
public class SimpleExecuteFactory {
private static Map<String, IExecute> cacheMap;
private static void loadAlarmExecute() {
Map<String, IExecute> map = new HashMap<>();
Iterator<IExecute> iExecutes = ServiceLoader.load(IExecute.class).iterator();
IExecute tmp;
while (iExecutes.hasNext()) {
tmp = iExecutes.next();
if (!map.containsKey(tmp.getName())) {
map.put(tmp.getName(), tmp);
} else {
throw new DuplicatedAlarmExecuteDefinedException(
"duplicated alarm execute defined!" +
"\n" +
">>name:" +
tmp.getName() +
">>>clz:" +
tmp.getClass() +
">>>clz:" +
map.get(tmp.getName())
);
}
}

cacheMap = map;
}

public static IExecute getExecute(String execute) {
if (cacheMap == null) {
synchronized (SimpleExecuteFactory.class) {
if (cacheMap == null) {
loadAlarmExecute();
}
}
}

// 如果不存在,则降级为 LogExecute
IExecute e = cacheMap.get(execute);
return e == null ? cacheMap.get(LogExecute.NAME) : e;
}
}

上面对外就暴露一个方法,内部比较简单,如果传入标识对应的报警器没有,则返回一个默认的,确保不会因此挂掉

通过SPI加载所有的执行器的逻辑就一行

1
Iterator<IExecute> iExecutes = ServiceLoader.load(IExecute.class).iterator();

然后需要关注的是循环内部,做了name的唯一性判断,不满足就直接抛出异常了

III. AlarmExecute内部实现

内部提供了两个基本的报警实现,比较简单

日志报警执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 有些报警,不需要立即上报,但是希望计数, 当大量出现时, 用于升级
* <p/>
* Created by yihui on 2017/4/28.
*/
public class LogExecute implements IExecute {
public static final String NAME = ExecuteNameGenerator.genExecuteName(LogExecute.class);

private static final Logger logger = LoggerFactory.getLogger("alarm");

@Override
public void sendMsg(List<String> users, String title, String msg) {
logger.info("Do send msg by {} to user:{}, title: {}, msg: {}", getName(), users, title, msg);
}
}

空报警执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 空报警执行器, 什么都不干
* <p>
* Created by yihui on 2017/5/12.
*/
public class NoneExecute implements IExecute {
public static final String NAME = ExecuteNameGenerator.genExecuteName(NoneExecute.class);

@Override
public void sendMsg(List<String> users, String title, String msg) {

}
}

IV. 小结

AlarmExecute 的定义,加载以及实现规则目前都已经完成

  • 定义:两个方法,一个执行报警方法,一个返回唯一标识方法
  • 加载:通过SPI方式加载所有定义的alarmExecute
  • 实现:由用户自定义实现IExecute接口,内部逻辑无任务特殊要求,只是需要确保每个executor的name唯一

整个系统的第一步已经迈出,但是有个问题就是什么时候,才会来调用 com.hust.hui.alarm.core.execut.SimpleExecuteFactory#getExecute 从而触发执行器的加载呢?

IMAGE

V. 其他

相关博文

  1. 报警系统QuickAlarm总纲
  2. 报警系统QuickAlarm之报警执行器的设计与实现
  3. 报警系统QuickAlarm之报警规则的设定与加载
  4. 报警系统QuickAlarm之报警规则解析
  5. 报警系统QuickAlarm之频率统计及接口封装
  6. 报警系统QuickAlarm使用手册
  7. 报警系统QuickAlarm之默认报警规则扩展

项目

个人博客: Z+|blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正,我的微博地址: 小灰灰Blog

扫描关注,java分享

QrCode

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×