Java 动态代理全解析:JDK 代理 VS CGLIB 代理

大家好!今天我们来聊一个在 Java 开发中非常实用但又容易让人混淆的话题 - 动态代理。无论你是在做框架开发,还是使用 Spring、Hibernate 等框架,动态代理都在背后默默工作。特别是在 AOP(面向切面编程)中,动态代理可以说是核心技术了。

Java 中主要有两种动态代理实现方式:JDK 自带的动态代理和 CGLIB 动态代理。它们各有特点,今天我们就通过实例深入分析,带你真正理解这两种技术。

什么是代理模式?

在讲动态代理前,我们先简单理解一下什么是代理模式。

graph LR Client[客户端] --> Proxy[代理对象] Proxy --> RealObject[真实对象]

简单来说,代理模式就是在调用者和实际对象之间加了一个"中间人"。这个中间人可以在不改变原始对象代码的情况下,增强原始对象的功能。比如添加日志、事务控制、权限验证等。

静态代理与动态代理的区别

静态代理 需要我们手动编写代理类,在编译期就确定了代理与被代理的关系。而动态代理则是在运行时动态生成代理类,无需手动编写代理类代码,大大提高了灵活性和可扩展性。

JDK 动态代理:基于接口的代理方式

JDK 自带的动态代理是 Java 原生支持的,它只能代理实现了接口的类,这是它最大的限制。

工作原理

sequenceDiagram participant Client as 客户端 participant Proxy as 代理对象 participant InvocationHandler as 调用处理器 participant RealObject as 真实对象 Client->>Proxy: 调用方法 Proxy->>InvocationHandler: invoke方法 InvocationHandler->>RealObject: 调用真实方法 RealObject-->>InvocationHandler: 返回结果 InvocationHandler-->>Proxy: 返回结果 Proxy-->>Client: 返回结果

JDK 动态代理的核心是InvocationHandler接口和Proxy类:

  1. 你需要实现InvocationHandler接口,在其中定义代理行为
  2. 使用Proxy.newProxyInstance()创建代理对象

代理类生成机制 : JDK 动态生成的代理类是java.lang.reflect.Proxy的子类,同时实现了目标接口。这也解释了为什么 JDK 代理只能转换为接口类型,而不能转换为目标类型 - 因为代理对象实际上是 Proxy 的子类,与目标类没有继承关系。

Proxy.newProxyInstance()方法有三个关键参数:

  • ClassLoader loader:用于定义代理类的类加载器,确保代理类与目标接口在同一类加载空间
  • Class<?>[] interfaces:代理类需要实现的接口列表,决定了代理对象的类型和可用方法
  • InvocationHandler h:方法调用处理器,包含代理的实际行为逻辑

实战案例

假设我们有一个计算器接口和实现:

java 复制代码
// 计算器接口
public interface Calculator {
    int add(int a, int b);
    int subtract(int a, int b);
}

// 实际实现
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int subtract(int a, int b) {
        return a - b;
    }
}

现在我们想在每次计算前后添加日志,不修改原代码的情况下,可以这样用 JDK 动态代理:

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

public class JdkProxyDemo {
    public static void main(String[] args) {
        // 创建真实对象
        Calculator realCalculator = new CalculatorImpl();

        // 创建调用处理器
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("[JDK代理]方法执行前: " + method.getName());
                // 执行真实对象的方法
                Object result = method.invoke(realCalculator, args);
                System.out.println("[JDK代理]方法执行后: " + method.getName() + ", 结果: " + result);
                return result;
            }
        };

        // 创建代理对象
        Calculator proxyCalculator = (Calculator) Proxy.newProxyInstance(
                Calculator.class.getClassLoader(),
                new Class[]{Calculator.class},
                handler);

        // 通过代理对象调用方法
        int addResult = proxyCalculator.add(5, 3);
        System.out.println("最终结果: " + addResult);

        int subtractResult = proxyCalculator.subtract(5, 3);
        System.out.println("最终结果: " + subtractResult);
    }
}

运行结果:

csharp 复制代码
[JDK代理]方法执行前: add
[JDK代理]方法执行后: add, 结果: 8
最终结果: 8
[JDK代理]方法执行前: subtract
[JDK代理]方法执行后: subtract, 结果: 2
最终结果: 2

JDK 动态代理的局限性

  1. 必须有接口:被代理的类必须实现至少一个接口
  2. 只能代理接口方法:非接口方法无法被代理
  3. 代理类型转换限制:只能转换为代理的接口类型,不能转换为被代理类的类型

