在开发系统日志功能时,我尝试使用 Spring AOP + 自定义注解实现异步日志记录,却发现切面里调用 @Async 异步方法并没有生效。
这篇文章记录了我的踩坑经历和解决方案,方便自己复盘
1.技术背景
在构建日志系统时,我希望实现以下目标:
- 对标注了自定义注解的方法进行操作日志记录;
- 在主业务发生事务回滚时,仍能保证日志数据成功写入数据库;
- 异步写入日志,提高系统性能并避免阻塞主业务线程;
- 提供可维护、高性能的日志方案。
设计思路:
- 定义自定义注解
@OperateLogAnnotation; - 使用 Spring AOP 在切面中拦截注解方法;
- 将日志异步写入数据库。
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; private String operateUser; 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); } }
|
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() ); } }
|
启动类上要加上@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 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
| 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!
更离谱的是,日志功能看着一切正常,仿佛异步已经跑起来了。
我:???被自家线程池白嫖了!
我想知道为什么没有用上,但是功能为什么还正常跑起来了,去网上查了一下为什么 @Async 方法不走 Spring 代理?
原因分析
- Spring AOP 代理机制
@Async 的异步功能依赖 Spring 的 AOP 代理实现- Spring 会通过 JDK 动态代理 或 CGLIB 为带有
@Async 的方法生成代理对象 - 异步逻辑实际上在代理对象里执行,如果调用没有走代理,就不会触发线程池
- 类内部调用不走代理
- 当在类内部直接调用
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;
logService.recordLogAsync(operateLog);
|
成功:

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; } }
|
- 异步方法返回值必须是
void 或 Future - 返回其他类型会导致异步失效
4.5. 方法用 static 修饰
1 2
| @Async public static void async(String value) { ... }
|
4.6. 方法用 final 修饰
1 2
| @Async public final void async(String value) { ... }
|
4.7. 业务类未加 @Service / @Component
1 2 3 4 5 6
| public class UserService {
@Async public void async(String value) { ... } }
|
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 - 返回值必须
void 或 Future - 不能是
static 或 final - 必须被 Spring 管理
- 调用必须走代理对象