在函数式接口的限定下,优雅地跨方法栈帧保存日志
使用背景:
一个函数式接口(如下文提到的重试IO请求的接口),其必定有需要实现的功能方法(如:发起IO请求)。
在这种前提下,如果想要在函数式接口中新增“保存日志”等功能的抽象方法,以便在某个特定的模板方法中使用,这会破坏函数式接口本身。(有且仅有一个抽象方法的接口是函数式接口)
那么,你可能会问,为什么要使用函数式接口?我给出一个函数式接口调用的例子:
// 构造函数式接口的具体实现类
RetryIoRequestFunc<Void,Void> retryIoRequestFunc = (param) -> {
mailService.sendRichEmail(emailInfo);
return null;
};
// 调用接口的模板方法
retryIoRequestFunc.retryWhenIOError(
null,taskLogDao,(e -> { saveTaskLog(e,unbalancedUserList); }));
}
可以看到,构造一个函数式接口的实现类非常方便,可以使用lambda表达式。
当然,你也可以不使用函数式接口来实现你的功能,这只是一种编程习惯,并非强制。此文章也只是将自己有关函数式编程的心得说出,并非是解决某一特定问题的最佳实践。
下面,我们就以 “重试IO请求” 此函数式接口为例,分析如何在其中加入 “保存日志” 的功能。
一.日志接口
/**
* 所有日志Dao层都需要实现的接口
* 此接口可以帮助你完成"在函数式接口的限定下,优雅地跨方法栈帧保存日志"
* @author: zack
* @create: 2019-02-21 18:45
*/
public interface BaseLogRepository<T extends BaseEntity> extends BaseStringRepository<T> {
/**
* 调用{@link SaveLogFunc}函数式接口的实现,对日志进行跨方法栈的保存
* 使用案例:{@link com.cesgroup.coin.coin.task.NotifyMgrUnbalancedUserTask#notifyMgrUnbalancedUserWithEmail}中
* 最后的retryIoRequestFunc.retryWhenIOError()方法
* @param saveLogFunc 保存日志的具体函数(不可为null)
* @param e 日志需要保存的异常信息(可以为null)
*/
default void saveLog(SaveLogFunc saveLogFunc,Exception e) {
saveLogFunc.saveLog(e);
}
}
二.保存日志这个行为的函数抽象
/**
* 保存日志这个行为的函数抽象(记录日志保存的相关方法)
* @author: zack
* @create: 2019-02-21 18:57
*/
@FunctionalInterface
public interface SaveLogFunc {
/**
* 保存日志的动作的抽象,需要具体的实现
*/
void saveLog(Exception e);
}
三.使用实例
3.1 具体使用场景(发送邮件时需要保存异常)
// ........上面代码略.........
// 6.构造邮件的发送行为(邮件可能会发送失败,所以使用失败重试机制进行邮件的发送)
RetryIoRequestFunc<Void,Void> retryIoRequestFunc = (param) -> {
mailService.sendRichEmail(emailInfo);
return null;
};
// 7.发送邮件(当多次重试都失败,需要将此情况记入定时任务的日志中,所以用3个参数的方法)
retryIoRequestFunc.retryWhenIOError(
null,taskLogDao,(e -> { saveTaskLog(e,unbalancedUserList); }));
}
3.2 具体的保存日志的行为
/**
* 保存定时任务的执行结果日志
*/
private void saveTaskLog(Exception e, List<UnbalancedUserVo> unbalancedUserList) {
TaskLog taskLog = new TaskLog();
taskLog.setTaskName(TASK_NAME);
taskLog.setOperateTarget(ERROR_SUBJECT);
taskLog.setRemark(unbalancedUserList.toString().substring(0,250)); //取头不取尾,250个字符串
taskLog.setOperateError(e.getMessage());
taskLog.setOperateResult(Constant.DEFAULT_NO);
taskLog.setType(TaskLog.TaskType.EMAIL_ERROR);
taskLog.setOperateTime(new Date());
taskLogDao.save(taskLog);
}
3. 3 上面例子的补充说明(重试IO请求的具体实现)
/**
* 发生IO异常,希望可以重试,用此函数接口的默认方法。
* 注:使用此接口需要实现{@link #methodToRetry}这个模板方法
* 使用案例:{@link com.cesgroup.coin.wechat.web.WeixinPageAuthController#getWxUserInfo}
* @author: zack
* @create: 2019-02-26 17:07
*/
@FunctionalInterface
public interface RetryIoRequestFunc<R,P> {
final Logger log = LoggerFactory.getLogger(RetryIoRequestFunc.class);
/**
* //出现UnknownHostException等异常(DNS解析错误)时的最大重试次数
*/
int MAX_RETRY_TIMES = 3;
/**
*重试的等待间隔(毫秒)
*/
int RETRY_SLEEP_MILLIS = 500;
/**
* 重载方法,多次重试失败后,不保存错误日志到数据库
* @see #retryWhenIOError(Object, BaseLogRepository, SaveLogFunc)
*/
default R retryWhenIOError(P param) {
return retryWhenIOError(param,null,null);
}
/**
* 当用户希望发生IO异常可以重试时,使用下面的方法。
* 经验上看,发生的IO异常多是UnknownHostException,其是因为网络不稳定造成的DNS解析异常
* 注:使用此方法需要实现{@link #methodToRetry}这个模板方法
* 重试并保存错误日志的使用案例:{@link com.cesgroup.coin.coin.task.NotifyMgrUnbalancedUserTask#notifyMgrUnbalancedUserWithEmail}
* @param repository 如何发生异常时需要保存日志到数据库,用{@link BaseLogRepository}接口的默认方法saveLog
* @param saveLogFunc {@link BaseLogRepository#saveLog(SaveLogFunc, Exception)}保存日志所需的执行函数
* @see BaseLogRepository#saveLog
*/
default R retryWhenIOError(P param, BaseLogRepository repository, SaveLogFunc saveLogFunc) {
int retryTimes = 0;
try {
do {
try {
return methodToRetry(param);
} catch (Exception e) {
User currentUser = (User) RequestUtils.getSession().getAttribute(WeixinConstant.SESSION_USER);
String userName = "";
if (currentUser != null) {
userName = currentUser.getUserName();
}
log.error("用户:{},第{}次重试,遇到的异常:{}",userName,retryTimes + 1,e);
Throwable cause = e.getCause();
// 经验上看,UnknownHostException是由DNS域名解析失败引起的,而IOException是UnknownHostException的父类
if (cause instanceof IOException) {
int sleepMillis = this.RETRY_SLEEP_MILLIS * (1 << retryTimes);
try {
log.warn("I/O异常,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
Thread.sleep(sleepMillis);
} catch (InterruptedException e1) {
throw new RuntimeException(e1);
}
} else {
throw e;
}
}
}
while (retryTimes++ < this.MAX_RETRY_TIMES);
log.warn("重试达到最大次数【{}】", this.MAX_RETRY_TIMES);
throw new RuntimeException("I/O异常,超出重试次数");
} catch (Exception e) {
log.error("请求发生未知异常,{},{}",e.getMessage(),e);
if (repository != null && saveLogFunc != null) {
repository.saveLog(saveLogFunc,e); //保存异常信息到数据库
}
return null;
}
}
/**
* 期望能够失败重试的I/O请求方法
*/
public abstract R methodToRetry(P param) throws Exception;