AOP 的灵魂:面向切面编程真的是“魔法”吗
2026/6/6 8:08:29 网站建设 项目流程

很多人第一次接触 AOP 时,感觉像是在看魔术:

“我就加了一个@Transactional注解,也没写commit()rollback(),事务怎么就自动提交了?”
“我就标了个@Log,日志怎么就凭空出现了?”

于是,大家惊呼:“Spring AOP 是魔法!”现在就让我们撕开这层“魔法”的包装纸。世界上没有魔法,只有底层原理设计模式。今天,我们就用手术刀般的精度,把 AOP 解剖给你看。

OOP 造车,AOP 搞服务

首先,我们要理清OOP(面向对象)AOP(面向切面)的关系。它们不是对立的,而是互补的。

  • OOP = 汽车制造厂🏭

    • 你关注的是核心部件:引擎(Engine)、轮胎(Tire)、底盘(Chassis)。
    • 代码里全是业务逻辑orderService.createOrder(),userService.login()
    • 痛点:如果你在制造引擎的代码里,硬编码了“加油”、“洗车”、“买保险”的逻辑,那这台引擎简直没法维护!每次换个保险公司,你都得拆引擎?
  • AOP = 汽车售后服务中心

    • 你关注的是横切服务:加油(事务)、洗车(日志)、安检(权限)、保险(监控)。
    • 核心理念:这些服务是所有车都需要的,但它们不属于某一辆车的核心结构。
    • 运作方式:车(Bean)出厂后,经过“服务站”(AOP 容器)。服务站给车套上一层“保护壳”(代理对象)。
      • 你想加油?车子开进加油站(前置通知),加完油再上路。
      • 你想洗车?车子开完回来,自动进洗车房(后置通知)。
    • 结果:引擎代码里干干净净,只有“转动”的逻辑。加油洗车的逻辑,全在服务站里。

OOP 纵向继承,解决复用;

AOP 横向切割,解决耦合。

核心术语解析

别被英文术语吓到,我们把它翻译成“人话”:当然这比较基础也简单:

术语英文通俗解释 (人话)生活化比喻 (汽车场景)
切面Aspect专门封装了“横切逻辑”的类。
(比如:把日志、事务、权限校验都写在这个类里)

售后服务站;

它不造车,但提供加油、洗车、保养等全套增值服务。

连接点JoinPoint程序运行过程中,所有可能插入代码的位置。
(比如:每个方法执行前、执行后、抛出异常时)
汽车的全生命周期时刻
启动、加速、刹车、熄火、加油、维修...
(理论上这些时刻都可以被干预)
切入点Pointcut从所有连接点中,真正选中要拦截的那些特定位置。
(通过规则过滤,比如只拦截save开头的方法)
选定的服务时刻
你决定只在 “加油” 和 “洗车” 这两个时刻介入服务,其他的(如“加速”、“刹车”)不管。
通知Advice在切入点具体要执行的代码逻辑。
(比如:方法执行前打印日志,执行后提交事务)
具体的动作指令
前置通知:“加满95号汽油”;
后置通知:“记录本次行驶里程”。
织入Weaving把切面代码应用到目标对象,生成代理对象的过程。
(编译时、加载时或运行时)
改装/组装过程
把服务站的功能“贴”到车上,最终交付给用户的是一辆带有售后服务的 “增强版汽车” (代理对象)。

切面(服务站)定义了要在哪些切入点(选定的时刻,如加油)执行什么通知(具体动作,如加满油),通过织入(改装)过程,将这些功能应用到程序的连接点(所有可能的时刻)上,最终实现业务逻辑与非业务逻辑的解耦。

原理:静态 vs 动态,谁是真身

AOP 的实现主要有两派:AspectJSpring AOP。它们的区别决定了性能和灵活性

1. 静态 AOP (AspectJ) —— “编译时整容”

  • 原理:在代码编译阶段(甚至加载阶段),直接修改.class文件字节码
  • 过程
    1. 你写代码:public void save() { ... }
    2. AspectJ 编译器(ajc)介入。
    3. 生成的字节码变成了:
