【AOP】AOP针对不同参数名的参数记录日志流水

【AOP】AOP针对不同参数名的参数记录日志流水

1. 场景描述

保存订单和修改订单的方法参数分别是 SaveOrderUpdateOrder 的对象 。SaveOrderUpdateOrder 这两个类分别有一个成员变量 idorderId 。这两个变量都代表订单id,但是它们两个的变量名不同。我们如何在AOP中针对不同的入参获得它们的订单id。


2. 准备工作

我们先放下这一问题,先搭建一下实际环境。

2.1 创建SaveOrder类

@Data
public class SaveOrder {
    private Long id;
}

2.2 创建UpdateOrder类

@Data
public class UpdateOrder {
    private Long orderId;
}

2.3 创建日志类

@Data
public class OperateLogDO {
    //订单id
    private Long orderId;
    //描述
    private String desc;
    //结果
    private String result;
}

2.4 创建订单服务类

创建订单服务实现类,并在其中定义保存订单方法和修改订单方法。

@Service
public class OrderService {

    public Boolean saveOrder(SaveOrder saveOrder) {
        System.out.println("保存订单,订单id为:" + saveOrder.getId());
        return true;
    }

    public Boolean updateOrder(UpdateOrder updateOrder) {
        System.out.println("更新订单,订单id为:" + updateOrder.getOrderId());
        return true;
    }
}

2.5 创建启动类

@SpringBootApplication
public class Application implements CommandLineRunner {
    public static void main(String[] args) {
        new SpringApplication(Application.class).run(args);
    }

    @Autowired
    private OrderService orderService;

    @Override
    public void run(String... args) throws Exception {
        SaveOrder saveOrder = new SaveOrder();
        saveOrder.setId(1L);
        orderService.saveOrder(saveOrder);

        UpdateOrder updateOrder = new UpdateOrder();
        updateOrder.setOrderId(2L);
        orderService.updateOrder(updateOrder);
    }
}

到此为止,最基本的环境搭建就已经完成了。

运行启动类,输入如下结果:


3.实现AOP获得不同的方法入参

3.1 创建一个切面类

AOP的切点我使用切点函数@annotation,所以我们先定义一个注解类:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordOperate {
    String desc() default "";
}

然后我们再创建一个切面类框架,around()方法暂时省略:

@Component
@Aspect
public class OperateAspect {

    @Pointcut("@annotation(aop.domain.RecordOperate)")
    public void pt() {
    }

    //避免对主链路的业务方法响应速度有影响,所以日志采用异步方法执行
    private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
    );

    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
       //......
    }
}

在AOP中写入日志如果采用同步的方式会降低主链路请求的响应速度,所以写日志的操作应该异步执行。

我们现在的问题就是如何在 around() 方法中针对不同的方法入参,有选择性的获取对应参数。

我们第一的想法就是通过 proceedingJoinPoint 获得其对应方法的参数列表,遍历其参数列表,判断这个参数是不是我们想要的参数。那么我们首先实现这一想法。


3.2 方法一:直接获得参数列表

3.2.1 定义通知方法
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Object result = proceedingJoinPoint.proceed();
    threadPoolExecutor.execute(() -> {
        long start = System.currentTimeMillis();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        //创建日志对象
        OperateLogDO operateLogDO = new OperateLogDO();
        //描述
        String desc = signature.getMethod().getAnnotation(RecordOperate.class).desc();
        //订单id
        Long id = null;

        //获得方法的参数数组
        Object[] args = proceedingJoinPoint.getArgs();
        //遍历参数
        for (Object arg : args) {
            if (arg instanceof SaveOrder) {
                id = ((SaveOrder) arg).getId();
            } else if (arg instanceof UpdateOrder) {
                id = ((UpdateOrder) arg).getOrderId();
            }
        }
        //set描述
        operateLogDO.setDesc(desc);
        //set订单id
        operateLogDO.setOrderId(id);
        //set结果
        operateLogDO.setResult(result.toString());

        long end = System.currentTimeMillis();
        //写入日志
        System.out.println("打印日志..."+operateLogDO.toString()+"方法耗时:"+(end-start)+"ms");
    });
    return result;
}

3.2.2 修改服务方法

写好了切面逻辑后,我们在对应的方法上面加入注解:

@RecordOperate(desc = "保存订单")
public Boolean saveOrder(SaveOrder saveOrder) {
    System.out.println("保存订单,订单id为:" + saveOrder.getId());
    return true;
}

