基于SpringBoot AOP和注解,优雅的实现系统操作日志异步记录

一、背景

    最近接手一个新的项目,做了两个多月的修修补补,简直无语到哭。总结起来有一下几点:

  • 日志打印:每个类中都定义了操作日志入库的方法,每个方法中都同步将操作日志记录入库。有时候一个简单的一行调用service接口的controller代码,加上操作日志入库的代码,竟然多了十几行代码(我就想知道,你们以前是按代码行数算钱的吗)。更重要的是没有一行注释,同步执行入库操作,他不增加耗时吗?
  • 系统日志:系统日志打印完全没有什么级别之分,全凭个人喜好,想用debug就用debug,想用info,就是info。很任性。
  • 分页查询:low到无法想象。全部都是将所有数据全部搂到没存中,然后荣list中分割,等到查询下一页的时候,我再全部搂出来,如法炮制。(没办法,老子内存大,任性!)
  • 。。。

    多余不在吐槽,槽点太多。今天是实在忍无可忍。开始优化第一步,首先干掉大部分操作日志代码。这些属于边缘代码,删掉不会影响原有逻辑调用,况且,这部分代码也是最多的。

    我采用的方式是Spring 的AOP环绕通知+自定义注解+SpringBoot的异步方法实现。下面具体介绍实现过程。

二、主要源码

1、定义调用方法类型枚举类

/**
 * desc: 方法类型
 * author: xuebin
 * date: 2020/03/20 17:51
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
public enum MethodType {
    ADD("add", "增加"),
    DELETE("delete", "删除"),
    UPDATE("update", "修改"),
    EXPORT("export", "导出"),
    SELECT("query", "查询");

    private String type;
    private String name;

    MethodType(String type, String name) {
        this.type = type;
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public String getName() {
        return name;
    }

    public static String getName(String type){
        for (MethodType modelType : MethodType.values()) {
            if (modelType.getType().equals(type))
                return modelType.getName();
        }
        return null;
    }

    public static String getType(String name){
        for (MethodType modelType : MethodType.values()) {
            if (modelType.getName().equals(name))
                return modelType.getType();
        }
        return "-1";
    }
}

2、自定义注解(凡是有该注解的方法都要将操作日志入库)

/**
 * desc: 操作日志注解类
 * author: xuebin
 * date: 2020/03/20 15:45
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogAnnotation {
    // 日志是否存入数据库
    boolean intoDB() default false;
    
    // 操作类型, 默认是查询
    MethodType methodType() default MethodType.SELECT;

    // 接口描述
    String description() default "";

}

3、定义切面类、切点和标有相关注解的拦截方法。

    这里采用了环绕通知,可以处理目标方法前、目标方法后的相关逻辑。但是主要,别漏了调用目标方法。

    需求简单,可以选择使用别的通知。这个下边汇总一下。

/**
 * desc: 日志记录切面
 * author: xuebin
 * date: 2020/03/20 15:48
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
@Aspect
@Component
public class LogAspect {

    private Logger logger = LoggerFactory.getLogger(LogAspect.class);
    @Resource
    private TestLogToDB testLogToDB;
    @Resource
    private HttpServletRequest request;

    /**
     * 定义切点
     */
    @Pointcut("execution(* com.sitech.open.operate..*.*(..))")
    public void log() {}

    @Around(value = "log() && @annotation(logAnnotation)")
    public Object around(ProceedingJoinPoint joinPoint, LogAnnotation logAnnotation) throws Throwable {
        // 目标方法执行前。这里可以加上自己的逻辑
        logger.info("目标方法执行前,可以添加自己的逻辑"+JSON.toJSONString(request.getParameterMap()));
        // 执行目标接口
        Object object = joinPoint.proceed();
        // 目标方法执行后
        logger.info("目标方法执行后,可以添加自己的逻辑"+JSON.toJSONString(object));
        // 调用异步方法
        testLogToDB.intoDB(request.getParameterMap(), object);
        return object;
    }
}

4、异步方法实现

/**
 * desc: 定义接口
 * author: xuebin
 * date: 2020/03/26 22:12
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
public interface TestLogToDB {

    void intoDB(Map<String, String[]> parameterMap, Object object);
}

/**
 * desc:
 * author: xuebin
 * date: 2020/03/26 22:13
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
@Service
@Transactional
public class TestLogToDBImpl implements TestLogToDB {

    private Logger logger = LoggerFactory.getLogger(LogAspect.class);
    @Async
    @Override
    public void intoDB(Map<String, String[]> parameterMap, Object object) {
        // 这里只做示例,不展示具体逻辑。使用者可自行实现各种效果
        logger.info("step inot async intoDB, 我是异步方法,我被调用了");
    }
}

5、注意

    使用异步方法时,需要在Spring的Application启动类上加上@EnableAsync,标记开启异步方法。

6、使用,测试类

    在需要使用日志入库的方法上加上我们定义的注解,如下:

/**
 * desc: 测试
 * author: xuebin
 * date: 2020/03/26 22:29
 * blog: www.cxyk.net
 * 公众号: cxyknet
 */
@RestController
@RequestMapping("/test")
public class TestController {
    Logger logger = LoggerFactory.getLogger(TestController.class);
    
    @LogAnnotation(description = "测试")
    @GetMapping("/test")
    public String test(@RequestParam String id, @RequestParam String userName){
        logger.info("step into test. id:{}; userName: {}", id, userName);
        return "success";
    }
}

7、测试,访问我们的controller中test接口

7.1、请求示例

7.2、相应结果

7.3、后台日志打印

INFO :2020-03-26 22:39:40,817 [ com.LogAspect: ] - 目标方法执行前,可以添加自己的逻辑{"id":["我是id"],"userName":["我是userName"]}
INFO :2020-03-26 22:39:40,820 [ com.TestController: ] - step into test. id:我是id; userName: 我是userName
INFO :2020-03-26 22:39:40,821 [ com.LogAspect: ] - 目标方法执行后,可以添加自己的逻辑"success"
INFO :2020-03-26 22:39:40,875 [ com.LogAspect: ] - step inot async intoDB, 我是异步方法,我被调用了

7.4、结果

    至此,我们基于注解实现的日志打印就已经初步完成。由于是样例,代码比较简陋。大家可以根据需要自己在方法内部实现需要的业务逻辑。

8、advices的类型

  • Before advice:在join point之前执行的advice,不能阻止程序的继续运行。
  • After returning advice:在join point完成之后执行的advice
  • After throwing advice:在执行的方法抛出异常之后执行。
  • After advice:在执行的join point退出之后执行不论正常退出或者抛出了异常。
  • Around advice:可以在方法调用之前或之后执行自定义行为,并且还可以选择继续执行join point或者执行另外的方法。

三、最后

    个人经验,有不足之处,欢迎评论指出。谢谢。

    欢迎关注我的个人公众号(cxyknet),不定期分享技术热点、框架原理源码阅读、资料干货。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值