public void save() { logBefore(); // 硬塞进去的代码 // 原始逻辑 logAfter(); // 硬塞进去的代码 }
  • 优点:性能极致(没有代理开销,直接调用),功能最强(能拦截字段访问、构造器等)。
  • 缺点:需要特殊编译器或 Agent,配置复杂,灵活性稍差。其实还好、相对来说
  • 适用场景:对性能极其敏感,或需要拦截非方法调用的场景(如监控字段变化)

2. 动态 AOP (Spring AOP) —— “运行时戴面具”

原理:基于动态代理。不修改原始类,而是在运行时创建一个代理对象(Proxy)

  • 过程
    1. Spring 容器启动。
    2. 发现OrderService需要 AOP。
    3. Spring直接给你OrderService实例,而是给你一个OrderServiceProxy
    4. 当你调用proxy.save()时:
      • Proxy 先执行logBefore()
      • Proxy 调用target.save()(原始对象)。
      • Proxy 再执行logAfter()
  • 实现技术
    • JDK 动态代理:如果目标类实现了接口,默认用这个。基于反射,只能代理接口方法
    • CGLIB:如果目标类没接口,用这个。基于字节码生成子类,重写方法来拦截。
  • 优点:灵活,配置简单(注解搞定),无需特殊编译器。
  • 缺点:有轻微性能损耗(反射/子类调用),只能拦截公共方法。
  • Spring 的选择:默认使用动态 AOP。因为它够用了,且开发体验最好。
ProxyFactory的决策树

“有接口用 JDK,没接口用 CGLIB”,这太浅了。Spring 内部是通过ProxyFactory进行精密计算的。

核心决策逻辑源码剖析

AnnotationAwareAspectJAutoProxyCreator决定要代理一个 Bean 时,它会调用createProxy方法。核心逻辑在DefaultAopProxyFactory中:

// org.springframework.aop.framework.DefaultAopProxyFactory public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { // 决策树开始 if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class..."); } // 情况 A: 目标是接口 -> 使用 JDK 动态代理 if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } // 情况 B: 目标是类 -> 使用 CGLIB return new ObjenesisCglibAopProxy(config); } // 情况 C: 默认情况 (有用户定义的接口) -> 使用 JDK 动态代理 else { return new JdkDynamicAopProxy(config); } }
  • proxyTargetClass=true:如果你强制配置了这个(或在@EnableAspectJAutoProxy(proxyTargetClass = true)中设置),Spring 会无视接口,直接使用 CGLIB。这在某些需要代理protected方法或最终类(配合特定配置)时有用,但会有轻微性能损耗。
  • Optimize:这是一个很少用的标记,用于告诉代理可以做一些激进优化(如去掉某些检查),通常不建议开启。
字节码层面的“真身”对比

场景:UserService接口及其实现UserServiceImpl

方案 A:JDK 动态代理 ($Proxy0)

  • 继承关系extends java.lang.ProxyimplementsUserService,SpringProxy,Advised...
  • 特点
    • 它是一个全新的类,由 JVM 在运行时生成。
    • 不能继承UserServiceImpl的任何非接口方法。
    • 所有方法调用最终都转发给InvocationHandler
$Proxy0 (JDK Proxy) |-- h (InvocationHandler) --> JdkDynamicAopProxy |-- advised (AdvisedSupport) --> 包含 Target(UserServiceImpl) + InterceptorChain
方案 B:CGLIB 动态代理 (UserServiceImpl$$EnhancerBySpringCGLIB$$...)
  • 继承关系extends UserServiceImplimplementsSpringProxy,Advised...
  • 特点
    • 它是UserServiceImpl子类
    • 它可以重写(Override)父类的所有非final方法。
    • 利用MethodInterceptor拦截方法调用。
UserServiceImpl$$EnhancerBySpringCGLIB$$... (CGLIB Proxy) |-- CALLBACK_0 (MethodInterceptor) --> DynamicAdvisedInterceptor |-- advised (AdvisedSupport) --> 包含 Target(UserServiceImpl) + InterceptorChain
  • JDK 代理是组合(持有目标对象)。
  • CGLIB 代理是继承(伪装成目标对象)。
  • 坑点:如果你的 Bean 中有public void internalMethod()没有在接口中定义,JDK 代理无法拦截该方法(因为接口里没有),而 CGLIB 可以。这就是为什么 Spring Boot 2.x 后默认倾向于 CGLIB(proxyTargetClass=true)的原因之一,为了减少这种“意外”。
执行的黑盒 —— 拦截器链的递归模型

AOP 最难理解、也是最核心的部分。Advice 是如何按顺序执行的——递归调用(责任链模式 + 栈帧)

核心类:ReflectiveMethodInvocation

深度源码剖析:Spring AOP 是如何“变魔术”的

场景设定

so easy

@Service public class OrderService { @Transactional // 这是一个切点! public void createOrder() { System.out.println("创建订单..."); // 模拟业务 } }

1. 启动BeanPostProcessor 登场

还记得我们之前讲的BeanPostProcessor吗?Spring AOP 的核心就是一个特殊的 BPP:AnnotationAwareAspectJAutoProxyCreator

时序图:Spring 容器启动与代理创建:简易版

  • 偷梁换柱:容器中存的从来都不是原始的OrderService,而是那个带着“面具”的Proxy
  • 透明性:调用者(Controller 或其他 Service)完全感知不到,因为它们拿到的引用类型依然是OrderService(接口或父类)。

2. 运行:代理对象的“拦截”艺术

在 Controller 中调用orderService.createOrder()时,实际发生了什么

// 这是 Spring 内部生成的代理类的大致逻辑 (简化版) public class OrderService$Proxy extends OrderService implements SpringAopProxy { private OrderService target; // 原始对象 private List<Interceptor> interceptors; // 拦截器链 (事务、日志等) @Override public void createOrder() { // 1. 获取拦截器链 List<Interceptor> chain = this.getInterceptorsAndDynamicInterceptionAdvice(...); // 2. 创建调用链执行器 MethodInvocation invocation = new ReflectiveMethodInvocation(target, this, method, args, chain); // 3. 执行链条 (责任链模式) invocation.proceed(); } } // 责任链执行逻辑 class MethodInvocation { int currentInterceptorIndex = -1; public Object proceed() { currentInterceptorIndex++; if (currentInterceptorIndex == interceptors.size()) { // 链子走完了,调用原始目标方法 return target.invoke(); } // 获取下一个拦截器 (比如事务拦截器) Interceptor interceptor = interceptors.get(currentInterceptorIndex); // 执行拦截器逻辑 (它内部会再次调用 proceed,形成递归) return interceptor.invoke(this); } } // 事务拦截器示例 class TransactionInterceptor implements Interceptor { public Object invoke(MethodInvocation inv) { TransactionStatus tx = beginTransaction(); // 前置:开启事务 try { Object ret = inv.proceed(); // 继续调用链 (下一个拦截器 或 目标方法) commit(tx); // 后置:提交事务 return ret; } catch (Exception e) { rollback(tx); // 异常:回滚事务 throw e; } } }

所谓的“魔法”,其实就是递归调用+责任链模式

  1. 代理方法被调用。
  2. 第一个拦截器(事务)执行前置逻辑。
  3. 调用proceed(),进入第二个拦截器(日志)。
  4. 日志执行前置,调用proceed(),进入原始方法
  5. 原始方法执行完毕,返回。
  6. 日志执行后置,返回。
  7. 事务执行后置(提交),返回。

什么时候该用 AOP

✅ 推荐使用 (横切关注点)❌ 不推荐使用 (核心业务逻辑)
日志记录 (Log):统一记录请求参数、耗时、异常堆栈。业务流程:订单状态流转、金额计算逻辑。
事务管理 (Transaction):数据库原子性操作。数据校验:具体的业务规则校验(如“库存是否充足”)。
权限控制 (Security):检查用户是否有角色/权限。复杂算法:推荐算法、加密解密核心实现。
性能监控 (Monitor):统计 API 响应时间、QPS。状态依赖:逻辑强依赖于特定实例状态的。
缓存处理 (Cache):统一读取/更新缓存逻辑。私有方法:Spring AOP 默认无法拦截私有方法。
参数校验 (Validation):统一 JSR-303 校验。自调用问题:类内部方法互相调用(this.method())不会触发 AOP。
避坑指南:自调用失效 如果你在 ServiceA 内部调用 this.methodB(),而 methodB 有 @Transactional,事务不会生效! 原因:this 指向的是原始对象,绕过了代理对象。 解法:注入自身 (@Autowired ServiceA self) 然后调用 self.methodB(),或者使用 AopContext.currentProxy()。

很多初级开发者喜欢用 AOP 炫技,把业务逻辑拆得七零八落,导致代码难以追踪。这是错误的!

“AOP 不是用来炫技的,它是为了将‘横切关注点’从核心业务中剥离出来,让代码回归纯粹的业务逻辑。”

  1. 纯粹性:你的 Service 层应该只关心“做什么”(业务),而不关心“怎么做保障”(事务、日志)。
  2. 可测试性:剥离了横切逻辑后,你的单元测试可以专注于业务断言,无需 Mock 复杂的日志或事务管理器。
  3. 可维护性:当需要修改日志格式或事务隔离级别时,只需修改一个切面,全系统生效。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询