Appearance
第55课:Spring Core - AOP
🎯 学习目标
- 理解 AOP 解决的是横切关注点复用问题,而不是替代业务逻辑。
- 掌握切面、连接点、切点、通知、目标对象、代理对象等核心术语。
- 能编写
@Aspect切面,使用前置、后置、异常、环绕通知。 - 理解 Spring AOP 基于代理,知道自调用、final 方法、代理类型带来的限制。
- 能识别日志、事务、权限、限流、审计等适合 AOP 的场景。
📖 一、为什么需要 AOP
业务方法中经常夹杂横切逻辑:
java
public OrderDTO createOrder(OrderCommand command) {
log.info("create order start");
long start = System.currentTimeMillis();
checkPermission();
try {
OrderDTO result = doCreate(command);
audit("CREATE_ORDER", command);
return result;
} finally {
log.info("create order cost={}", System.currentTimeMillis() - start);
}
}问题:
text
日志、权限、审计、耗时统计重复出现
业务逻辑被横切逻辑淹没
规则变更需要修改大量方法
难以统一控制顺序和异常处理AOP(Aspect-Oriented Programming,面向切面编程)把这些横切关注点抽到切面中。
📖 二、AOP 核心术语
| 术语 | 含义 |
|---|---|
| JoinPoint | 连接点,程序执行点,Spring AOP 中主要是方法执行 |
| Pointcut | 切点,匹配哪些连接点 |
| Advice | 通知,在匹配点执行的增强逻辑 |
| Aspect | 切面,切点 + 通知的组合 |
| Target | 目标对象,原始业务对象 |
| Proxy | 代理对象,调用方实际调用的对象 |
| Weaving | 织入,把增强逻辑应用到目标对象 |
Spring AOP 默认基于代理,不会修改你的字节码文件。
📖 三、添加依赖
Spring Boot 项目中:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>然后就可以使用 @Aspect。
📖 四、基础切面示例
java
@Aspect
@Component
public class ServiceLogAspect {
@Pointcut("execution(* com.example..service..*(..))")
public void serviceMethods() {
}
@Before("serviceMethods()")
public void before(JoinPoint joinPoint) {
log.info("method start: {}", joinPoint.getSignature());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("method success: {}", joinPoint.getSignature());
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
log.warn("method failed: {}", joinPoint.getSignature(), ex);
}
}这个切面会增强 com.example 下 service 包中的方法。
📖 五、环绕通知
环绕通知最强,也最容易写错。
java
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed();
} finally {
long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
log.info("method={}, costMs={}", joinPoint.getSignature(), costMs);
}
}必须调用 joinPoint.proceed(),否则目标方法不会执行。
环绕通知适合:
text
耗时统计
限流
分布式锁
统一日志
动态数据源
幂等控制不适合把复杂业务逻辑塞进去。
📖 六、切点表达式
常用 execution:
java
// 匹配 com.example.service 包下所有类所有方法
execution(* com.example.service.*.*(..))
// 匹配 service 包及子包
execution(* com.example.service..*.*(..))
// 匹配 public 方法
execution(public * *(..))
// 匹配返回值为 UserDTO 的方法
execution(com.example.UserDTO *(..))按注解匹配:
java
@Around("@annotation(com.example.RateLimited)")
public Object rateLimit(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}自定义注解:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
String action();
}使用:
java
@AuditLog(action = "CREATE_ORDER")
public OrderDTO createOrder(OrderCommand command) {
return doCreate(command);
}📖 七、Spring AOP 的代理机制
Spring AOP 通常使用两种代理:
| 代理 | 条件 | 特点 |
|---|---|---|
| JDK 动态代理 | 目标类实现接口 | 基于接口代理 |
| CGLIB 代理 | 目标类无接口或强制类代理 | 基于子类代理 |
调用链:
text
调用方 -> 代理对象 -> 切面逻辑 -> 目标对象方法这解释了很多限制。
⚠️ 八、常见陷阱
1. 自调用失效
java
@Service
public class OrderService {
public void outer() {
inner();
}
@Transactional
public void inner() {
}
}outer() 内部直接调用 inner(),没有经过代理对象,AOP 不生效。事务、缓存、异步注解都有类似问题。
2. final 方法无法被 CGLIB 增强
CGLIB 基于子类覆盖方法,final 方法不能被覆盖。
3. 切点范围过大
java
execution(* com.example..*(..))可能匹配 Controller、Repository、Config、Aspect 自己,导致性能和行为问题。
4. 环绕通知吞异常
错误示例:
java
try {
return pjp.proceed();
} catch (Exception e) {
log.error("failed", e);
return null;
}这会改变业务语义。除非明确设计降级,否则异常应继续抛出。
🆚 九、Java vs C 对比
| 维度 | C 常见方式 | Spring AOP |
|---|---|---|
| 横切逻辑 | 宏、函数包装、回调 | 切面 + 代理 |
| 权限/日志 | 手动嵌入函数 | 注解或切点统一增强 |
| 动态增强 | 函数指针表替换 | JDK Proxy/CGLIB |
| 限制 | 类型信息少 | 受代理边界影响 |
AOP 可以看作一种更结构化的函数包装机制,但它依赖 Java 的反射、代理和容器。
💡 十、最佳实践
- AOP 只处理横切关注点,不写核心业务规则。
- 优先用注解切点表达显式增强,避免切点范围过大。
- 环绕通知必须保证
proceed()调用和异常语义正确。 - 需要事务、缓存、异步时,要理解代理和自调用限制。
- 切面执行顺序用
@Order明确控制。 - 日志切面不要打印敏感参数和大对象。
- 高频方法上的切面要注意性能开销。
- 如果增强逻辑复杂到难以理解,考虑显式代码而不是 AOP。
🧭 十一、适合 AOP 的场景
text
接口访问日志
方法耗时统计
权限校验
审计日志
分布式锁
幂等控制
限流
事务管理
缓存管理
动态数据源不适合:
text
核心业务流程
复杂状态转换
需要明确阅读顺序的业务规则
强依赖调用上下文的逻辑🔍 十二、自测问题
text
AOP 解决什么问题?
Aspect、Pointcut、Advice 分别是什么?
@Around 为什么必须调用 proceed?
Spring AOP 为什么有自调用失效问题?
JDK 动态代理和 CGLIB 有什么区别?
为什么 final 方法可能无法增强?
事务注解为什么本质上依赖 AOP?
什么逻辑不应该写进切面?🧪 十三、实战案例:接口耗时统计切面
一个可落地的耗时切面应该避免打印大参数,并且保留 traceId:
java
@Aspect
@Component
public class ApiCostAspect {
@Around("@within(org.springframework.web.bind.annotation.RestController)")
public Object recordCost(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
String method = pjp.getSignature().toShortString();
try {
return pjp.proceed();
} finally {
long costMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
log.info("api cost, method={}, costMs={}", method, costMs);
}
}
}如果要记录请求参数,应做限制:
text
不打印密码、Token、身份证、银行卡。
不打印 MultipartFile。
不打印超大集合。
只打印必要业务 ID。🧭 十四、AOP 排查清单
切面不生效时,按顺序检查:
text
目标类是否是 Spring Bean?
调用是否经过代理对象?
方法是否 public?
方法是否 final?
切点表达式是否匹配?
切面类是否被扫描到?
是否有多个切面顺序冲突?
是否内部自调用绕过代理?如果是事务、缓存、异步不生效,也可以按这份清单排查,因为它们都依赖 Spring 代理机制。
📌 十五、学习建议
建议用一个小项目分别实现:
text
耗时统计切面
审计日志切面
自定义注解限流切面
故意制造同类自调用失效
故意写错切点表达式再修复这样能真正理解 AOP 的边界,而不是只会背术语。
📚 十六、关键术语速查
text
JoinPoint:可被增强的位置。
Pointcut:筛选哪些位置要增强。
Advice:增强逻辑本身。
Aspect:切点和通知的组合。
Proxy:调用方实际拿到的代理对象。
Target:被代理的业务对象。
Weaving:把增强应用到目标的过程。看懂这些术语,后续理解事务、缓存、异步和安全代理会轻松很多。
🎓 小结
AOP 是 Spring 中非常重要的扩展机制。它让日志、事务、权限、审计等横切逻辑从业务代码中分离出来。但 Spring AOP 基于代理,必须理解代理边界,否则会遇到事务失效、自调用失效、切点误伤等问题。