Spring AOP 核心原理

候选人小张坐在字节跳动的面试间里,面试官翻到简历上"熟练使用 Spring AOP 做统一日志记录",开口问道:

"Spring AOP 的原理是什么?"

小张说:"用的是动态代理。"面试官点点头,继续追问:"那 Spring AOP 和 AspectJ 有什么区别?"

小张停顿了两秒,说:"AspectJ 是编译期的?"面试官没说话,又问:"CGLIB 代理的 fastClass 机制是什么?"

小张开始擦汗。

【面试官心理】 我问他 AOP 原理,其实不是想听"动态代理"四个字。我想知道的是:他能不能说出 Spring AOP 的织入时机、为什么默认选 CGLIB、@EnableAspectJAutoProxy 到底干了什么。能答出这些的,基本都是 P6 以上的候选人。

一、AOP 核心概念 🔴

1.1 五个基本术语

AOP(面向切面编程)的五个核心概念,很多候选人能背出来,但面试官真正想知道的是:你能不能用自己的话说清楚,以及它们在 Spring 里怎么落地。

概念解释Spring AOP 中的体现
切面(Aspect)横切关注点的模块化封装@Aspect 注解的类
切点(Pointcut)精准定位连接点的表达式@Pointcut("execution(* com.xxx.UserService.*(..))")
通知(Advice)切点处执行的逻辑@Before/@After/@Around
连接点(Join Point)程序执行的某个位置Spring AOP 中只有方法执行
织入(Weaving)将切面应用到目标对象的过程编译期/类加载期/运行期

【面试官心理】 这五个概念我能倒背如成,所以我会跳过名词解释直接追问:请告诉我 Spring AOP 的织入时机是什么时候?为什么?

1.2 ❌ 错误示范

候选人原话:"Spring AOP 用的是 JDK 动态代理,所以能对所有类生效。"

问题诊断

  • 混淆了 JDK 动态代理和 CGLIB 的适用范围
  • 没搞清楚 Spring AOP 默认用哪种代理
  • 以为 AOP 能拦截所有类

面试官内心 OS:这个候选人连 Spring AOP 默认用 CGLIB 都不知道,肯定没看过源码。

1.3 标准回答

P5 级别

Spring AOP 的原理是动态代理。Spring 在运行时为目标对象创建代理对象,代理对象持有目标对象的引用,在方法调用的前后织入通知逻辑。如果目标类实现了接口,Spring 使用 JDK 动态代理;如果没有实现接口,使用 CGLIB 字节码生成代理。

P6 级别

Spring AOP 使用了两种代理机制。默认情况下,Spring 优先选择 CGLIB(从 Spring 3.2 开始 CGLIB 被打包进 spring-core,不再需要单独依赖)。JDK 动态代理要求目标类必须实现接口,代理类与目标类实现同一接口,所以只能拦截接口中声明的方法。而 CGLIB 通过继承目标类生成子类,覆盖非 final 方法来实现代理,可以拦截所有方法,包括 private 方法(注意:final 方法和 static 方法不能被代理)。

P7 级别

Spring AOP 本质上是一个 AOP 织入框架,它的核心是 AopProxy 接口体系。JdkDynamicAopProxy 实现 AopProxy,通过 Proxy.newProxyInstance() 创建 JDK 动态代理;CglibAopProxy 通过 Enhancer 创建 CGLIB 代理,并在内部实现了 FastClass 机制——所谓 FastClass,就是为目标类和代理类各生成一个 FastClass,将方法调用从"反射调用"降维到"直接方法调用",避免在热点路径上大量使用 Method.invoke() 带来的性能开销。这个设计在 Spring 4.x 中被引入,用来弥合 AOP 带来的性能差距。

二、Spring AOP vs AspectJ 🟡

2.1 织入时机对比

这是面试官最爱的追问点。两个字之差,背后是编译期处理运行期处理的根本区别。

维度Spring AOPAspectJ
织入时机运行期编译期/编译后/类加载期
代理方式运行时生成代理对象通过 ajc 编译器直接修改字节码
性能开销有(代理创建+方法调用拦截)无(编译时就织入了)
功能范围只能拦截方法执行可拦截构造器、字段、静态初始化等
配置复杂度低,注解即可高,需要 AspectJ 编译器或 LTW
是否需要源码不需要需要(编译时织入)

2.2 为什么 Spring 还要提供 AspectJ 支持?

Spring AOP 功能有限,但 AspectJ 配置复杂。Spring 的解法是:用 AspectJ 的语法,但用 Spring AOP 的织入时机

关键注解是 @EnableAspectJAutoProxy。它做了什么?

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    // proxyTargetClass = true 时强制使用 CGLIB(默认 true)
    boolean proxyTargetClass() default true;
    // exposeProxy = true 时将代理对象暴露到 AopContext
    boolean exposeProxy() default false;
}

AspectJAutoProxyRegistrar 做的事很直接:往容器里注册了一个 AnnotationAwareAspectJAutoProxyCreator(BeanPostProcessor)。这个后置处理器在 Bean 初始化完成后,扫描所有 @Aspect 注解的切面,为匹配的目标 Bean 生成代理。

【面试官心理】 我问他 @EnableAspectJAutoProxy,其实是在试探他有没有动手看过这个注解的源码,以及知不知道 proxyTargetClassexposeProxy 两个参数的真正用途。大部分候选人只知道"加这个注解才能用 AOP",说不出背后的 BeanPostProcessor 机制。

三、两种代理深度对比 🔴

3.1 JDK 动态代理原理

