APE
文章标签归档关于

© 2026 APE.PUB

2026-07-06· 13 分钟

AOP

javaaop

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。

← 返回首页