小技巧 - 使用自定义注解实现异步日志

980 阅读3分钟

这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战

前言

在工作中常常遇到需要持久化日志的情况,特别是那种做平台的,别人的数据送进来,你再给别人,出问题找不到日志,那不是屎也是屎了

当然具体的日志持久化方案和架构、设计、业务有关,不管怎么样,执行这一部分代码的耗时是无法忽视的


问题的出现

假设需求是用户登录后台系统,要保存登录日志,持久化的方式是存到数据库的一张表中,很简单的一个需求,按照正常的同步代码逻辑可能是这样写的

LoginServiceImpl

@Override
public String login(String username, String password) {
    //生成token,刷新token,缓存token等逻辑
    String token = "生成的新Token并刷新Token";

    //持久化日志到数据库
    LoginLogEntity loginLog = getLoginLog(username, password);
    loginLogDao.save(loginLog);
    return token;
}

这样做有两个缺点:

  • 由于这个日志持久化逻辑是同步的,假如这个保存日志的逻辑失败,用户登录的时候就报异常了
  • 如果这个日志保存的逻辑再复杂一点,用户的每次登录都要等待这一段代码的执行时间

使用AOP实现异步日志

使用AOP将原本同步的操作改为异步

为了便于维护,我们需要创建一个自定义注解,如果不使用注解的话,维护的同事可能并不知道你这个方法织入了AOP,可能造成代码逻辑被忽略,徒增维护成本,另一个原因是注解的形式可以增加复用性;通过自定义注解我们可以实现自动记录日志(入参、出参、响应耗时等等) ,非业务核心代码的分离等等

需要引入Aspectj的依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

创建一个自定义注解

@Retention(RetentionPolicy.RUNTIME)
public @interface LoginLog {
    //定义一个注解参数,用来标识登录不同的系统
    String system();
}

创建这个注解的实现

@Aspect
@Component
@Slf4j
public class LoginLogAspect {
    @Pointcut("@annotation(com.suckmydisk.mapstructdemo.annotation.LoginLog)")
    public void pointCut() {
        //织入点设置为自定义注解
    }

    /** 在注解的方法运行后会调用以下的代码 */
    @AfterReturning(value = "pointCut() && @annotation(loginLog)", returning = "token")
    public void loginLog(JoinPoint joinPoint, String token, LoginLog loginLog) {
        Object[] args = joinPoint.getArgs();
        String username = args[0].toString();
        String password = args[1].toString();
        String loginSystem = loginLog.system();

        log.info("用户名:{}, 密码:{}, token:{},登录的系统是:{}", username, password, token, loginSystem);
    }
}

实际使用

UserServiceImpl类的登录方法上加上日志注解:

@Service
public class UserServiceImpl implements UserService {
    @Override
    @LoginLog(system = "后台管理系统")
    public String login(String username, String password) {
        return "生成的新Token";
    }
}

在LoginController中调用

@PostMapping("/login")
public void login(@RequestParam String username,
                  @RequestParam String password) {
    userService.login(username, password);
}

调用登录接口后,控制台输出:


总结

  1. LoginLogAspect.javaloginLog() 方法中我们配置的是@AfterReturning,这是在被注解的方法返回了值后loginLog()才会被触发
  2. 同样是loginLog() 方法,由于使用了 && @annotation(loginLog) 来获取注解中的属性值,所以方法中要加上参数 LoginLog loginLog,并且自定义注解上要加 @Retention(RetentionPolicy.RUNTIME),不然启动时会报错
  3. 由于AOP的原理是基于动态代理实现的,真实对象之间的调用会使AOP失效,所以编程时要特别注意这点
  4. 多写注释,少留坑,给新来的小伙伴留下最后温柔