ch3.ng

Spring AOP无侵入实现日志


平常我们聊起Spring Aop的使用场景最多的就是权限拦截统一日志等。那么什么样的日志是比较优雅的实现方式?我们在开始实现日志前首先思考,什么样的日志是好的日志?需要满足哪些属性?达到什么样的目的?

简介

日志好比叙事,谁在什么时间发生了什么事,事情的结果是什么?满足这4点就能算得上合格的日志。

实现

  1. 定义一个日志描述类,描述日志产生的用户(系统)、主体、时间、地点、行为、结果、变化过程、异常等。
@Setter
@Getter
@Builder
@ToString
public class Log implements Serializable {

    /**
     * 日志唯一id,用于标记日志等唯一性
     */
    private Serializable id;

    /**
     * 日志发生在那个系统
     */
    private System system;

    /**
     * 日志发生在那个模块
     */
    private String module;

    /**
     * 日志发生时间
     */
    private LocalDateTime time;

    /**
     * 谁产生等日志
     */
    private String user;

    /**
     * 本次日志等行为
     */
    private String action;

    /**
     * 本次日志行为类型
     */
    private ActionType actionType;

    /**
     * 行为结果
     */
    private Status status;

    /**
     * 主体ID
     * 订单ID,用户ID
     */
    private Serializable subjectId;

    /**
     * 行为主体
     * 订单,用户,收获地址一般可能是领域对象
     */
    private String subject;

    /**
     * 本次行为的参数
     */
    private String parmas;

    /**
     * 行为发生前的值
     */
    private String preValue;

    /**
     * 行为发生后的值
     */
    private String afterValue;

    /**
     * 本次行为发生异常的信息
     */
    private String exception;
  1. 通过自定义注解,在我们需要的函数上打上注解完成日志记录的同时,对已有的代码达到无侵入;
@Component
@Aspect
@Slf4j
public class LogAspect {


    @AfterReturning(pointcut = "@annotation(l0g)",returning = "result")
    public void afterReturning(JoinPoint joinPoint, L0g l0g, Object result){
        me.Cheng.t.domain.Log loog = me.Cheng.t.domain.Log.builder().actionType(l0g.actionType())
                .action(l0g.action())
                .id(UUID.randomUUID())
                .system(System.ORDER)
                .module("订单管理")
                .user("zhangsan")
                .time(LocalDateTime.now())
                .status(Status.SUCCESS)
                .build();

        if(LogContextHandler.get().isPresent()){
            LogContext logContext = LogContextHandler.get().get();
            loog.setPreValue(logContext.getPreValue());
            loog.setAfterValue(logContext.getAfterValue());
        }

        //这里可以进行本地记录
        log.info("l000000g:{}",loog);
        //这里可以上报日志系统
    }


    @AfterThrowing(pointcut = "@annotation(l0g)",throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint, L0g l0g, Throwable exception){

        Object[] args = joinPoint.getArgs();
        String arg = (String)args[0];

        me.Cheng.t.domain.Log loog = me.Cheng.t.domain.Log.builder().actionType(l0g.actionType())
                .action(l0g.action())
                .id(UUID.randomUUID())
                .system(System.ORDER)
                .module("订单管理")
                .user("zhangsan")
                .time(LocalDateTime.now())
                .status(Status.EXCEPTION)
                .parmas(arg)
                .exception(exception.getMessage())
                .build();

        if(LogContextHandler.get().isPresent()){
            LogContext logContext = LogContextHandler.get().get();
            loog.setPreValue(logContext.getPreValue());
            loog.setAfterValue(logContext.getAfterValue());
        }

        //这里可以进行本地记录
        log.info("l000000g:{}",loog);
        //这里可以上报日志系统
    }

  1. 在服务层完成日志的收集
@Service
public class OrderServiceImpl implements OrderService {


    @L0g(actionType = ActionType.MODIFY,action = "新增订单")
    @Override
    public void updateOrder(Order order) {
        LogContextHandler.setLogContext(new LogContext().preValue("\"{price:5.00}\"").afterValue("\"{price:6.00}\""));

    }

    @L0g(actionType = ActionType.DELETE,action = "删除订单")
    @Override
    public void deleteOrder(String orderId) {
        throw new RuntimeException("订单不存在");
    }
}

  1. 通过本地线程达到日志收集过程中参数的传递
public class LogContextHandler {

    private static final ThreadLocal<LogContext> threadLocal = new ThreadLocal<>();

    public static void setLogContext(LogContext logContext){
        threadLocal.set(logContext);
    }

    public static Optional<LogContext> get(){
        return Optional.ofNullable(threadLocal.get());
    }

}

所谓的无侵入只发生在记录日志行为,要想记录行为本身产生的数据,始终是需要少量的日志代码侵入。这样的代价是无法避免的也是值得的。

当然该Demo也放在了这里