切面编程案例(打印业务日志流水)

需求

需求:对每次实际的操作记录日志流水,每个操作会带有商家号和店铺号,

要求:由于现有业务代码已经够复杂,该日志需求和现有业务逻辑没有耦合,并且对一致性要求不高,不需要保证事务,是一个横向逻辑,需要尽可能少的侵入主链路逻辑。

思路:使用AOP方式,在每个操作方法之前,去加一个注解,通过切面处理方法一步的写入流水。

难点:入参并不标准,如何在统一的一个切面里面去根据不同的入参去取到想要的参数。就是说入参可能是不同的类型,但是都会包含商家ID订单ID这些参数,如何在一个切面里面指定不同的取值逻辑。

实现

首先新建一个service,OrderService,里面定义两个方法:

@Service
public class OrderService {

    public Boolean saveOrder(SaveOrder saveOrder){
        System.out.println("save order , orderId: " + saveOrder.getId());
        return true;
    }

    public Boolean updateOrder(UpdateOrder updateOrder){
        System.out.println("update order , orderId: "+ updateOrder.getOrderId());
        return true;
    }
}

以及相应的两个类:SaveOrder和UpdateOrder

@Data
public class SaveOrder {
    private Long id;
}

@Data
public class UpdateOrder {
    private Long OrderId;
}

还有用于输出流水的日志类对象:

@Data
public class OperateLogDO {
    private Long orderId;
    private String desc;
    private String result;
}

我们希望对两个方法记录流水,使用注解配合AOP:

定义注解:

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

    String desc() default "";
}

里面仅有一个描述信息,在两个方法上分别加上:

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

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

然后处理切面,需要做的是:定义切入点,编写横切逻辑,然后交给Spring去织入:

创建OperateAspect类,用@Aspect和@Component去修饰

//	确定切入点
@Pointcut("@annotation(com.emnets.annotation.RecordOperate)")
public void pointcut(){
}

编写横切逻辑:

为了不影响性能,使用一个异步的线程池,使用ThreadPoolExecutor创建了一个大小为1的线程池对象,其中包含一个核心线程和一个最大线程,线程池的空闲线程会等待1秒钟后自动被销毁,线程池使用了一个容量为100的阻塞队列来存放等待执行的任务。

//	创建线程
private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
	1,1,1,TimeUnit.SECONDS,new LinkedBlockingDeque<>(100)
);

切面逻辑,这里使用环绕通知,相比前置通知使用的切入点对象JointPoint,环绕通知使用ProceedingJoinPoint作为参数。首先通过切入点获取函数签名(相比反射里面的Method,获取签名更加准确,可以区别处重载方法)。再通过签名获取注解,然后用于填充日志类对象:

MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
RecordOperate annotation = methodSignature.getMethod().getAnnotation(RecordOperate.class);

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

还有一个信息就是OrderId的获取,如果使用if去判断类型不够优雅,在对象里面加入函数获取就对主链逻辑侵入太多,这里写一个convert接口将两个对象SaveOrder和UpdateOrder统一化的转换成Log对象:

//	首先定义一个接口
public interface Convert<PARAM> {

  	//	定义转化方法,返回结果就是日志对象
    OperateLogDO convert(PARAM param);

}

然后分别实现两个对象转化Log的实体类:

public class SaveOrderConvert implements Convert<SaveOrder> {

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

这里我们通过注解来获取convert类型,然后完成转换,所以在注解里面加入convert属性,用以存储convert的类属性:

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

    String desc() default "";
		//	添加了convert属性,这个类必须是Convert的子类
    Class<? extends Convert> convert();
}
//	修改两个注解,添加了转换类的属性
@RecordOperate(desc = "保存订单",convert = SaveOrderConvert.class)
public Boolean saveOrder(SaveOrder saveOrder){}

@RecordOperate(desc = "更新订单",convert = UpdateOrderConvert.class)
public Boolean updateOrder(UpdateOrder updateOrder){}

在横切逻辑中:获取,新建,完成转换

//  这个convert获取到的是参数的类的类型,是SaveOrderConvert.class或者UpdateOrderConvert.class
Class<? extends Convert> convert = annotation.convert();
//  根据类去新建实体类
Convert logConvert = convert.newInstance();
//  利用切入点里面的第一个参数,去作为Convert接口里面的convert方法的参数,获取到的就是转换后的带有相应ID的OperateLogDO
OperateLogDO operateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);
//	之后添加剩余的属性,完成流水日志打印
operateLogDO.setDesc(annotation.desc());
operateLogDO.setResult(result.toString());

System.out.println("insert operateLog "+ JSON.toJSONString(operateLogDO));

OperateAspect类完整的代码:

@Aspect
@Component
public class OperateAspect {

    @Pointcut("@annotation(com.emnets.annotation.RecordOperate)")
    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();
                RecordOperate annotation = methodSignature.getMethod().getAnnotation(RecordOperate.class);

                //  这个convert获取到的是参数的类的类型,是SaveOrderConvert.class或者UpdateOrderConvert.class
                Class<? extends Convert> convert = annotation.convert();
                //  根据类去新建实体类
                Convert logConvert = convert.newInstance();
                //  利用切入点里面的第一个参数,去作为Convert接口里面的convert方法的参数,获取到的就是转换后的带有相应ID的OperateLogDO
                OperateLogDO operateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);

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

                System.out.println("insert operateLog "+ JSON.toJSONString(operateLogDO));

            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
        return result;
    }


}

之后新建Application去运行:

@SpringBootApplication
public class Application implements CommandLineRunner {


    public static void main(String[] args) {
        System.out.println("Hello world!");
    }

    @Autowired
    private OrderService service;


    @Override
    public void run(String... args) throws Exception {

        SaveOrder saveOrder = new SaveOrder();
        saveOrder.setId(1L);
        service.saveOrder(saveOrder);

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

运行

结果应该是:

save order , orderId: 1
update order , orderId: 2
insert operateLog {"desc":"保存订单","orderId":1,"result":"true"}
insert operateLog {"desc":"更新订单","orderId":2,"result":"true"}

先有两个订单的创建,再有流水日志输出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值