根据前面一篇总纲的博文,将整体结构划分为了四大块,本文则主要目标集中在第一块,报警执行器(AlarmExecute)的设计与加载上了
主要的关注点无外乎 定义-》加载-》实现逻辑三块了:
在定义接口之前,先来根据几个问题来加深下这个概念的理解:
根据上面的基础知识,那么很容易给出接口的定义了
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
表示获取标识,默认给了一个实现,规则如下
Execute
(如果不是以这个结尾的就不需要了) SmsExecute -> SMS; LogExecute -> LOG;
上面接口定义中的 sendMsg
中,支持给多个用户发送报警信息,如果要求每个报警信息都不同,比如最常见的是:
当然这样的场景完全可以自己在实现中来做
当然更激进一点就是,穿进来的title或者content作为一个key,然后我可以通过这个key,到其他的地方(如db,缓存等)获取报警内容,甚至我连传进来的报警人都不care,直接从其他地方来获取
简单来说,这个实现委托给用户自己实现,你完全可以随意的控制,做任何你想做的事情
加载AlarmExecut,貌似没有什么特别复杂的东西,一般的思路是创建一个简单工厂类,然后实例化对应的Executor返回,(再多一点确保只有一个实例对象,加以缓存)
很简单的实现,但是我们需要加载用户自定义的执行器,要怎么支持呢?
这个可算是最容易想到的了,直接让用户把自己的Executor实例,主动的扔进来
将前面说的简单工厂,改成抽象工厂类,让后具体的加载委托给用户自己来做
如果所有的AlarmExecute都委托给Spring容器来管理,那么就很简单了,直接通过 ApplicationContext#getBean
来获取所有的执行器即可
通过JDK的spi机制来实现(详细后面来说)
针对上面的几个手段,首先排除掉前面两个,因为不满足我们的设计目标一:
然后也排除掉spring容器,因为我们希望这个东西,可以较独立的被引用到java工程中,后面可以看情况实现一个spring版
从使用来讲,由spring容器来托管的方式,对使用者而言,是最简单,成本最低的,因为不需要额外添加SPI配置
我们采用SPI方式来实现加载,对于SPI是什么东西,这里不详细展看,有兴趣的童鞋可以看我之前的一个系类博文:自定义SPI框架设计
实现方式,可说是非常简单了
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加载所有的执行器的逻辑就一行
Iterator<IExecute> iExecutes = ServiceLoader.load(IExecute.class).iterator();
然后需要关注的是循环内部,做了name的唯一性判断,不满足就直接抛出异常了
内部提供了两个基本的报警实现,比较简单
日志报警执行器
/** * 有些报警,不需要立即上报,但是希望计数, 当大量出现时, 用于升级 * <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); } }
空报警执行器
/** * 空报警执行器, 什么都不干 * <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) { } }
AlarmExecute 的定义,加载以及实现规则目前都已经完成
IExecute
接口,内部逻辑无任务特殊要求,只是需要确保每个executor的name唯一 整个系统的第一步已经迈出,但是有个问题就是什么时候,才会来调用 com.hust.hui.alarm.core.execut.SimpleExecuteFactory#getExecute
从而触发执行器的加载呢?