需求
需求:对每次实际的操作记录日志流水,每个操作会带有商家号和店铺号,
要求:由于现有业务代码已经够复杂,该日志需求和现有业务逻辑没有耦合,并且对一致性要求不高,不需要保证事务,是一个横向逻辑,需要尽可能少的侵入主链路逻辑。
思路:使用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"}
先有两个订单的创建,再有流水日志输出。