开闭原则正确姿势, 使用AOP优雅的记录日志, 非常的哇塞

👳我亲爱的各位大佬们好😘😘😘
♨️本篇文章记录的为 JDK8 新特性 Stream API 进阶 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛
👨‍🔧 个人主页 : 阿千弟
🔥 相关内容👉👉👉 : 都2023年了,如果不会Lambda表达式、函数式编程?你确定能看懂公司代码?

最近工作中遇到了一个新的需求, 让我把前端发送请求的所有操作统统用日志记录下来, 但是呢前端是h5的页面, 任何一个对页面的操作都记录下来也不太现实, 于是AOP这时候就是一个很好的选择, 但是在操作很多的状态下, 对每一个操作都写一个切面来记录也不太现实, 应优先考虑如何对切面进行抽取与拓展.

因为代码逻辑比较复杂, 所以每次排查问题的时候都非常耗时, 所以这时候用AOP的方式需合的就是在每一个操作的方法之前去加一个注解,通过切面处理这个方法义步的去协助流水表
在这里插入图片描述

当前场景

为了把保存订单和更新订单的方便区分, 我们分别用不同的实体类entity来表示

SaveOrder

@Data
public class SaveOrder {
    private Long id;
}

UpdateOrder

@Data
public class UpdateOrder {
    private Long OrderId;
}

下面是Service层接口

public interface OrderService {

    Boolean saveOrder(SaveOrder saveOrder);

    Boolean updateOrder(UpdateOrder updateOrder);
}

下面是impl实现类

@Service
public class OrderServiceImpl implements OrderService {

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

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

上面的代码很简单,很方便的就能看懂, 下面我们就来编写自定义注解和AOP切面

自定义注解

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

	//desc 是用来放日志类型的描述
    String desc() default " ";
	//convert 用来放日志类型的转变类
    Class<? extends Convert> convert();

}

我们可以看到在自定义的注解类中有一个desc()属性, 它用于记录的字段信息;

还有一个convert()方法, 这个方法很关键 :

当我们每次调用 OrderService 的时候, 要根据不同的方法进行不同日志记录, 那么我们怎么知道, 我们调用的是 SaveOrder 还是 UpdateOrder , 所以不管是SaveOrder还是UpdateOrder, 我们都需要记录日志, 但是呢, 我们不可能对每个操作都单独的写一个切面记录日志, 所以呢需要写一个 convert() 对不同的方法属性转化成统一的log实体输出

下面编写转换器convert()

代码很简单, 泛型用PARAM表示, 返回结果是约定好的OperateLogDO

public interface Convert<PARAM> {
    OperateLogDO convert(PARAM param);
}

下面的代码是转换器的用法

SaveOrderConvert通过实现接口来重写convert()方法

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

UpdateOrderConvert通过实现接口来重写convert()方法

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

OperateLogDO实体

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

AOP切面

@Component
@Aspect
public class OperateAspect {
    /**
     * 定义切入点
     * 横切逻辑
     * 织入
     */

    @Pointcut("@annotation(com.example.demo_aop.annotation.RecordOperate)")
    public void pointCut(){};
	
	//定义线程池的目的是记录日志不需要特别强的同步性, 所以创建线程异步记录
    private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,1,1,TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
    );

    @Around("pointCut()")
    public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result = proceedingJoinPoint.proceed();
        threadPoolExecutor.execute(() -> {
        //这段Java代码获取了一个切点方法的签名信息,
        //并且通过反射获取了该方法上的RecordOperate注解。
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            RecordOperate annotation = methodSignature.getMethod().getAnnotation(RecordOperate.class);

			//通过反射判断是哪个方法的转换器,并获取
            Class<? extends Convert> convert = annotation.convert();
            OperateLogDO operateLogDO = null;
            try {
            //创建Convert类的一个新实例
                Convert logConvert = convert.newInstance();
                operateLogDO = logConvert.convert(proceedingJoinPoint.getArgs()[0]);
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            operateLogDO.setDesc(annotation.desc());
            operateLogDO.setResult(result.toString());
            System.out.println("insert operateLog " + JSON.toJSONString(operateLogDO));
        });
        return result;
    }
}

这段切面代码也比较的简单, 但是呢需要具备一定的java基础功底

自定义注解应用

@Service
public class OrderServiceImpl implements OrderService {

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

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

代码测试
@SpringBootApplication
public class AopApplication implements CommandLineRunner {
    public static void main(String[] args) {
        SpringApplication.run(AopApplication.class, args);
    }
   
    @Resource
    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);
    }
}
(JVM running for 5.141)
save order, orderId : 1
update order, orderId : 2
insert operateLog {"desc":"保存订单","id":1,"result":"true"}
insert operateLog {"desc":"更新订单","id":2,"result":"true"}

大家也可以看到,这样做的好处是通过抽取公共组件,使用aop切面方式实现流水日志输出,并且代码无侵入,满足开闭原则!

在这里插入图片描述

如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对spring, 分布式, 云原生感兴趣的朋友,请多多关注💖💖💖
👨‍🔧 个人主页 : 阿千弟

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿千弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值