在开发系统日志功能时,我尝试使用 Spring AOP + 自定义注解实现异步日志记录,却发现切面里调用
@Async异步方法并没有生效。这篇文章记录了我的踩坑经历和解决方案,方便自己复盘 可以访问我的博客获取更佳显示内容 blog.chenpia.top/
可以访问我的博客获取更佳显示内容:ChenPi's Blog
1.技术背景
在构建日志系统时,我希望实现以下目标:
- 对标注了自定义注解的方法进行操作日志记录;
- 在主业务发生事务回滚时,仍能保证日志数据成功写入数据库;
- 异步写入日志,提高系统性能并避免阻塞主业务线程;
- 提供可维护、高性能的日志方案。
设计思路:
- 定义自定义注解
@OperateLogAnnotation; - 使用 Spring AOP 在切面中拦截注解方法;
- 将日志异步写入数据库。
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! 更离谱的是,日志功能看着一切正常,仿佛异步已经跑起来了。 我:???被自家线程池白嫖了!
-
当时写的“错误示例”:
@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); } } } -
运行结果——“假异步”
控制台输出:
能看出跑在
http-nio-8090-exec-4线程,与主业务线程一致,属于同步调用,我人都傻了: 这分明是 Tomcat 的同步请求线程,哪门子异步?!通过debug,发现的确是跑在
http-nio-8090-exec-4线程。
我想知道为什么没有用上,但是功能为什么还正常跑起来了,去网上查了一下为什么
@Async方法不走 Spring 代理?
原因分析
- Spring AOP 代理机制
@Async的异步功能依赖 Spring 的 AOP 代理实现- Spring 会通过 JDK 动态代理 或 CGLIB 为带有
@Async的方法生成代理对象 - 异步逻辑实际上在代理对象里执行,如果调用没有走代理,就不会触发线程池
- 类内部调用不走代理
- 当在类内部直接调用
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); // 走代理,线程池生效
成功:
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;
}
}
- 异步方法返回值必须是
void或Future - 返回其他类型会导致异步失效
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 - 返回值必须
void或Future - 不能是
static或final - 必须被 Spring 管理
- 调用必须走代理对象