用 @aspect + @around 拦截带自定义注解的方法最稳妥,因仅 @around 能通过 proceed() 获取返回值、捕获异常、精确计时;注解须声明 @retention(runtime) 和 @target(method);参数宜浅层反射打印,异步日志需透传上下文。

直接说结论:用 @Aspect + @Around 拦截带自定义注解的方法最稳妥,别用 @Before 或 @AfterReturning 单独捕获操作日志——它们拿不到完整执行结果和异常信息。
为什么必须用 @Around 而不是其他通知类型
业务日志的关键字段(如耗时、返回值、异常堆栈、参数快照)只有在方法执行前后都能触达的时机才能完整收集。@Before 看不到结果,@AfterReturning 捕不到异常,@AfterThrowing 又拿不到正常返回值。只有 @Around 能用 proceed() 控制执行流,统一兜底。
- 异常发生时,
proceed()会抛出异常,你可以在catch块里记录堆栈 - 正常返回时,
Object result = joinPoint.proceed()能拿到任意类型的返回值 - 耗时计算必须包裹
proceed()前后时间戳,@Around是唯一能精确控制的位置
自定义注解要声明 RetentionPolicy.RUNTIME 和 ElementType.METHOD
如果注解没加 @Retention(RetentionPolicy.RUNTIME),Spring AOP 在运行期根本读不到它;如果只支持 TYPE 或漏了 METHOD,切点表达式 @annotation(com.example.LogOperation) 就会匹配失败,不报错但也不生效。
- 正确写法:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogOperation { String value() default ""; } - 切点表达式必须和注解全限定名一致:
@Pointcut("@annotation(com.example.LogOperation)") - 别在注解里放复杂对象或非基本类型,默认值只能是字符串、类、枚举、注解或它们的数组
参数提取别直接序列化整个 joinPoint.getArgs()
日志中记录参数是为了可追溯,不是存档。直接 JSON.toJSONString(joinPoint.getArgs()) 容易触发 Hibernate 代理对象的懒加载、循环引用、敏感字段泄露(比如密码字段),甚至引发 StackOverflowError。
- 优先用
ToStringBuilder.reflectionToString(arg)(Apache Commons Lang)做浅层反射打印 - 对 Spring MVC 的
@RequestBody参数,建议在 Controller 层用@ModelAttribute或额外封装一层 DTO,避免拦截到原始HttpServletRequest - 明确过滤掉
org.springframework、javax.servlet等框架类型参数,它们对业务日志无意义
异步记录日志时注意事务传播与上下文丢失
如果在 @Around 里直接开新线程或丢进线程池记日志,会出现两个严重问题:一是数据库事务还没提交,日志里却写了“操作成功”;二是 SecurityContext、RequestContextHolder 在子线程里为空,用户 ID、请求路径等关键字段取不到。
- 推荐用 Spring 的
ApplicationEventPublisher发布日志事件,由@EventListener异步消费,天然继承事务状态 - 若必须用线程池,需手动传递
SecurityContextHolder.getContext()和RequestContextHolder.getRequestAttributes() - 千万别在日志记录逻辑里调用
Thread.sleep()或阻塞 I/O,会拖慢主业务响应
真正难的不是写切面,而是让日志字段既完整又安全、既及时又不拖累主流程——参数怎么取、异常怎么归类、耗时怎么算、上下文怎么透传,每个点都得结合具体框架行为来校准。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