CGLIB 动态代理:基于继承的代理方式

CGLIB (Code Generation Library) 是一个强大的高性能代码生成库,其工作原理是通过继承被代理类生成子类,所以它可以代理没有接口的类

工作原理

graph TD A[原始类] --> |继承| B[CGLIB生成的子类] B --> |重写方法| C[增强功能] C --> |调用| D[原始方法]

CGLIB 动态代理的核心是MethodInterceptor接口:

  1. 实现MethodInterceptor接口定义拦截行为
  2. 使用Enhancer类创建代理对象

实战案例

首先,需要添加 CGLIB 依赖(如果使用 Spring,已经包含了 CGLIB):

Maven:

xml 复制代码
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

现在我们创建一个没有实现接口的类:

java 复制代码
// 没有实现接口的类
public class MathCalculator {
    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) throw new IllegalArgumentException("除数不能为0");
        return a / b;
    }
}

使用 CGLIB 创建代理:

java 复制代码
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxyDemo {
    public static void main(String[] args) {
        // 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        // 设置父类
        enhancer.setSuperclass(MathCalculator.class);
        // 设置回调
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
                                   MethodProxy proxy) throws Throwable {
                System.out.println("[CGLIB代理]方法执行前: " + method.getName());
                // 调用原始方法 - 注意这里使用的是invokeSuper而非反射调用
                Object result = proxy.invokeSuper(obj, args);
                System.out.println("[CGLIB代理]方法执行后: " + method.getName() + ", 结果: " + result);
                return result;
            }
        });

        // 创建代理对象
        MathCalculator proxyCalculator = (MathCalculator) enhancer.create();

        // 通过代理对象调用方法
        int multiplyResult = proxyCalculator.multiply(5, 3);
        System.out.println("最终结果: " + multiplyResult);

        int divideResult = proxyCalculator.divide(10, 2);
        System.out.println("最终结果: " + divideResult);
    }
}

proxy.invokeSuper vs method.invoke 性能差异

CGLIB 的MethodProxy.invokeSuper和反射的Method.invoke有显著性能差异:

  • method.invoke使用 Java 反射 API 调用方法,需要进行安全检查和参数包装
  • proxy.invokeSuper使用 CGLIB 的 Fast Method 技术,生成了直接调用父类方法的字节码,避免了反射调用的开销
  • 在高频调用场景下,invokeSuper的性能通常比反射调用高 5-10 倍

运行结果:

makefile 复制代码
[CGLIB代理]方法执行前: multiply
[CGLIB代理]方法执行后: multiply, 结果: 15
最终结果: 15
[CGLIB代理]方法执行前: divide
[CGLIB代理]方法执行后: divide, 结果: 5
最终结果: 5

CGLIB 的优势与局限性

优势:

  1. 无需接口:可以代理没有实现接口的类
  2. 更广泛的应用场景:几乎可以代理任何类
  3. 更高的方法调用性能:方法调用时比 JDK 代理更高效

局限性:

  1. 无法代理 final 类:CGLIB 是通过继承实现的,而 final 类不能被继承
  2. 无法代理 final 方法:final 方法不能被重写
  3. 构造函数调用:创建代理对象时会调用父类构造函数,需要确保有可访问的构造函数
  4. 必须引入额外依赖:需要引入 CGLIB 库(虽然 Spring 已包含)
  5. 继承带来的复杂性:代理类是目标类的子类,可能引入类继承层级的复杂性,而 JDK 代理通过接口实现更松耦合

两种代理方式的性能比较

graph LR A[性能比较] --> B[创建代理对象] A --> C[方法调用性能] B --> B1[JDK: 较快] B --> B2[CGLIB: 较慢] C --> C1[JDK: 反射调用,较慢] C --> C2[CGLIB: 直接方法调用,较快]
  1. 创建代理对象

    • JDK 动态代理创建代理对象速度较快
    • CGLIB 生成子类需要更多时间
  2. 方法调用

    • JDK 代理使用反射机制调用方法,有一定性能开销
    • CGLIB 通过子类直接调用方法,避免了反射开销
    • 早期版本中 CGLIB 性能明显优于 JDK
    • JDK 8 以后,两者性能差距缩小
    • 当调用次数增加时,CGLIB 通常表现更好

如何选择 JDK 代理还是 CGLIB 代理?

使用 JDK 动态代理的情况

  1. 被代理类已经实现了接口
  2. 只需要代理接口中定义的方法
  3. 在 JDK 8+环境中性能已经足够好

