AOP
Java 面向切面编程(AOP)
一、什么是 AOP
AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,用于将横切关注点(cross-cutting concerns)从业务逻辑中分离出来。
解决的问题
日志、事务、权限校验、性能监控等功能会散落在很多方法里,造成代码重复和耦合。AOP 让你把这些逻辑集中定义,再"织入"到目标方法上。
二、核心概念
| 术语 | 含义 |
|------|------|
| Aspect(切面) | 横切关注点的模块化封装(一个类) |
| Join Point(连接点) | 程序执行中可被拦截的点(如方法调用) |
| Pointcut(切点) | 匹配哪些连接点的表达式 |
| Advice(通知) | 在切点执行的代码(Before/After/Around 等) |
| Weaving(织入) | 把切面应用到目标对象的过程 |
通知类型
@Before— 方法执行前@After— 方法执行后(无论成功失败)@AfterReturning— 正常返回后@AfterThrowing— 抛异常后@Around— 环绕通知(最强大,可控制是否执行原方法)
三、示例:一一对应五大概念
目标类
@Service
public class UserService {
public User getUser(Long id) { // ← 【连接点】
return userRepository.findById(id);
}
public void deleteUser(Long id) { // ← 【连接点】
userRepository.deleteById(id);
}
}
切面类
@Aspect // ← 【切面】整个类就是一个切面
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.UserService.*(..))") // ← 【切点】
public void userServiceMethods() {}
@Before("userServiceMethods()") // ← 【通知】(Before)
public void logBefore(JoinPoint jp) {
System.out.println("调用: " + jp.getSignature().getName());
}
@Around("userServiceMethods()") // ← 【通知】(Around)
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
对应关系
| 概念 | 对应位置 |
|------|---------|
| 切面 Aspect | LoggingAspect 类 |
| 连接点 JoinPoint | getUser()、deleteUser() 等所有可被拦截的方法 |
| 切点 Pointcut | execution( ...UserService.(..)) 筛选表达式 |
| 通知 Advice | logBefore()、measureTime() 方法体 |
| 织入 Weaving | Spring 启动时通过动态代理把切面缝合到目标方法上 |
连接点 vs 切点
- 连接点:可以被拦截的地方(候选集,类似全班同学)
- 切点:实际要拦截哪些(筛选规则,类似"所有姓张的同学")
四、通知的执行顺序
@Around 是"包裹"型通知,它把 @Before 和目标方法都套在 pjp.proceed() 内部。
执行顺序
1. measureTime 进入 → 记录 start
2. measureTime 调用 pjp.proceed() ──┐
3. logBefore 执行 → 打印日志
4. 真正的 getUser(1L) 执行
5. pjp.proceed() 返回 ───────────────┘
6. measureTime 打印耗时
洋葱模型
┌─────────────────────────────────────┐
│ measureTime 开始 │ ← 最外层
│ ┌───────────────────────────────┐ │
│ │ logBefore 打印日志 │ │ ← 中间层
│ │ ┌─────────────────────────┐ │ │
│ │ │ getUser() 真正执行 │ │ │ ← 最内层
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
│ measureTime 打印耗时 │ ← 回到最外层
└─────────────────────────────────────┘
多切面排序
不同切面之间可用 @Order(n) 控制优先级,数字越小越靠外。
五、实现原理
AOP 的本质是代理模式——框架不修改原始类,而是生成代理对象,把通知逻辑"包"在目标方法外面。
两大主流实现
| 实现 | 时机 | 特点 |
|------|------|------|
| Spring AOP | 运行时动态代理 | 轻量,仅拦截 Spring Bean 的方法 |
| AspectJ | 编译期/类加载期字节码织入 | 功能强,可拦截字段、构造器、静态方法 |
Spring AOP 的两种代理方式
JDK 动态代理(目标类实现接口时)
基于 java.lang.reflect.Proxy,生成一个实现相同接口的代理类。
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
// 前置通知
Object result = method.invoke(target, args);
// 后置通知
return result;
}
);
CGLIB 代理(目标类没有接口时)
通过 ASM 字节码框架生成目标类的子类作为代理。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
Object result = proxy.invokeSuper(obj, args);
return result;
});
UserService proxy = (UserService) enhancer.create();
Spring 的选择策略
目标类实现了接口?
├─ 是 → JDK 动态代理(默认)
└─ 否 → CGLIB 代理
可用 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制 CGLIB。
Spring Boot 2.0+ 默认改为 CGLIB,避免开发者忘写接口踩坑。
六、JDK 代理 vs CGLIB
| 维度 | JDK 动态代理 | CGLIB |
|------|------------|-------|
| 代理方式 | 实现接口 | 继承目标类 |
| 是否需要接口 | ✅ 必须 | ❌ 不需要 |
| 能否代理 final 类 | 不涉及 | ❌ 不能 |
| 能否代理 final 方法 | 不涉及 | ❌ 不能 |
| 是否调用目标类构造器 | ❌ 不会 | ✅ 会(可能产生副作用) |
| 依赖 | JDK 原生 | 第三方库(基于 ASM) |
| 现代 JDK 性能 | 更快 | 略慢 |
为什么不能"全用 CGLIB"
1. 无法代理 final 类和方法(继承机制限制)
2. 会调用目标类构造器,可能引入副作用
3. JDK 代理零依赖,符合"面向接口编程"原则
4. 现代 JDK 中 JDK 代理性能更优
七、Spring AOP 完整组件栈
┌─────────────────────────────────────────┐
│ 用户层:@Aspect / @Before / @Around │
├─────────────────────────────────────────┤
│ Spring AOP 框架 │
│ - AnnotationAwareAspectJAutoProxyCreator│ 扫描 @Aspect
│ - AspectJExpressionPointcut │ 解析切点表达式
│ - Advisor(切点 + 通知) │
│ - ProxyFactory │ 生产代理对象
├─────────────────────────────────────────┤
│ 代理生成层 │
│ - JdkDynamicAopProxy → java.lang.reflect│
│ - CglibAopProxy → CGLIB → ASM │
├─────────────────────────────────────────┤
│ JVM │
└─────────────────────────────────────────┘
依赖的库:
spring-aop(核心)spring-aspects(提供@Aspect注解语法)aspectjweaver.jar(切点表达式解析)cglib(已内嵌到spring-core)
八、AspectJ 字节码织入
AspectJ 直接修改字节码,三种织入时机:
| 时机 | 方式 |
|------|------|
| 编译时(CTW) | 用 ajc 替代 javac |
| 编译后(PCW) | 对已编译 jar 用 ajc 再处理 |
| 类加载时(LTW) | Java Agent:-javaagent:aspectjweaver.jar |
九、Spring AOP vs AspectJ
| 维度 | Spring AOP | AspectJ |
|------|-----------|---------|
| 原理 | 运行时动态代理 | 编译/加载时字节码织入 |
| 拦截范围 | 仅 Spring Bean 的方法 | 任意类,包括字段、构造器、静态方法 |
| 性能 | 有反射/代理开销 | 字节码已织入,几乎无开销 |
| 自调用是否生效 | ❌ 不生效(绕过代理) | ✅ 生效 |
| 使用复杂度 | 低 | 需配置编译器或 Agent |
十、常见坑:自调用问题
Spring AOP 是代理模式,同类内方法互相调用不会触发切面:
@Service
public class UserService {
public void a() {
this.b(); // ← 走 this,绕过代理,b() 的切面不生效
}
@Transactional
public void b() { ... }
}
原因:this 指向原始对象,不是代理对象。AspectJ 没有这个问题。
解决办法:
- 注入自己:
@Autowired private UserService self;然后self.b() - 拆分到不同的 Bean
- 改用 AspectJ
十一、典型应用场景
- 日志记录、审计
- 事务管理(
@Transactional底层就是 AOP) - 权限校验、接口鉴权
- 性能监控
- 缓存(
@Cacheable) - 异常统一处理、重试机制
总结
AOP 本质:通过代理模式,把横切逻辑(切面)按规则(切点)织入到目标方法(连接点)外面(通知)。
>
Spring AOP:运行时动态代理,接口走 JDK Proxy,无接口走 CGLIB。
AspectJ:编译/加载期字节码织入,功能更强、性能更好。
>
日常 99% 场景用 Spring AOP 足够,遇到性能敏感或需拦截非方法连接点时再考虑 AspectJ。