Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Java 后端绕不开的核心能力。它能把日志、事务、权限、监控这些散落在各个业务方法里的横切逻辑抽离成独立切面,让业务代码只关心业务本身。本文用三个可落地的实战案例,讲清楚 Spring AOP 的切点、通知与常见坑,帮你把重复代码彻底收敛。
一、为什么需要 AOP:横切关注点
真实项目里,几乎每个方法都要打日志、做鉴权、统计耗时、处理事务。这些逻辑和业务本身无关,却反复出现在 Service 层的每一个角落,既难维护又容易漏写。AOP 的做法是:把这些“横切关注点”单独写成一个切面(Aspect),再通过切点(Pointcut)精准织入到目标方法上。业务方法因此回到干净的状态,而横切逻辑只维护一份。
需要说明一个底层事实:Spring AOP 默认基于动态代理实现——有接口时用 JDK 动态代理,无接口时用 CGLIB 生成子类。这也解释了为什么很多“切面不生效”的坑都和代理机制有关,后面会专门展开。
举个直观的例子:一个下单方法如果没有 AOP,往往要在开头打印入参、中间开事务、结尾做审计,还要在每个异常分支补日志;一旦换个人维护,很容易漏掉某一处。引入切面后,这些动作被统一收口,方法体只剩“扣库存、写订单”两行核心逻辑,可读性和可测试性都明显提升。这正是 AOP 在团队规范里最受欢迎的原因。
二、核心概念:切点、通知、切面
2.1 切点 Pointcut 怎么写
切点决定“在哪里织入”。最常用的是 execution 表达式:execution(* com.demo.service..*.*(..)) 表示匹配 service 包下所有类的所有方法。配合 @annotation 可以按自定义注解织入,这是做权限、日志最干净的方式。实际项目里建议优先用注解切点,因为业务语义清晰,不会因包名调整而误伤。
| 表达式片段 | 含义 |
|---|---|
| execution(* *(..)) | 任意方法 |
| execution(public * com.demo..*.*(..)) | com.demo 下所有 public 方法 |
| @annotation(com.demo.Log) | 带 @Log 注解的方法 |
| within(com.demo.service..*) | service 包内所有方法 |
| args(java.lang.String) | 仅匹配入参含 String 的方法 |
2.2 五种通知类型
| 通知注解 | 执行时机 | 典型用途 |
|---|---|---|
| @Before | 目标方法前 | 参数校验、权限检查 |
| @After | 目标方法后(无论成败) | 资源清理 |
| @AfterReturning | 正常返回后 | 结果加工、审计 |
| @AfterThrowing | 抛异常后 | 异常告警 |
| @Around | 包裹整个方法 | 日志、耗时、重试 |
其中 @Around 最强大,能拿到 ProceedingJoinPoint,手动控制是否执行目标方法,是实现统一日志和耗时统计的首选。
三、实战一:统一接口日志切面
下面这个切面会在所有 Controller 方法前后打印入参、出参和耗时,再也不用在每个接口里手写日志。
@Aspect
@Component
public class ApiLogAspect {
private static final Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
@Pointcut("execution(* com.demo.controller..*.*(..))")
public void apiPointcut() {}
@Around("apiPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
String name = pjp.getSignature().toShortString();
log.info("==> 进入 {} 入参={}", name, pjp.getArgs());
try {
Object result = pjp.proceed();
log.info("<== 离开 {} 耗时={}ms 出参={}", name,
System.currentTimeMillis() - start, result);
return result;
} catch (Throwable t) {
log.error("<== 异常 {} 耗时={}ms", name,
System.currentTimeMillis() - start, t);
throw t;
}
}
}
关键是 pjp.proceed() 必须被调用一次(且只一次),否则目标方法不会执行。配合 @Order 还能让日志切面在最外层,保证耗时统计覆盖权限、事务等内层切面。
四、实战二:声明式事务的本质就是 AOP
你每天都在用的 @Transactional,底层正是 Spring AOP 的事务切面。它会在方法前开启事务、正常返回时提交、抛异常时回滚。理解这一点,很多事务失效问题就迎刃而解:比如同类方法自调用会绕过代理,导致事务不生效。关于事务的传播行为与失效场景,建议结合专文深入:Spring 事务传播机制深度解析 与 Spring 事务失效实战:8 个不回滚的坑。
五、实战三:基于注解的权限校验切面
自定义一个 @RequireRole 注解,再写一个环绕切面,就能把权限判断从业务代码里彻底移除。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
@Aspect
@Component
public class RoleAspect {
@Before("@annotation(requireRole)")
public void check(RequireRole requireRole) {
String role = CurrentUser.getRole();
for (String r : requireRole.value()) {
if (r.equals(role)) return;
}
throw new AccessDeniedException("角色 [" + role + "] 无权访问");
}
}
// 使用方式
@RequireRole({"ADMIN"})
public void deleteUser(Long id) { ... }
这种“注解 + 切面”的模式在网关、审计、限流里极为常见,比在每个方法里写 if 判断清爽得多。你甚至可以把它和 Spring Boot 异步线程池 @Async 组合,让耗时操作在独立线程执行。
六、切面顺序与优先级
当多个切面作用于同一方法时,用 @Order 指定顺序:数字越小越先执行(@Before 先跑,@After 后跑)。典型排序是:最外层日志切面(捕获总耗时)→ 权限切面 → 事务切面 → 业务方法。
@Aspect
@Component
@Order(1) // 最外层:统一日志
public class ApiLogAspect { ... }
@Aspect
@Component
@Order(2) // 内层:权限校验
public class RoleAspect { ... }
七、常见坑与排查
| 现象 | 根因 | 修复 |
|---|---|---|
| 切面完全不生效 | 同类自调用绕过代理 | 拆到另一个 Bean,或用 AopContext.currentProxy() |
| 私有方法没被织入 | Spring AOP 只代理 public 方法 | 改为 public,或改用 AspectJ 编译期织入 |
| 切点表达式过宽 | execution 匹配到不想要的类 | 收紧包路径,或改用 @annotation |
| 循环依赖报错 | 切面与被切 Bean 相互注入 | 用 @Lazy 或抽离切面依赖 |
最容易踩的是“同类自调用”:在 Service 内部方法 A 调用带 @Transactional 的方法 B,B 的事务会失效,因为调用走的是 this 而非代理对象。解决办法是把 B 拆到另一个 Bean,或通过 AopContext.currentProxy() 拿到代理再调用。AOP 的代理本质决定了这一切,想深入可看 Java 动态代理实战:JDK 与 CGLIB 对比。
另外补充一点代理选型:当目标类没有实现接口时,Spring 会自动切换到 CGLIB,通过继承生成子类来完成织入;若你强制要求基于接口,可在配置中关闭 CGLIB。无论哪种方式,final 类或 final 方法都无法被代理,这也是设计上要留意的约束。
八、性能与最佳实践
- 切点表达式尽量精确,避免对高频方法做昂贵的 @Around 序列化日志。
- 日志切面里不要打印超大对象,防止 OOM 与性能抖动。
- @Around 务必在 finally 或 catch 中保证 proceed 被调用或异常被抛出。
- 对外调用(如 Java HttpClient 调用 REST API)建议统一套一层耗时与异常切面,便于全链路排查。
九、小结
Spring AOP 的价值在于“一处定义、处处生效”。掌握切点表达式、五种通知、@Order 顺序与代理失效这四个要点,你就能把日志、事务、权限等横切逻辑收进干净可复用的切面里,让业务代码回归纯粹。建议从日志切面动手,再逐步扩展到权限与监控,体会切面带来的维护红利。




