业务背景
一个虚拟货币系统,需要日常监控,例如每日的新增流量统计、交易流水统计、异常交易统计等。
每日统计一次以上信息,并将统计信息添加到excel表中,以邮件的形式进行发送。
业务需求
1.可以在监控使用者无感知的情况下,添加新的监控任务
2.监控信息以excel文件形式告知管理者
监控设计
1.首先看监控使用者(定时任务)如何触发所有的监控:
2.monitorExecutor的代码
/**
* 监控任务链的执行器。外部想执行所有监控请调用{@link #execute()}方法
* 使用案例:{@link com.cesgroup.coin.cron.SystemMonitorTask#executeMonitorTask}
* createTime: 2019-04-19 14:45
* @author zack
*/
@Slf4j
@Component
@ConditionalOnBean(CoinMonitor.class)
public class MonitorExecutor implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
private Map<String, CoinMonitor> coinMonitorMap;
private volatile CoinMonitor endOfMonitorChain;
@Override
public void afterPropertiesSet() {
this.coinMonitorMap = applicationContext.getBeansOfType(CoinMonitor.class);
log.info("加载的CoinMonitor:{}",coinMonitorMap);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 执行监控任务链,并将监控信息汇总到一个数据Map,以进行和Excel模板附件的结合,最后生成邮件附件Resource
* 注:easyPoi的模板和数据只能结合一次。所以每个监控的internalExecute()方法传递的是数据而不是生成的文件流。
* @return 因为我们系统的监控信息都是以excel文件形式告知管理者,所以监控链的最终产物是文件
*/
public FileSystemResource execute() {
initMonitorChain();
assert endOfMonitorChain != null;
//执行所有监控,获得包含所有监控信息的excel附件
return endOfMonitorChain.execute();
}
private void initMonitorChain() {
if (endOfMonitorChain == null) {
synchronized (this) {
if (endOfMonitorChain == null) {
int index = 0;
CoinMonitor monitor = null;
for (CoinMonitor nextMonitor : coinMonitorMap.values()) {
if (index == 0) {
monitor = nextMonitor;
} else {
monitor = monitor.addMonitor(nextMonitor);
}
index++;
}
this.endOfMonitorChain = monitor;
}
}
}
}
}
3.所有监控的父类,CoinMonitor
/**
* 所有监控类都必须继承此接口,并被纳入spring的bean管理。如此才能每天定时执行监控任务。
* 监控执行入口:{@link MonitorExecutor#execute}
* 监控类示例:{@link UserBalanceMonitor}
* createTime: 2019-04-17 10:38
* @author zack
*/
public abstract class CoinMonitor {
protected Logger log = LoggerFactory.getLogger(CoinMonitor.class);
private CoinMonitor lastMonitor;
/**
* 添加下一个需要执行的监控,以便汇总监控信息,统一将信息添加到邮件附件
*/
CoinMonitor addMonitor(CoinMonitor nextMonitor) {
nextMonitor.lastMonitor = this;
return nextMonitor;
}
/**
* 执行监控任务链,并将监控信息汇总到一个数据Map,以进行和Excel模板附件的结合,最后生成邮件附件Resource
* 注:easyPoi的模板和数据只能结合一次。所以每个监控的internalExecute()方法传递的是数据而不是生成的文件流。
* @return 因为我们系统的监控信息都是以excel文件形式告知管理者,所以监控链的最终产物是文件
*/
FileSystemResource execute() {
Map<Integer, Map<String, Object>> sheetsData = new HashMap<>();
sheetsData = execute(sheetsData);
return mergeDataIntoExcelTemplate(sheetsData);
}
/**
* @param sheetsData 上一个监控产生的Excel附件所需的Data
* @return excel监控附件所需数据,key是excel表单的sheet的num,从0开始
*/
private Map<Integer, Map<String, Object>> execute(Map<Integer, Map<String, Object>> sheetsData) {
sheetsData = addMonitorData(sheetsData);
if (lastMonitor != null) {
sheetsData = lastMonitor.execute(sheetsData);
}
return sheetsData;
}
/**
* 将数据和excel模板相结合,生成最终的excel附件
* @param sheetsData excel监控附件所需的全部数据,key是excel表单的sheet的num,从0开始
* @return 邮件Excel附件Resource对象
*/
private FileSystemResource mergeDataIntoExcelTemplate(Map<Integer, Map<String, Object>> sheetsData) {
TemplateExportParams excelTemplate = new TemplateExportParams(
Constant.MONOTOR_EXCEL_TEMPLATE_CLASSPATH,true);
Map<Integer, Map<String, Object>> defaultSheetsData = addDataToEverySheet(excelTemplate,sheetsData);
try (FileOutputStream fos =
new FileOutputStream(Constant.MONITOR_TEMP_DATA_PATH)) {
Workbook workbook = MutiSheetExcelExportUtil.exportExcel(defaultSheetsData, excelTemplate);
workbook.write(fos);
} catch (Exception e) {
log.error("导出Excel监控数据异常:{}",e);
}
return new FileSystemResource(Constant.MONITOR_TEMP_DATA_PATH);
}
/**
* 实现为excel的每个sheet页添加空的数据,避免某个monitor因为异常没有对相应sheet页提供数据,造成全部监控信息的丢失
* @param excelTemplate 监控信息的excel模板
* @param sheetsData 所有监控器产生的监控信息
* @return 就算某个sheet页没有信息,也为其赋予空map
*/
@SuppressWarnings("unchecked")
private Map<Integer, Map<String, Object>> addDataToEverySheet(TemplateExportParams excelTemplate,
Map<Integer, Map<String, Object>> sheetsData) {
Workbook wb = ExcelCache.getWorkbook(excelTemplate.getTemplateUrl(), excelTemplate.getSheetNum(),
excelTemplate.isScanAllsheet());
int sheetNum = wb.getNumberOfSheets();
Map<Integer, Map<String, Object>> defaultSheetsData = new HashMap<>();
for (int i = 0;i < sheetNum;i++) {
defaultSheetsData.put(i,new HashMap<>());
}
for (Map.Entry entry : sheetsData.entrySet()) {
defaultSheetsData.put((Integer) entry.getKey(),(Map<String, Object>) entry.getValue());
}
return defaultSheetsData;
}
/**
* 每个监控器需要执行的监控任务
* 示例:{@link UserBalanceMonitor#addMonitorData}
* @param sheetsData excel附件所需数据的集合,key是sheet的num,从0开始
* @return 添加上本次监控信息的map数据集(key是sheet的num,value是对应sheet的数据)
*/
protected abstract Map<Integer, Map<String, Object>> addMonitorData(Map<Integer, Map<String, Object>> sheetsData);
}