日志注解实现异步记录开发与踩坑

57 阅读7分钟

在开发系统日志功能时,我尝试使用 Spring AOP + 自定义注解实现异步日志记录,却发现切面里调用 @Async 异步方法并没有生效。

这篇文章记录了我的踩坑经历和解决方案,方便自己复盘 可以访问我的博客获取更佳显示内容 blog.chenpia.top/

可以访问我的博客获取更佳显示内容:ChenPi's Blog

1.技术背景

在构建日志系统时,我希望实现以下目标:

  • 对标注了自定义注解的方法进行操作日志记录;
  • 在主业务发生事务回滚时,仍能保证日志数据成功写入数据库;
  • 异步写入日志,提高系统性能并避免阻塞主业务线程;
  • 提供可维护、高性能的日志方案。

设计思路:

  1. 定义自定义注解 @OperateLogAnnotation
  2. 使用 Spring AOP 在切面中拦截注解方法;
  3. 将日志异步写入数据库。

2.日志记录实现方式

日志记录通常放在 finally 块中,确保无论方法正常返回还是抛异常,日志都会被记录。

实体类 OperateLog

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
   private Integer id; //ID
   private String operateUser; //操作人ID
   private LocalDateTime operateTime; //操作时间
   private String className; //操作类名
   private String methodName; //操作方法名
   private String methodParams; //操作方法参数
   private String returnValue; //操作方法返回值
   private String exceptionInfo;//异常信息
   private Long costTime; //操作耗时
}

自定义注解 Log

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

DemoController

@RestController
@RequestMapping("/demo")
public class DemoController {

    @Autowired
    private DemoServiceImpl demoService;

    @Log
    @GetMapping("/test")
    public String test(@RequestParam String param) {
        demoService.doBusinessLogic(param);
        return "success";
    }
}

DemoServiceImpl

@Service
public class DemoServiceImpl {

    @Autowired
    private DemoMapper demoMapper;

    @Transactional(rollbackFor = Exception.class)
    public void doBusinessLogic(String param) {
        // 模拟数据库操作
        demoMapper.insertDemo(param);
        
        // 模拟异常测试事务回滚
        // int i = i/0;
    }
}

LogAspect

@Aspect
@Component
@Slf4j
public class LogAspect {

   @Autowired
   private LogService logService;

   @Autowired
   private Executor logExecutor;

   @Autowired
   private OperateLogMapper operateLogMapper;
   
   // 处理逻辑
}

AsyncConfig

@Configuration
@EnableAsync
public class AsyncConfig {

   @Bean("logExecutor")
   public Executor logExecutor() {
       return new ThreadPoolExecutor(
               4,          // 核心线程数
               8,                      // 最大线程数
               60L,                    // 空闲线程存活时间
               TimeUnit.SECONDS,
               new LinkedBlockingQueue<>(100),  // 队列容量
               new ThreadFactoryBuilder().setNamePrefix("log-async-").build(),
               new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
       );
       // return executor;
   }
}

AsyncConfig

启动类上要加上@EnableAsync注解

@SpringBootApplication
@ComponentScan(basePackages = "com.nanoch")
@MapperScan("com.nanoch.syncheart.mapper")
@EnableAspectJAutoProxy
@EnableAsync	// 使用@Async要加上
public class SyncHeartApplication {

   public static void main(String[] args) {
       SpringApplication.run(SyncHeartApplication.class, args);
       System.out.println("启动成功。。。❥(^_-)");
   }

}

整体思路:

@Around("@annotation(com.nanoch.syncheart.annotation.Log)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
    // 公共信息
    String operateUser = "1"; // 模拟当前登录用户
    LocalDateTime operateTime = LocalDateTime.now();
    String className = joinPoint.getTarget().getClass().getName();
    String methodName = joinPoint.getSignature().getName();
    String methodParams = Arrays.toString(joinPoint.getArgs());

    long begin = System.currentTimeMillis();
    Object result = null;
    String exceptionInfo = null;

    try {
        result = joinPoint.proceed();
        return result;
    } catch (Exception e) {
        log.error("操作异常: {}", e.getMessage());
        exceptionInfo = e.getMessage();
        throw e;
    } finally {
        long end = System.currentTimeMillis();
        long costTime = end - begin;

        // 构建日志对象
        String returnValue = new ObjectMapper().writeValueAsString(result);
        OperateLog operateLog = new OperateLog(null, operateUser, operateTime,
                className, methodName, methodParams, returnValue, exceptionInfo, costTime);
        
        // ===== 日志记录方式 =====
        // 下面根据方式替换即可
    }
}

2.1.同步记录

// ===== 同步记录 =====
try {
    operateLogMapper.insert(operateLog);
    log.info("同步记录日志: {}", operateLog);
} catch (Exception e) {
    log.error("记录日志失败: {}", operateLog, e);
}

特点:

  • 简单直接,阻塞主线程

2.2.线程池异步记录

// ===== 线程池异步记录 =====
logExecutor.execute(() -> {
    try {
        operateLogMapper.insert(operateLog);
        log.info("线程池异步记录日志: {}", operateLog);
    } catch (Exception e) {
        log.error("异步日志写入失败: {}", operateLog, e);
    }
});

特点

  • 异步执行,提高性能,可复用线程池,节省资源

2.3.@Async 注解异步记录

// ===== @Async 注解方式 =====
logService.recordLogAsync(operateLog);

特点

  • 使用 Spring AOP 内置异步功能,异步方法需在独立 Service,类内部调用不生效

3.线程池白建了?——类内部调用 @Async 的坑

其实一和二的实现都没什么踩坑的,一般写到二就可以了,我是想到有这个@Async,就像简化一下代码,就是懒......

一开始,我把日志记录方法抽到一个独立方法里,并加上:

