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

这篇文章记录了我的踩坑经历和解决方案,方便自己复盘

1.技术背景

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

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

设计思路:

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

2.日志记录实现方式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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; //操作耗时
}
1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/demo")
public class DemoController {

@Autowired
private DemoServiceImpl demoService;

@Log
@GetMapping("/test")
public String test(@RequestParam String param) {
demoService.doBusinessLogic(param);
return "success";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class DemoServiceImpl {

@Autowired
private DemoMapper demoMapper;

@Transactional(rollbackFor = Exception.class)
public void doBusinessLogic(String param) {
// 模拟数据库操作
demoMapper.insertDemo(param);

// 模拟异常测试事务回滚
// int i = i/0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
@Component
@Slf4j
public class LogAspect {

@Autowired
private LogService logService;

@Autowired
private Executor logExecutor;

@Autowired
private OperateLogMapper operateLogMapper;

// 处理逻辑
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

启动类上要加上@EnableAsync注解

1
2
3
4
5
6
7
8
9
10
11
12
13
@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("启动成功。。。❥(^_-)");
}

}

整体思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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.同步记录

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

特点:

  • 简单直接,阻塞主线程

2.2.线程池异步记录

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

特点

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

2.3.@Async 注解异步记录

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

特点

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

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

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

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

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

然后在切面里直接调用:

1
recordLogAsync(operateLog);

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @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 注入的代理对象调用异步方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);
}
}
}
1
2
3
4
5
@Autowired
private LogService logService;

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

成功:

image-20251110193831579

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

伪代码如下:

1
2
3
4
5
6
7
8
9
@Slf4j
@Service
public class UserService {

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

4.1. 未使用 @EnableAsync

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

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

4.2. 内部方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
@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

1
2
3
4
5
6
7
8
9
@Slf4j
@Service
public class UserService {

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

4.4. 方法返回值错误

1
2
3
4
5
6
7
8
9
@Service
public class UserService {

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

4.5. 方法用 static 修饰

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

4.6. 方法用 final 修饰

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

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

1
2
3
4
5
6
//@Service
public class UserService {

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

4.8. 自己 new 对象

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

4.9. Spring 无法扫描异步类

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

4.10.总结

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

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