使用SpringAOP实现低侵入的日志采集功能

2022-11-22
预计阅读时间:3分钟

最近看过关注的B站up主极海Channel发表的一条关于使用Spring AOP实现日志记录的视频,对业务代码低侵入的前提下完成了日志的记录,同时也通过合理的设计实现了不同业务操作下不同名称的入参向日志字段的灵活转换,个人觉得很有必要记录下来方便以后在遇到同样的场景时作为参考,同时也借此机会回忆下Spring AOP编程相关流程与知识点!

下面通过代码和一些描述来「复盘」整个样例的实现过程(基于SpringBoot的环境):

依赖引入

首先在pom.xml中加入spring-boot-starter-aop依赖完成AOP相关依赖的自动配置

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编码实现

  1. 创建OrderService.java来模拟订单操作相关业务类
@Service
public class OrderService {

    @RecordOperation(desc = "保存订单", convert = SaveOrderConvert.class)
    public Boolean saveOrder(SaveOrder saveOrder){
        System.out.println("save order, orderId :" + saveOrder.getId());
        return true;
    }

    @RecordOperation(desc = "更新订单", convert = UpdateOrderConvert.class)
    public Boolean updateOrder(UpdateOrder updateOrder){
        System.out.println("update order, orederId:" + updateOrder.getOrderId());
        return true;
    }
}
  1. 创建SaveOrder.java来定义保存订单业务操作实体类以及Id字段
@Data
public class SaveOrder {

    //订单id
    private Long id;
}
  1. 创建UpdateOrder.java来定义更新订单业务操作实体类以及Id字段
@Data
public class UpdateOrder {

    //订单id
    private Long orderId;
}
  1. 创建OpreateLogDO.java来定义日志(记录)实体类以及字段
@Data
public class OpreateLogDO {

    //订单id
    private long orderId;

    //业务描述
    private String desc;

    //操作结果
    private  String result;
}
  1. 创建RecordOperation.java定义注解来标注切入点
//该注解在默认在编译期消失,需要指定在运行时也能保留它的功能
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RecordOperation {

    //业务描述
    String desc() default "";

    //继承自Convert接口的业务实体,本案例中包含SaveOrderConvert、UpdateOrderConvert两个实现类
    Class<? extends Convert> convert();
}
  1. 创建OperateAspect.java来定义切面类
@Component
//使用@Aspect将该类声明为一个切面类
@Aspect
public class OperateAspect {
    /**
     * 定义切点
     * 定义横切逻辑
     * 织入
     */
    @Pointcut("@annotation(com.devon.quiz.aop.RecordOperation)")
    public void pointcut(){}

    //使用异步执行方式记录日志,创建线程池
    private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,1,1, TimeUnit.SECONDS,new LinkedBlockingDeque<>(100)
    );

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = proceedingJoinPoint.proceed();
        threadPoolExecutor.execute(() -> {
            try {
                //此处使用反射来拿到切入点位置的方法签名以及注解
                MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
                RecordOperation annotation = methodSignature.getMethod().getAnnotation(RecordOperation.class);

                Class<? extends Convert> convert = annotation.convert();
                Convert logConvert = convert.newInstance();
                OpreateLogDO opreateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);

                //OpreateLogDO opreateLogDO = new OpreateLogDO();
                opreateLogDO.setDesc(annotation.desc());
                opreateLogDO.setResult(result.toString());

                //opreateLogDO.setOrderId();
                System.out.println("insert operationlog " + JSON.toJSONString(opreateLogDO));
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });

        return result;
    }

}
  1. 创建Convert.java定义入参适配接口
//添加泛型来泛化入参
public interface Convert<PARAM> {

    OpreateLogDO convert(PARAM param);
}
  1. 创建SaveOrderConvert.java定义SaveOrder实体类的入参适配实现类
public class SaveOrderConvert implements Convert<SaveOrder>{

    @Override
    public OpreateLogDO convert(SaveOrder saveOrder) {
        OpreateLogDO opreateLogDO = new OpreateLogDO();
        opreateLogDO.setOrderId(saveOrder.getId());
        return opreateLogDO;
    }
}
  1. 创建UpdateOrderConvert.java定义UpdateOrder实体类的入参适配实现类
public class UpdateOrderConvert implements Convert<UpdateOrder>{
    @Override
    public OpreateLogDO convert(UpdateOrder updateOrder) {
        OpreateLogDO opreateLogDO = new OpreateLogDO();
        opreateLogDO.setOrderId(updateOrder.getOrderId());
        return opreateLogDO;
    }
}

测试运行

在单元测试中测试运行日志采集功能:

@Test
	public void testOrderAop(){
		SaveOrder saveOrder = new SaveOrder();
		saveOrder.setId(1L);
		orderService.saveOrder(saveOrder);
		UpdateOrder updateOrder = new UpdateOrder();
		updateOrder.setOrderId(2L);
		orderService.updateOrder(updateOrder);
	}

运行结果如下:

Snipaste_2022-11-22_13-40-52

  • 另:期间出现的bug以及解决方法

上述过程中切点处的@RecordOperation注解默认没有做任何修饰,所以在SpringBoot项目编译期就无法起到作用,导致无法完成横切的操作,会出现下图所示的异常:

Snipaste_2022-11-22_13-42-55

解决上述问题需要在自定义注解上方添加修饰来使该注解的效果持续到运行期,修饰部分代码如下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented

致此,使用Spring AOP实现低侵入性的日志采集实现完成,希望能够帮助到你!