使用 CGLIB 的情况

  1. 被代理类没有实现接口
  2. 需要代理类中的所有方法,包括非接口方法
  3. 对性能有极高要求的场景(方法会被频繁调用)

Spring AOP 中的选择逻辑

Spring 框架的选择逻辑非常值得借鉴:

  1. 如果目标对象实现了接口,默认使用 JDK 动态代理
  2. 如果目标对象没有实现接口,则使用 CGLIB 代理
  3. 可以强制使用 CGLIB(设置proxy-target-class="true"

在 Spring 中,@Configuration类的特殊处理也使用了 CGLIB 代理:

  • Spring 通过 CGLIB 代理@Configuration
  • 这允许 Spring 在调用同一配置类中的@Bean方法时返回同一实例(单例行为)
  • 解决了 Bean 之间的循环依赖问题

实际应用场景

1. 事务管理

事务管理是代理模式最典型的应用场景之一。下面是一个更贴近实际的事务管理实现:

java 复制代码
import java.sql.Connection;
import java.sql.SQLException;

// 数据库操作接口
public interface UserDao {
    void save(User user) throws SQLException;
    User findById(int id) throws SQLException;
}

// 实现类
public class UserDaoImpl implements UserDao {
    @Override
    public void save(User user) throws SQLException {
        System.out.println("保存用户: " + user.getName());
    }

    @Override
    public User findById(int id) throws SQLException {
        System.out.println("查找用户ID: " + id);
        return new User(id, "用户" + id);
    }
}

// 使用JDK代理实现事务
public class TransactionProxy {
    public static UserDao createProxy(final UserDao target) {
        return (UserDao) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Connection conn = null;
                    try {
                        // 获取数据库连接
                        conn = DataSourceUtils.getConnection();
                        // 开启事务
                        conn.setAutoCommit(false);
                        System.out.println("开始事务...");

                        // 执行实际方法
                        Object result = method.invoke(target, args);

                        // 提交事务
                        conn.commit();
                        System.out.println("提交事务...");
                        return result;
                    } catch (Exception e) {
                        // 发生异常,回滚事务
                        if (conn != null) {
                            try {
                                conn.rollback();
                                System.out.println("回滚事务...");
                            } catch (SQLException ex) {
                                System.err.println("回滚事务失败: " + ex.getMessage());
                            }
                        }
                        throw e;
                    } finally {
                        // 释放连接
                        if (conn != null) {
                            try {
                                conn.close();
                            } catch (SQLException e) {
                                System.err.println("关闭连接失败: " + e.getMessage());
                            }
                        }
                    }
                }
            });
    }
}

// 使用 - 更现代的try-with-resources写法
public class TransactionProxyJava7 {
    public static UserDao createProxy(final UserDao target) {
        return (UserDao) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    try (Connection conn = DataSourceUtils.getConnection()) {
                        conn.setAutoCommit(false);
                        System.out.println("开始事务...");

                        try {
                            Object result = method.invoke(target, args);
                            conn.commit();
                            System.out.println("提交事务...");
                            return result;
                        } catch (Exception e) {
                            conn.rollback();
                            System.out.println("回滚事务...");
                            throw e;
                        }
                    }
                }
            });
    }
}

// 使用
UserDao dao = TransactionProxy.createProxy(new UserDaoImpl());
dao.save(new User(1, "张三"));

2. 方法执行时间统计

监控每个方法的执行时间是性能分析的重要手段:

java 复制代码
// CGLIB实现的性能监控代理
public class PerformanceProxy {
    public static <T> T createProxy(Class<T> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
                                  MethodProxy proxy) throws Throwable {
                long start = System.currentTimeMillis();
                Object result = proxy.invokeSuper(obj, args);
                long end = System.currentTimeMillis();
                System.out.println(method.getName() + "方法执行时间: " + (end - start) + "ms");
                return result;
            }
        });
        return (T) enhancer.create();
    }
}

// 使用
MathCalculator calculator = PerformanceProxy.createProxy(MathCalculator.class);
calculator.multiply(999, 1001);

常见问题与解决方案

1. ClassCastException 异常

问题:使用 JDK 动态代理时,尝试将代理对象转换为目标类型而不是接口类型。

java 复制代码
Calculator realCalculator = new CalculatorImpl();
// ... 创建代理
Calculator proxyCalculator = (Calculator) Proxy.newProxyInstance(...);

// 错误做法 - 会抛出ClassCastException
CalculatorImpl impl = (CalculatorImpl) proxyCalculator; // 错误!