JDK 动态代理的核心是 Proxy.newProxyInstance(),它需要三个要素:

  1. ClassLoader:用来加载生成的代理类
  2. Interface[]:目标类实现的接口数组(代理类和目标类实现相同接口)
  3. InvocationHandler:方法调用的处理逻辑
public class JdkProxyFactory {

    public static Object createProxy(final UserService target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            (proxy, method, args) -> {
                // 前置通知
                System.out.println("Before: " + method.getName());
                // 调用目标方法
                Object result = method.invoke(target, args);
                // 后置通知
                System.out.println("After: " + method.getName());
                return result;
            }
        );
    }
}

关键限制:JDK 代理只能代理接口,无法代理类。如果 UserService 是一个没有实现任何接口的普通类,JDK 动态代理直接报错。

3.2 CGLIB 代理原理

CGLIB(Code Generation Library)的核心是 Enhancer 类,它通过继承目标类生成子类,覆盖所有非 final 方法来实现拦截。

public class CglibProxyFactory {

    public static Object createProxy(final UserService target) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
            // 前置通知
            System.out.println("Before: " + method.getName());
            // 调用目标方法(注意这里用 methodProxy 而非 method.invoke)
            // methodProxy.invoke(target, args) 避免了反射
            Object result = methodProxy.invoke(target, args);
            // 后置通知
            System.out.println("After: " + method.getName());
            return result;
        });
        return enhancer.create();
    }
}

3.3 FastClass 机制

这是 P6/P7 的分水岭问题。

在 CGLIB 早期版本中,方法调用走的是 method.invoke(target, args) —— 每次调用都要做方法访问检查(JVM 的 AccessibleObject.checkAccess()),开销不小。Spring 4.x 引入 FastClass 后,生成的 FastClass 不走反射,直接通过索引调用方法:

// CGLIB 生成的 FastClass 大致结构
public class UserService$FastClassByCGLIB$xxx extends FastClass {

    public Object invoke(int var1, Object var2, Object[] var3) throws Exception {
        // 根据方法索引 var1 直接 dispatch,不再用反射
        if (var1 == 0) {
            return ((UserService) var2).saveUser((String) var3[0]);
        }
        if (var1 == 1) {
            return ((UserService) var2).getUser((Long) var3[0]);
        }
        // ...
    }

    public int getIndex(String name, Class[] parameterTypes) {
        // 建立方法名+参数类型 到索引的映射
    }
}

调用链从 反射调用(Method.invoke)变成 直接调用(FastClass 索引查找),在高频调用场景下性能提升显著。

3.4 代理的失效场景 ⚠️

90% 的候选人答不出这三个坑:

坑一:内部调用(self-invocation)

@Service
public class OrderService {

    public void createOrder() {
        // 这里调用的是 this,不是代理对象!
        // AOP 通知不会生效!
        this.calculatePrice(); // ⚠️ 失效
    }

    @Transactional
    public void calculatePrice() {
        // 事务不会生效
    }
}

坑二:private 方法永远无法被代理

CGLIB 通过继承实现,private 方法在子类中不可见,所以任何 AOP 通知对 private 方法都无效。

坑三:构造器返回时 target 已经变了

Spring AOP 的代理在目标对象的方法调用前后织入逻辑,但如果目标对象内部直接 new 了一个对象替换自己,代理管不到。

💡

解决内部调用失效,有三种方案:一是用 AspectJ 编译器(编译时织入,绕过代理);二是注入自身(@Autowired private OrderService self);三是开启 exposeProxy = true,通过 AopContext.currentProxy() 获取代理对象。

四、追问升级

第一层追问

面试官:"Spring AOP 是在什么时候创建代理对象的?"

候选人:"Bean 初始化的时候。"

考察点:BeanPostProcessor 机制

第二层追问

面试官:"BeanPostProcessor 和普通 Bean 的执行顺序是什么?"

候选人:... (很多人答不上来)

考察点:Spring Bean 生命周期理解深度

第三层追问

面试官:"为什么 Spring 默认使用 CGLIB 而不是 JDK 动态代理?"

候选人:"因为 CGLIB 更快?"

考察点:Spring 3.x 版本演进、proxyTargetClass 参数的理解

第四层追问

面试官:"你刚才说 private 方法不能被代理,那 Spring 事务的 @Transactional 注解在 private 方法上会怎么样?"

候选人:... (P7 分水岭)

考察点:Spring 事务基于 AOP,私有方法上的注解等同于无效

【面试官心理】 AOP 这道题我通常用来筛选三个层次:知道动态代理的是 60 分,知道 CGLIB 和 fastClass 的是 80 分,知道内部调用失效和 private 方法陷阱的,才是真正在项目里踩过坑的。

五、生产避坑

线上场景:某服务在高峰期大量接口超时,排查发现是 AOP 切面里做了同步的日志 DB 写入。

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.xxx.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        // ⚠️ 问题:每次调用都同步写数据库,高峰期拖垮整个服务
        logService.save(pjp.getSignature() + ", cost=" + (System.currentTimeMillis() - start));
        return result;
    }
}

排查方法

  1. 开启 Spring AOP debug 日志:logging.level.org.springframework.aop=DEBUG
  2. 使用 Arthas 的 trace 命令:trace com.xxx.* * 观察方法耗时分布
  3. 检查切面中的 IO 操作(DB、文件、网络)

正确做法:切面中的耗时操作应该走异步队列,不阻塞主流程。

六、工程选型

场景推荐方案
方法级别的监控和日志Spring AOP(注解驱动,最简洁)
统一事务管理Spring AOP + @Transactional
性能要求极高的切面逻辑AspectJ 编译时织入
需要拦截构造器、字段访问AspectJ(LTW 模式)
类内部方法调用需要被拦截注入自身或 AspectJ