自定义注解+AOP记录系统异常日志

360 阅读2分钟

🦄最近在学习Java时遇到了这样一段代码。看到下面第六行代码的时候有点懵🥵,仔细阅读后,发现作者采用了aop自定义注解的方式😕,在系统发生未知的运行时异常的时候,自动获取异常发生的位置、异常栈信息并将异常保存在数据库中。 使用AOP统一处理系统级别的异常,使代码可读性简洁性更好😁。

/** 拦截未知的运行时异常 */
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResultVo runtimeException(RuntimeException e) {
    ResultExceptionAdvice resultExceptionAdvice = SpringContextUtil.getBean(ResultExceptionAdvice.class);
    resultExceptionAdvice.runtimeException(e);
    log.error("【系统异常】", e);
    return ResultVoUtil.error(500, "未知错误:EX8846");
}

经过梳理,大概步骤如下:

  1. 定义异常通知器
/**
 * 异常通知器
 */
@ControllerAdvice
public class ResultExceptionAdvice {
    /** 运行切入程序集合 */
    private List<ExceptionAdvice> proceed = new ArrayList<>();

    /** 添加切入程序 */
    public void putProceed(ExceptionAdvice advice){
        proceed.add(advice);
    }

    /** 执行异常通知 */
    public void runtimeException(RuntimeException e){
        for (ExceptionAdvice ea : proceed) {
            ExceptionAdvice advice = SpringContextUtil.getBean(ea.getClass());
            //调用了advice的实现类(ActionLogProceedAdvice)的run()方法
            advice.run(e);
        }
    }
}
  1. 定义异常通知器接口及实现类
/**
 * 异常通知器接口
 */
public interface ExceptionAdvice {
    void run(RuntimeException e);
}
public class ActionLogProceedAdvice implements ExceptionAdvice {

    @Override
    @ActionLog(key = SystemAction.RUNTIME_EXCEPTION, action = SystemAction.class)
    public void run(RuntimeException e) {}
}
  1. 自定义@ActionLog注解
/**
 * 行为日志注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ActionLog {
    // 日志名称
    String name() default "";
    // 日志消息
    String message() default "";
    // 行为key
    String key() default "";
    // 行为类
    Class<? extends BaseActionMap> action() default BaseActionMap.class;
}
  1. 行为日志注解AOP
/**
 * 行为日志注解AOP
 */
@Aspect
@Component
@Slf4j
public class ActionLogAop {
    private final static String DEFAULT_ACTION_NAME = "default";

    @Pointcut("@annotation(com.linln.component.actionLog.annotation.ActionLog)")
    public void actionLog() {};

    @Around("actionLog()")
    public Object recordLog(ProceedingJoinPoint point) throws Throwable {
        // 先执行切入点,获取返回值
        Object proceed = point.proceed();

        /* 读取ActionLog注解消息 */
        Method targetMethod = ((MethodSignature)(point.getSignature())).getMethod();
        com.linln.component.actionLog.annotation.ActionLog anno =
                targetMethod.getAnnotation(com.linln.component.actionLog.annotation.ActionLog.class);
        // 获取name值
        String name = anno.name();
        // 获取message值
        String message = anno.message();
        // 获取key值,这里key为 "runtime_exception"
        String key = anno.key();
        // 获取行为模型,这里action就是SystemAction.class
        Class<? extends BaseActionMap> action = anno.action();
        BaseActionMap instance = action.newInstance();
        Object actionModel = instance.get(!key.isEmpty() ? key : DEFAULT_ACTION_NAME);
        Assert.notNull(actionModel, "无法获取日志的行为方法,请检查:"+point.getSignature());

        // 封装日志实例对象
        ActionLog actionLog = new ActionLog();
        actionLog.setIpaddr(ShiroUtil.getIp());
        actionLog.setClazz(point.getTarget().getClass().getName());
        actionLog.setMethod(targetMethod.getName());
        actionLog.setType(((ActionModel) actionModel).getType());
        actionLog.setName(!name.isEmpty() ? name : ((ActionModel) actionModel).getName());
        actionLog.setMessage(message);
        actionLog.setOperBy(ShiroUtil.getSubject());
        if(ShiroUtil.getSubject() != null){
            actionLog.setOperName(ShiroUtil.getSubject().getNickname());
        }

        //判断是否为普通实例对象
        if(actionModel instanceof BusinessType){
            actionLog.setMessage(((BusinessType) actionModel).getMessage());
        }else {
            // 重置日志-自定义日志数据
            ResetLog resetLog = new ResetLog();
            resetLog.setActionLog(actionLog);
            resetLog.setRetValue(proceed);
            resetLog.setJoinPoint(point);
            try {
                //得到action表示的类中指定名称,且形参为ResetLog.class类型的方法,其中((BusinessMethod)actionModel).getMethod()的计算结为"runtimeException"
                Method method = action.getDeclaredMethod(((BusinessMethod)actionModel).getMethod(), ResetLog.class);
                method.invoke(instance, resetLog);
                if(!resetLog.getRecord()) {
                    return proceed;
                }
            } catch (NoSuchMethodException e) {
                log.error("获取行为对象方法错误!请检查方法名称是否正确!", e);
                e.printStackTrace();
            }
        }

        // 保存日志
        ActionLogService actionLogService = SpringContextUtil.getBean(ActionLogService.class);
        actionLogService.save(actionLog);

        return proceed;
    }
}

上面代码中@Pointcut和@Around可以合并写在一起,如下图

image.png

  1. 其他涉及到的类...略

文章内的代码来自:gitee.com/aun