@RecordOperate(desc = "更新订单")
public Boolean updateOrder(UpdateOrder updateOrder) {
    System.out.println("更新订单,订单id为:" + updateOrder.getOrderId());
    return true;
}

运行启动类,输出结果:

image-20230325225736845

3.2.3 方法弊端

这样写有一个问题,就是代码耦合比较大,没有扩展性。每次新增一个方法就得修改一次切面逻辑。比如我们又来了一个 deleteOrder(DeleteOrder deleteOrder) 方法,这个参数中有一个成员变量 deleteId ,他也是订单id的意思。那么我们就得修改切面逻辑:

//获得方法的参数数组
    Object[] args = proceedingJoinPoint.getArgs();
    //遍历参数
    for (Object arg : args) {
        if (arg instanceof SaveOrder) {
            id = ((SaveOrder) arg).getId();
        } else if (arg instanceof UpdateOrder) {
            id = ((UpdateOrder) arg).getOrderId();
        } else if (arg instanceof DeleteOrder) {
            id = ((UpdateOrder) arg).getDeleteId();
        } 
    }

这样每新增一个方法就得去修改原有代码,非常的不便。所以我们采用另一种更优雅的解决办法。


3.3 方法二:使用对象转换器

3.3.1 创建一个转换器接口
public interface Convert<PARAM> {

    OperateLogDO convert(PARAM param);
}

每一个方法入参都调用这个方法直接转换成日志对象,就不需要对每一个参数循环判断了。


3.3.2 创建转换器实现类

我们有多少个不同的参数对象,就创建多少个转换器实现类。

首先创建 SaveOrder 的对象转换器

public class SaveOrderConvert implements Convert<SaveOrder> {
    @Override
    public OperateLogDO convert(SaveOrder saveOrder) {
        OperateLogDO operateLogDO = new OperateLogDO();
        operateLogDO.setOrderId(saveOrder.getId());
        return operateLogDO;
    }
}

然后创建 UpdateOrder 的对象转换器

public class UpdateOrderConvert implements Convert<UpdateOrder> {
    @Override
    public OperateLogDO convert(UpdateOrder updateOrder) {
        OperateLogDO operateLogDO = new OperateLogDO();
        operateLogDO.setOrderId(updateOrder.getOrderId());
        return operateLogDO;
    }
}

3.3.3 修改切点注解类

在原来的切点注解类中添加一个转换器接口类变量:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordOperate {
    String desc() default "";
    Class<? extends Convert> convert();
}

3.3.4 修改服务方法
@Service
public class OrderService {

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

    @RecordOperate(desc = "更新订单", convert = UpdateOrderConvert.class)
    public Boolean updateOrder(UpdateOrder updateOrder) {
        System.out.println("更新订单,订单id为:" + updateOrder.getOrderId());
        return true;
    }
}

在注解中添加了这一变量后方便我们对不同的入参都统一转换成了日志对象。


3.3.5 定义通知方法
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Object result = proceedingJoinPoint.proceed();
    threadPoolExecutor.execute(() -> {
        try {
            long start = System.currentTimeMillis();
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            //描述
            String desc = signature.getMethod().getAnnotation(RecordOperate.class).desc();
            //获得方法上填入注解的Class对象
            Class<? extends Convert> convert = signature.getMethod().getAnnotation(RecordOperate.class).convert();
            //使用反射调用convert()方法将不同的入参统一转换成 operateLogDO 对象
            Convert logConvert = convert.newInstance();
            OperateLogDO operateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);

            operateLogDO.setDesc(desc);
            operateLogDO.setResult(result.toString());

            long end = System.currentTimeMillis();
            System.out.println("打印日志..." + operateLogDO.toString() + "方法耗时:" + (end - start) + "ms");
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    });
    return result;
}

运行启动类,输出如下结果:

image-20230325231135449

这样就解决了方法一的弊端,将来再来了一个 deleteOrder(DeleteOrder deleteOrder) 方法,我们只需要再去创建 DeleteOrder 的转换器实现类即可,完全不需要再去修改切面逻辑。这样也满足了设计模式的开闭原则——“对扩展开放,对修改关闭”。


3.4 其他方法

除了上面两种方法外,相信还有很多其他的解决方法。像是创建一个统一的 Order 类,在其中统一定义订单id的成员变量名为 id 。然后所有和Order类(SaveOrder,UpdateOrder等等)相关的类都继承该它,这样我们就统一了入参。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值