在函数式接口的限定下,优雅地跨方法栈帧保存日志

在函数式接口的限定下,优雅地跨方法栈帧保存日志

使用背景:

一个函数式接口(如下文提到的重试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;

猜你喜欢

转载自blog.csdn.net/zjx130/article/details/87971278