如何优雅的记录用户的操作日志

453 阅读3分钟

最近业务有需求要对所有的用户操作进行日志记录,方便管理员查询不同权限级别的用户对系统的操作记录,现有的日志只是记录了异常信息、业务出错、重要功能的执行进行了记录,并不能满足需求要求,最直接的解决方法是在每个接口上去添加log.info之类的代码,但是这种方式对业务代码的植入性太强,记录日志的代码和业务代码耦合性太强,对于代码的可读性和可维护性来说是一个灾难。如果能在每个接口上添加一个注解来记录日志则要优雅的多。 通过接口来记录日志需要用到Spring的动态代理技术,定义一个切面,应用到所有加了这个注解的接口,在接口执行的切面上获取接口方法的参数和执行结果,将要记录的信息记录到数据库(或者是日志文件或者是其他方式)。

1、定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogBook {

    /**
     * 模块
     *
     * @return
     */
    String module();

    /**
     * 跟踪标识(业务标识)
     *
     * @return
     */
    String traceId();

    /**
     * 跟踪标签(业务标识标签)
     *
     * @return
     */
    String traceTag() default "";

    /**
     * 操作内容
     *
     * @return
     */
    String[] content();

    /**
     * 操作类型
     * 为null时,框架根据request-method进行匹配
     *
     * @return
     */
    String operateType() default "";

    /**
     * 操作员 
     *
     * @return
     */
    String operator() default "_header";

    /**
     * 启停
     *
     * @return
     */
    boolean enable() default true;
}

2、定义切面类:

定义切面类使用Aop的注解@Aspect来定义,对制定的注解进行环切,定义如下:

@Aspect
public class GenericRestLogBookAspect {

    //切面 包含注解
    @Pointcut("@annotation(com.xxx.annotation.LogBook)")
    public void intercept() {

    }


    /**
     * 对方法进行环切
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "intercept()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {


        /**
         * 当前注解实例
         */
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        LogBook annotation = AnnotationUtils.findAnnotation(methodSignature.getMethod(), LogBook.class);
        if (annotation == null || !annotation.enable()) {
            return joinPoint.proceed();
        }

        /**
         * 基础参数
         */
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = this.getRequest(args);

        LogBookRecord init = new LogBookRecord();
        init.setCreateDate(new Date());         //标准时间

        EvaluationContext evaluationContext = expressionParser.initContext(joinPoint);


        /**
         * 执行
         */
        Object out = null;
        Exception err = null;
        try {
            //前置
            this.beforeResolving(request, evaluationContext, annotation, args, init);
            //业务执行
            out = joinPoint.proceed();
        } catch (Exception e) {
            err = e;
            throw e;
        } finally {
            //后置
            this.afterResolving(request, evaluationContext, annotation, args, init, out, err);
            //释放
            expressionParser.removeContext();
        }
        return out;
    }

    /**
     * 前置处理
     *
     * @param handler
     * @param request
     * @param evaluationContext
     * @param annotation
     * @param args
     * @param init
     */
    private void beforeResolving(HttpServletRequest request,
                                 EvaluationContext evaluationContext,
                                 LogBook annotation,
                                 Object[] args,
                                 LogBookRecord init) {       
        //这里的resolving方法从request中解析出相关参数信息到LogBookRecord对象中
        resolving(request, evaluationContext, annotation, args, init);
    }


    /**
     * 后置处理
     *
     * @param handler
     * @param request
     * @param evaluationContext
     * @param annotation
     * @param args
     * @param record
     * @param result
     * @param err
     */
    private void afterResolving(HttpServletRequest request,
                                EvaluationContext evaluationContext,
                                LogBook annotation,
                                Object[] args,
                                LogBookRecord record,
                                Object result, Exception err) {        
        /**
         * 异步处理
         */
        try {
            //重新构建 EvaluationContext
            EvaluationContext evaluationAfter = expressionParser.setContextResult(evaluationContext, result, err);
            threadPool.getTaskExecutor().execute(() -> {
                //解析 - 根据业务接口返回结果信息解析到LogBookRecord中
                List<LogBookRecord> records = resolvingAfter(request, evaluationAfter, annotation, args, result, err, record);
                //存储(可以存储到数据库,也可以存储到日志文件或者其他地方)
                persistHandler.saveBatch(records);
            });
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }


    /**
     * 默认从参数中获取
     *
     * @param args
     * @return
     */
    private HttpServletRequest getRequest(Object[] args) {
        Optional<HttpServletRequest> ops = Stream.of(args).filter(e -> e.getClass().isAssignableFrom(HttpServletRequest.class)).map(e -> (HttpServletRequest) e).findFirst();
        return ops.orElseGet(() -> ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
    }
}

3、定义开关属性:

通过这个开关属性,控制开启还是关闭日志记录。

@Configuration
@ConfigurationProperties(prefix = "xxx.logbook")
public class LogBookProperties implements Serializable {

    private boolean enable;

    public boolean isEnable() {
        return enable;
    }

    public void setEnable(boolean enable) {
        this.enable = enable;
    }
}

4、定义配置类:

在配置类中定义切面类的Bean,并通过开关属性进行开关控制;

@Configuration
@ConditionalOnProperty(name = "xxx.logbook.enable", havingValue = "true")
@ComponentScan("com.xxx.logbook")
public class LogBookAutoConfiguration {    

    /**
     * 切面
     *
     * @param factory
     * @param expressionParser
     * @return
     */
    @Bean
    public GenericRestLogBookAspect logBookAspect(LogBookExpressionParser expressionParser,
                                                  LogBookPersistHandler persistHandler, LogBookThreadPool threadPool) {
        return new GenericRestLogBookAspect(expressionParser, persistHandler, threadPool);
    }
}

5、应用示例:

@LogBook(module = "custom", 
         traceId = "{{#dto.id}}",
         content = {"用户 :提交了新数据:{{#dto.filed1}} {{#dto.filed2}} {{#dto.filed3}} "}, 
         operateType = OperateTypes.ADD)