@Async("logExecutor")
public void recordLogAsync(OperateLog operateLog) { ... }

然后在切面里直接调用:

recordLogAsync(operateLog);

日志功能是正常运行了,但是控制台日志功能的输出,线程名并没有按照我定义的线程池前缀 log-async-打印输出。

场景复现: 我美滋滋地写了自定义线程池,前缀叫 log-async-,结果控制台死活打印不出这串前缀,线程名居然是 http-nio-8090-exec-4! 更离谱的是,日志功能看着一切正常,仿佛异步已经跑起来了。 我:???被自家线程池白嫖了!091AEB45

  • 当时写的“错误示例”:

    @Aspect
    @Component
    @Slf4j
    public class LogAspect {
        
        @Autowired
        private Executor logExecutor;
    
        @Autowired
        private OperateLogMapper operateLogMapper;
        
        // 处理逻辑
        @Around("@annotation(com.nanoch.syncheart.annotation.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
            // ===== 日志记录方式 =====
        
        	// ===== @Async 注解方式 =====
    		recordLogAsync(operateLog);
        }
        
        @Async("logExecutor")
        public void recordLogAsync(OperateLog operateLog) {
            log.info("异步日志记录任务开始,当前线程: {}", Thread.currentThread().getName());
            try {
                operateLogMapper.insert(operateLog);
                log.info("异步记录操作日志: {}", operateLog);
            } catch (Exception e) {
                log.error("异步记录系统操作日志失败,日志信息:{}", operateLog, e);
            }
        }
    }
    
  • 运行结果——“假异步”

    控制台输出:

    image-20251110185716157

    能看出跑在http-nio-8090-exec-4线程,与主业务线程一致,属于同步调用,我人都傻了: 这分明是 Tomcat 的同步请求线程,哪门子异步?!

    通过debug,发现的确是跑在http-nio-8090-exec-4线程。

    image-20251110185437453

我想知道为什么没有用上,但是功能为什么还正常跑起来了,去网上查了一下为什么 @Async 方法不走 Spring 代理?091A5474

原因分析

  1. Spring AOP 代理机制
    • @Async 的异步功能依赖 Spring 的 AOP 代理实现
    • Spring 会通过 JDK 动态代理CGLIB 为带有 @Async 的方法生成代理对象
    • 异步逻辑实际上在代理对象里执行,如果调用没有走代理,就不会触发线程池
  2. 类内部调用不走代理
    • 当在类内部直接调用 recordLogAsync()(相当于 this.recordLogAsync())时,调用的是原对象自身的方法,而非代理对象的方法
    • 结果:@Async 注解逻辑失效,自定义线程池没生效

正确做法

  • 将异步方法放在独立的 Service 类
  • 通过 Spring 注入的代理对象调用异步方法:
@Slf4j
@Service
public class LogService {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Async("logExecutor")
    public void recordLogAsync(OperateLog operateLog) {
        log.info("异步日志记录任务开始,当前线程: {}", Thread.currentThread().getName());
        try {
            operateLogMapper.insert(operateLog);
            log.info("异步记录操作日志: {}", operateLog);
        } catch (Exception e) {
            log.error("异步记录系统操作日志失败,日志信息:{}", operateLog, e);
        }
    }
}
@Autowired
private LogService logService;

// ===== @Async 注解方式 =====
logService.recordLogAsync(operateLog); // 走代理,线程池生效

成功:

image-20251110193831579

4.@Async 异步失效的 9 种场景

伪代码如下:

@Slf4j
@Service
public class UserService {

    @Async
    public void async(String value) {
        log.info("async:{}", value);
    }
}

4.1. 未使用 @EnableAsync

在 Spring 中要启用 @Async 功能,需要在启动类或配置类上添加:

@EnableAsync
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • @EnableAsync 是开关,默认关闭
  • 未添加该注解时,异步方法不会生效

4.2. 内部方法调用

@Slf4j
@Service
public class UserService {

    public void test() {
        async("test");
    }

    @Async
    public void async(String value) {
        log.info("async:{}", value);
    }
}
  • 类内部调用 async() 时,相当于 this.async()
  • Spring 代理未走,因此异步失效
  • 正确做法:将异步方法放在独立 Service,通过注入的代理对象调用

4.3. 方法非 public

@Slf4j
@Service
public class UserService {

    @Async
    private void async(String value) {
        log.info("async:{}", value);
    }
}
  • 代理无法调用 private 方法
  • 方法必须是 public

4.4. 方法返回值错误

@Service
public class UserService {

    @Async
    public String async(String value) {
        log.info("async:{}", value);
        return value;
    }
}
  • 异步方法返回值必须是 voidFuture
  • 返回其他类型会导致异步失效

4.5. 方法用 static 修饰

@Async
public static void async(String value) { ... }
  • static 方法无法被重写
  • 异步功能失效

4.6. 方法用 final 修饰

@Async
public final void async(String value) { ... }
  • final 方法无法被重写
  • 异步功能失效

4.7. 业务类未加 @Service / @Component

//@Service
public class UserService {

    @Async
    public void async(String value) { ... }
}
  • Spring 容器未管理该类
  • 异步功能失效

4.8. 自己 new 对象

UserService userService = new UserService();
userService.async("test");
  • 手动创建的对象不在 Spring 容器
  • 异步功能失效

4.9. Spring 无法扫描异步类

  • @ComponentScan 没有包含 Service 类所在包
  • Spring 不会管理该类
  • 异步功能失效

4.10.总结

@Async 异步失效主要与 Spring AOP 代理 相关:

  • 方法必须 public
  • 返回值必须 voidFuture
  • 不能是 staticfinal
  • 必须被 Spring 管理
  • 调用必须走代理对象