Spring AOP 代理模式:CGLIB 与 JDK 动态代理区别

Spring AOP 底层默认支持两种代理模式:JDK 动态代理CGLIB 动态代理。很多同学只知道 Spring 会自动选择代理方式,却不清楚两者的区别、适用场景,面试时被问到就卡壳;甚至在项目中遇到代理失效的问题,也不知道如何排查。

一、什么是 Spring AOP 代理?

在讲区别之前,我们先明确一个核心概念:Spring AOP 的本质是「动态代理」,简单来说,就是 Spring 会在运行时,动态生成一个「代理对象」,这个代理对象会包裹住目标对象(我们的业务对象),当我们调用目标对象的方法时,实际上是先调用代理对象的方法,代理对象在方法执行前后,插入增强逻辑(如日志、限流),最后再执行目标对象的核心业务逻辑。

举个通俗的例子:你去房产中介买房,中介就是「代理对象」,你(目标对象)的核心需求是「买房」,而中介会在你买房前后,帮你做很多增强操作(找房源、谈价格、办过户),这些增强操作不影响你买房的核心需求,却能帮你省去很多麻烦------这就是代理模式的核心价值:解耦增强逻辑与业务逻辑

Spring AOP 之所以支持两种代理模式,是为了适配不同的场景(有无接口),下面我们分别拆解两种代理模式的核心原理。

二、核心原理

两种代理模式的核心区别,本质是「生成代理对象的方式不同」,底层依赖的技术也不同,我们分别拆解,结合简单代码示例,让你一眼看懂。

1. JDK 动态代理

JDK 动态代理是 JDK 自带的代理方式,不需要依赖任何第三方jar包,核心依赖 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口,其核心原理是:基于接口生成代理对象,代理对象会实现目标对象所实现的所有接口,通过调用接口方法,触发增强逻辑。

1.1 核心条件

JDK 动态代理有一个硬性要求:目标对象必须实现至少一个接口。如果目标对象没有实现任何接口,JDK 动态代理就无法生成代理对象,此时 Spring 会自动切换为 CGLIB 代理。

1.2 简单示例(手动实现 JDK 动态代理)

我们通过一个简单的案例,手动实现 JDK 动态代理,理解其底层逻辑:

go 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 1. 定义一个接口(JDK动态代理必须有接口)
public interface UserService {
    void addUser(String username);
}

// 2. 实现接口(目标对象)
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String username) {
        System.out.println("核心业务:新增用户 -> " + username);
    }
}

// 3. 实现 InvocationHandler(增强逻辑)
public class JdkProxyHandler implements InvocationHandler {
    // 目标对象(被代理的对象)
    private final Object target;

    // 构造方法,注入目标对象
    public JdkProxyHandler(Object target) {
        this.target = target;
    }