解决方案

  • 只转换为接口类型
  • 如果必须转换为实现类类型,改用 CGLIB 代理

2. "final"方法或类无法被代理

问题:CGLIB 无法代理 final 类或方法。

解决方案

  • 移除 final 修饰符(如果可以修改源码)
  • 使用组合设计模式而不是继承
  • 为类创建接口,并使用 JDK 动态代理

3. "self-invocation"问题

问题:当目标对象内部方法调用自己的其他方法时,这些内部调用不会被代理拦截。

java 复制代码
public class ServiceImpl implements Service {
    public void methodA() {
        System.out.println("Method A");
        // 这个调用不会被代理!
        this.methodB();
    }

    public void methodB() {
        System.out.println("Method B");
    }
}

为了更清晰地理解这个问题,下面展示 CGLIB 代理也存在相同的自调用问题:

java 复制代码
// CGLIB代理示例
public class SelfInvocationDemo {
    public static void main(String[] args) {
        ServiceImpl service = new ServiceImpl();

        // 创建CGLIB代理
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ServiceImpl.class);
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
                                   MethodProxy proxy) throws Throwable {
                System.out.println("进入方法: " + method.getName());
                Object result = proxy.invokeSuper(obj, args);
                System.out.println("离开方法: " + method.getName());
                return result;
            }
        });

        ServiceImpl proxyService = (ServiceImpl) enhancer.create();
        // 调用methodA会间接调用methodB,但methodB的调用不会被拦截
        proxyService.methodA();
    }
}

// 输出结果:
// 进入方法: methodA
// Method A
// Method B    <-- 注意这里没有拦截日志
// 离开方法: methodA

解决方案

  • 避免直接使用 this 调用
  • A 通过 AOP 框架获取代理对象(例如 Spring 的 AopContext)
  • 重构代码移除自调用

4. 多层代理性能问题

问题:在一个对象上应用多个代理层会导致性能下降。

解决方案

  • 合并多个代理为一个
  • 使用责任链模式重构
  • 使用专业的 AOP 框架(如 AspectJ)

总结比较

特性 JDK 动态代理 CGLIB 动态代理
原理 基于接口 基于继承
限制条件 必须实现接口 不能是 final 类/方法
性能 JDK 8 后已优化 方法调用性能略优
创建速度 较快 较慢
方法拦截范围 仅接口方法 所有非 final 方法
适用场景 所有实现了接口的类 几乎所有非 final 类
便捷性 JDK 自带 需要额外依赖
耦合性 接口实现,松耦合 继承实现,紧耦合

结语

动态代理是 Java 中非常强大的技术,掌握了 JDK 代理和 CGLIB 代理的区别和使用方法,你就能更好地理解 Spring AOP 等框架的实现原理,也能在自己的项目中灵活运用这一技术。

记住,选择代理方式应该基于你的实际需求:

  • 如果目标类实现了接口,优先考虑 JDK 动态代理
  • 如果目标类没有实现接口或需要代理所有方法,考虑 CGLIB 代理
  • 在特别注重性能的场景,可以通过基准测试来确定最佳选择

感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~

相关推荐
m0_684598532 小时前
如何开发英语在线训练小程序:从0到1的详细步骤
java·微信小程序·小程序·小程序开发
ml130185288742 小时前
开发一个环保回收小程序需要哪些功能?环保回收小程序
java·大数据·微信小程序·小程序·开源软件
zybishe3 小时前
免费送源码:Java+ssm+MySQL 酒店预订管理系统的设计与实现 计算机毕业设计原创定制
java·大数据·python·mysql·微信小程序·php·课程设计
anlogic4 小时前
Java基础 4.12
java·开发语言
weisian1514 小时前
Java常用工具算法-7--秘钥托管云服务2(阿里云 KMS)
java·安全·阿里云
Alt.95 小时前
SpringMVC基础二(RestFul、接收数据、视图跳转)
java·开发语言·前端·mvc
寒页_5 小时前
2025年第十六届蓝桥杯省赛真题解析 Java B组(简单经验分享)
java·数据结构·经验分享·算法·蓝桥杯
Koma-forever5 小时前
java设计模式-适配器模式
java·设计模式·适配器模式
Yolo@~5 小时前
SpringBoot无法访问静态资源文件CSS、Js问题
java·spring boot·后端
Jennifer33K6 小时前
IDEA 调用 Generate 生成 Getter/Setter 快捷键
java·ide·intellij-idea