java使用自定义注解实现业务操作日志保存

  • 整体描述
  • 具体实现
  • 1. 创建自定义注解
  • 2. 创建自定义注解解析器
  • 2.1 方法说明:请求前
  • 2.2 方法说明:请求中
  • 2.3 方法说明:请求后
  • 2.4 方法说明:拦截异常
  • 2.5 注解解析器完整代码
  • 3. 在controller层使用注解
  • 结语


整体描述

使用Springboot框架,之前项目中有Log的注解,但是不满足项目需求,需要对指定的几个接口进行操作日志的保存,这里对Log注解进行改造。过程不算复杂,再次记录一下。
注:在此之前,需要创建数据库和相关Mapper和Service的基础类,这个也不是本文的重点,而且是非常基础的操作,就不在文章里写了。需要将操作日志存入数据库的话,此步骤是必须的。

具体实现

1. 创建自定义注解

创建一个自定义注解BUsinessLog,在里面定义注解所需要的参数。这里加了一个MethodName的参数,用于在处理注解的时候,根据MethodName进行不同的业务处理。

/**
 * 自定义操作日志记录注解
 *
 * @author nhx
 * @date 2022-10-14
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BusinessLog {
    /**
     * 操作人名称
     */
    public String OperationName() default "";

    /**
     * 方法名称
     */
    public String MethodName() default "";

    /**
     * 操作类别
     */
    public BusinessType OperationType() default BusinessType.OTHER;

    /**
     * 操作内容
     */
    public String OperationContent() default "";

    /**
     * 操作详情
     */
    public String OperationDetail() default "";
}

2. 创建自定义注解解析器

注解创建完了,需要一个解析器去对注解进行操作,下面我们需要创建一个注解解析器。
这里重点讲一下解析器里的几个方法:

2.1 方法说明:请求前

这个方法在请求处理之前调用,用于处理一些删除操作,由于删除操作在接口请求处理之前,数据还是在的,可以进行一下数据的保存,在接口调用之后数据就没了。

/**
     * 处理请求前执行
     *
     * @param joinPoint 切点
     */
    @Before(value = "logPointCut()")
    public void before(JoinPoint joinPoint) {
        log.info("before");
    }

2.2 方法说明:请求中

这个方法在请求过程中全程都覆盖,可以用来获得请求用时等操作

/**
     * 处理请求中执行
     *
     * @param point 切点
     */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) {
        log.info("around");
    }

2.3 方法说明:请求后

这个方法在接口请求完成调用,可以用来保存请求结果和请求信息等操作。

/**
     * 处理请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        log.info("doAfterReturning");
    }

2.4 方法说明:拦截异常

这个方法用于拦截请求过程中的异常

/**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        log.info("doAfterThrowing");
    }

2.5 注解解析器完整代码

完整代码,里面有个获取请求参数的方法,可以获取请求的参数,和注解中的MethodName一起使用,可以进行一些业务逻辑的处理。

/**
 * 注解解析
 *
 * @author nhx
 * @date 2022-10-14
 */

@Aspect
@Component
public class MyLogAspect {

    private static final Logger log = LoggerFactory.getLogger(MyLogAspect.class);

    @Pointcut("@annotation(com.psim.framework.myannotation.BusinessLog)")
    public void logPointCut() {

    }

    /**
     * 处理完请求前执行
     *
     * @param joinPoint 切点
     */
    @Before(value = "logPointCut()")
    public void before(JoinPoint joinPoint) {
        log.info("before");
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        log.info("doAfterReturning");
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        log.info("doAfterThrowing");
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            BusinessLog controllerLog = getAnnotationLog(joinPoint);
            if (Objects.isNull(controllerLog)) {
                return;
            }
            // 获取当前的用户
            LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
            // 数据库日志
            SysOperationLog sysOperationLog = new SysOperationLog();
            sysOperationLog.setStatus(String.valueOf(BusinessStatus.SUCCESS.ordinal()));
            if (Objects.nonNull(loginUser)) {
                sysOperationLog.setOperationName(loginUser.getUsername());
            }
            if (Objects.nonNull(e)) {
                sysOperationLog.setStatus(String.valueOf(BusinessStatus.FAIL.ordinal()));
            }
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, sysOperationLog);
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordSysOperationLog(sysOperationLog));
        } catch (Exception exp) {
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param businessLog     日志
     * @param sysOperationLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, BusinessLog businessLog, SysOperationLog sysOperationLog) throws Exception {
        // 设置action动作
        sysOperationLog.setOperationType(String.valueOf(businessLog.OperationType().ordinal()));
        // 设置操作内容
        sysOperationLog.setOperationContent(businessLog.OperationContent());
        // TODO 获取MethodName,根据不同的方法获取不同的参数进行处理
        String methodName = businessLog.MethodName();
        
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private BusinessLog getAnnotationLog(JoinPoint joinPoint) throws Exception {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();

        if (method != null) {
            return method.getAnnotation(BusinessLog.class);
        }
        return null;
    }

    /**
     * 获取参数Map集合
     *
     * @param joinPoint
     * @return
     */
    Map<String, Object> getNameAndValue(JoinPoint joinPoint) {
        Map<String, Object> param = new HashMap<>();
        Object[] paramValues = joinPoint.getArgs();
        String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        for (int i = 0; i < paramNames.length; i++) {
            param.put(paramNames[i], paramValues[i]);
        }
        return param;
    }

    /**
     * 处理完请求中执行
     *
     * @param point 切点
     */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) {
        log.info("around");
        //获取方法名称
        Signature methodName = point.getSignature();
        //日志输出
        log.info(methodName + "进来了");
        Long l1 = System.currentTimeMillis();
        Object obj = null;
        try {
            obj = point.proceed(point.getArgs());
        } catch (Throwable e) {
            e.printStackTrace();
        }
        log.info(methodName + "bye" + "\t耗時 " + (System.currentTimeMillis() - l1));
        //记录一个耗时时间,将证明日志通知
        return obj;
    }

3. 在controller层使用注解

调用就非常简单了,直接在controller的方法上面加上注解就可以了。

@BusinessLog(OperationContent = "这里是操作内容", OperationType = BusinessType.INSERT, MethodName = "这里是方法名称")

可以看一下debug日志,先log出了around方法,然后是before方法,最后是doAfterReturning方法,说明around是在before方法之前调用的。

结语

自定义注解保存日志功能就完成了,这里注意一下,如果需要将日志存入数据库,建议使用异步操作,这样不会拖慢接口调用的时间,具体可以参考我之前写的一篇文章:浅谈java开启异步线程的几种方法(@Async,AsyncManager,线程池),使用AsyncManager的方式就可以。