    /**
     * 代理对象的核心方法:所有代理对象的方法调用,都会触发这个方法
     * @param proxy 代理对象本身
     * @param method 目标方法(被调用的方法)
     * @param args 目标方法的参数
     * @return 目标方法的返回值
     * @throws Throwable 异常抛出
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 增强逻辑:方法执行前
        System.out.println("JDK代理 - 方法执行前:记录日志,用户新增操作开始");
        
        // 执行目标对象的核心业务方法
        Object result = method.invoke(target, args);
        
        // 增强逻辑:方法执行后
        System.out.println("JDK代理 - 方法执行后:记录日志,用户新增操作结束");
        
        return result;
    }
}

// 4. 测试 JDK 动态代理
public class JdkProxyTest {
    public static void main(String[] args) {
        // 目标对象
        UserService target = new UserServiceImpl();
        
        // 生成代理对象(核心:Proxy.newProxyInstance)
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 类加载器(与目标对象一致)
                target.getClass().getInterfaces(),  // 目标对象实现的所有接口
                new JdkProxyHandler(target)         // 增强逻辑处理器
        );
        
        // 调用代理对象的方法(实际触发 invoke 方法)
        proxy.addUser("张三");
    }
}
1.3 运行结果
go 复制代码
JDK代理 - 方法执行前:记录日志,用户新增操作开始
核心业务:新增用户 -> 张三
JDK代理 - 方法执行后:记录日志,用户新增操作结束
1.4 总结

JDK 动态代理的核心是「面向接口」,代理对象和目标对象是「兄弟关系」(都实现了同一个接口),通过接口方法间接调用目标对象的方法,增强逻辑在 InvocationHandler 的 invoke 方法中实现。

2. CGLIB 动态代理

CGLIB(Code Generation Library)是一个第三方代码生成库,Spring 已经内置了 CGLIB 依赖(无需额外导入),其核心原理是:基于继承生成代理对象,代理对象会继承目标对象,重写目标对象的方法,在重写的方法中插入增强逻辑。

2.1 核心条件

CGLIB 代理没有接口要求,目标对象可以有接口,也可以没有接口。它通过继承目标对象实现代理,因此有一个注意点:目标对象的方法不能被 final 修饰(如果方法被 final 修饰,子类无法重写,CGLIB 无法实现增强)。

2.2 简单示例(手动实现 CGLIB 代理)

同样通过案例,手动实现 CGLIB 代理,对比 JDK 代理的区别:

go 复制代码
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

// 1. 目标对象(无需实现接口)
public class OrderService {
    public void addOrder(String orderNo) {
        System.out.println("核心业务:新增订单 -> " + orderNo);
    }
    
    // final 方法(CGLIB 无法增强,重写不了)
    public final void deleteOrder(String orderNo) {
        System.out.println("删除订单 -> " + orderNo);
    }
}

// 2. 实现 MethodInterceptor(增强逻辑,类似 JDK 的 InvocationHandler)
public class CglibProxyInterceptor implements MethodInterceptor {
    // 目标对象
    private final Object target;

    public CglibProxyInterceptor(Object target) {
        this.target = target;
    }

    /**
     * 代理对象的核心方法:所有代理对象的方法调用,都会触发这个方法
     * @param o 代理对象本身
     * @param method 目标方法
     * @param args 目标方法参数
     * @param methodProxy 方法代理(用于调用目标方法)
     * @return 目标方法返回值
     * @throws Throwable 异常抛出
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 增强逻辑:方法执行前
        System.out.println("CGLIB代理 - 方法执行前:记录日志,订单新增操作开始");
        
        // 执行目标对象的核心业务方法(两种方式,推荐用 methodProxy.invokeSuper)
        Object result = methodProxy.invokeSuper(o, args);
        // 不推荐:method.invoke(target, args)(会触发二次代理,效率低)
        
        // 增强逻辑:方法执行后
        System.out.println("CGLIB代理 - 方法执行后:记录日志,订单新增操作结束");
        
        return result;
    }
}

// 3. 测试 CGLIB 动态代理
public class CglibProxyTest {
    public static void main(String[] args) {
        // 目标对象
        OrderService target = new OrderService();
        
        // 生成代理对象(核心:Enhancer)
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass()); // 设置父类(目标对象)
        enhancer.setCallback(new CglibProxyInterceptor(target)); // 设置增强逻辑
        OrderService proxy = (OrderService) enhancer.create(); // 生成代理对象
        
        // 调用代理对象的方法(可增强)
        proxy.addOrder("ORDER20260417001");
        
        // 调用 final 方法(无法增强,直接执行目标方法)
        proxy.deleteOrder("ORDER20260417001");
    }
}
2.3 运行结果
go 复制代码
CGLIB代理 - 方法执行前:记录日志,订单新增操作开始
核心业务:新增订单 -> ORDER20260417001
CGLIB代理 - 方法执行后:记录日志,订单新增操作结束
删除订单 -> ORDER20260417001

注意:deleteOrder 方法被 final 修饰,CGLIB 无法重写,因此没有执行增强逻辑,直接调用了目标对象的方法。

2.4 核心总结

CGLIB 代理的核心是「面向继承」,代理对象是目标对象的「子类」,通过重写目标方法实现增强,无需目标对象实现接口,但目标方法不能是 final 修饰。

三、区别对比

这是本文的核心内容,也是面试高频考点。我们从「底层原理、依赖条件、性能、适用场景」等8个维度,对比两种代理模式的区别,用表格呈现,清晰易懂,方便记忆。

对比维度 JDK 动态代理 CGLIB 动态代理
底层原理 基于接口,通过 Proxy 类生成代理对象,实现目标接口 基于继承,通过 Enhancer 生成代理对象,继承目标对象
依赖条件 目标对象必须实现至少一个接口 无接口要求,目标对象可无接口
方法限制 只能增强接口中定义的方法,无法增强类中独有的方法 可增强目标对象的所有非 final 方法(final 方法无法重写,不能增强)
代理对象关系 代理对象与目标对象是「兄弟关系」(同实现一个接口) 代理对象与目标对象是「父子关系」(代理对象是目标对象的子类)
性能表现 生成代理对象速度快,执行速度稍慢(需要通过反射调用接口方法) 生成代理对象速度慢(需要生成子类字节码),执行速度快(直接调用子类方法,无需反射)
依赖jar包 JDK 自带,无需额外导入 需要 CGLIB 依赖(Spring 已内置,无需手动导入)
Spring 优先级 首选:如果目标对象有接口,默认使用 JDK 动态代理 兜底:如果目标对象无接口,自动切换为 CGLIB 代理;也可手动指定强制使用 CGLIB
适用场景 目标对象有接口(如 Service 层接口、Dao 层接口),适合大多数企业级项目场景 目标对象无接口(如普通工具类)、需要增强类中独有的非 final 方法

补充说明:Spring Boot 2.x 版本后,默认情况下,如果目标对象有接口,使用 JDK 动态代理;如果目标对象无接口,使用 CGLIB 代理。如果想强制使用 CGLIB 代理,只需在配置文件中添加一行配置:spring.aop.proxy-target-class=true

四、Spring AOP 代理选择源码解析

很多同学好奇:Spring 是如何自动选择 JDK 还是 CGLIB 代理的?我们通过 Spring 核心源码片段,简单解析其选择逻辑,不用深入看懂每一行代码,重点理解核心判断逻辑即可。

go 复制代码
// Spring AOP 代理选择的核心类:DefaultAopProxyFactory
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        // 核心判断逻辑:
        // 1. config.isOptimize():是否开启优化(默认false)
        // 2. config.isProxyTargetClass():是否强制使用 CGLIB 代理(默认false)
        // 3. hasNoUserSuppliedProxyInterfaces(config):目标对象是否没有实现任何接口
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class");
            }
            // 如果目标对象是接口,还是使用 JDK 动态代理
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            // 否则,使用 CGLIB 代理
            return new ObjenesisCglibAopProxy(config);
        } else {
            // 目标对象有接口,使用 JDK 动态代理
            return new JdkDynamicAopProxy(config);
        }
    }

    // 判断目标对象是否没有实现任何接口(除了 Spring 内部的接口)
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] interfaces = config.getProxiedInterfaces();
        return interfaces == null || interfaces.length == 0 || 
               (interfaces.length == 1 && SpringProxy.class.isAssignableFrom(interfaces[0]));
    }
}
核心逻辑总结
    1. 如果配置了 proxy-target-class=true(强制使用 CGLIB),无论目标对象是否有接口,都使用 CGLIB 代理(除非目标对象是接口);
    1. 如果没有强制配置,判断目标对象是否有接口:有接口 → JDK 代理,无接口 → CGLIB 代理;
    1. 即使目标对象有接口,如果开启了优化(isOptimize=true),也会使用 CGLIB 代理(优化的核心是提升执行速度)。

五、代理失效的3种高频场景及解决方案

在实际项目中,经常会遇到「AOP 增强不生效」的问题,本质都是「代理对象未被调用」,结合两种代理模式的特点,我们总结3种高频失效场景,给出解决方案,帮你快速排错。

场景1:目标方法被 final 修饰(CGLIB 代理失效)

❌ 问题现象:使用 CGLIB 代理时,final 修饰的方法,增强逻辑(如日志、限流)不生效;

✅ 原因:CGLIB 代理基于继承,final 方法无法被子类重写,因此无法插入增强逻辑;

✅ 解决方案:移除目标方法的 final 修饰符,确保方法可被重写。

场景2:目标对象自己调用自己的方法(代理失效)

❌ 问题现象:在 Service 类中,方法 A 调用方法 B,方法 B 添加了 AOP 增强注解(如 @DS、@Log),但增强逻辑不生效;

✅ 原因:方法 A 直接调用方法 B,是「目标对象内部调用」,没有经过代理对象,因此无法触发增强逻辑(无论是 JDK 还是 CGLIB 代理,都会失效);

✅ 解决方案:

    1. 方式1:通过 Spring 上下文获取代理对象,再调用方法 B(推荐);
    1. 方式2:在 Service 类中注入自身代理对象(用 @Autowired 注入,避免直接 this 调用);
    1. 方式3:使用 AopContext.currentProxy() 获取当前代理对象,再调用方法 B。
go 复制代码
// 错误示例(内部调用,代理失效)
@Service
public class OrderService {
    public void methodA() {
        // this 是目标对象,不是代理对象,methodB 的增强不生效
        this.methodB();
    }
    
    @Log // AOP 增强注解
    public void methodB() {
        System.out.println("核心业务方法");
    }
}

// 正确示例(通过代理对象调用)
@Service
public class OrderService {
    @Autowired
    private OrderService orderService; // 注入自身代理对象
    
    public void methodA() {
        // 调用代理对象的 methodB,增强生效
        orderService.methodB();
    }
    
    @Log
    public void methodB() {
        System.out.println("核心业务方法");
    }
}

场景3:目标对象未被 Spring 管理(代理失效)

❌ 问题现象:手动 new 出来的目标对象,添加了 AOP 增强注解,但增强逻辑不生效;

✅ 原因:Spring AOP 只对 Spring 容器管理的 Bean 生成代理对象,手动 new 的对象不属于 Spring 管理,无法生成代理;

✅ 解决方案:通过 @Autowired、@Resource 等方式注入目标对象,不要手动 new。

七、文末小结

Spring AOP 的两种代理模式,是 AOP 核心底层知识,也是企业面试的高频考点,总结下来核心要点:

    1. JDK 动态代理:面向接口,无额外依赖,生成快、执行慢,适合有接口的场景;
    1. CGLIB 动态代理:面向继承,无接口要求,生成慢、执行快,适合无接口或需要增强类独有方法的场景;
    1. Spring 会自动选择代理模式,也可通过配置强制使用 CGLIB;
    1. 代理失效的核心原因是「未调用代理对象」,重点关注 final 方法、内部调用、Spring 管理这3个场景。

掌握这两种代理模式的区别和适用场景,不仅能帮你快速排查项目中的 AOP 问题,还能在面试中脱颖而出。其实不用死记硬背,理解底层原理(接口 vs 继承),很多区别自然就能记住。

如果你在项目中遇到代理相关的问题,或者有其他疑问,欢迎在评论区留言交流,一起避坑、一起进步!

别忘了点赞+在看+收藏三连,关注我,解锁更多 Spring AOP 实战干货,下期再见❤️

相关推荐
RNEA ESIO2 小时前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php
23471021272 小时前
4.15 学习笔记
开发语言·软件测试·python
flushmeteor2 小时前
java的动态代理和字节码生成技术
java·动态代理·代理·字节码生成
eggwyw2 小时前
基于SpringBoot和PostGIS的云南与缅甸的千里边境线实战
java·spring boot·spring
0xDevNull2 小时前
MySQL 别名(Alias)指南:从入门到避坑
java·数据库·sql
浮游本尊2 小时前
一次合同同步背后的多阶段流水线:从外部主数据到本地歧义消解
后端
lv__pf2 小时前
springboot原理
java·spring boot·后端
java1234_小锋2 小时前
Java高频面试题:什么是可重入锁?
java·开发语言