目录

引言

一、JDK 动态代理 - 来自官方的正统实现

1. 核心原理

2. 代码实战

二、CGLIB 动态代理 - 突破接口的限制

1. 核心原理

2. 代码实战

三、全方位对决:JDK Proxy vs. CGLIB

四、AOP(尤其是Spring AOP)为何倾向于选择CGLIB?

1. 最大的优势:无需接口,为开发提供更大的灵活性

2. 解决了“方法自调用(Self-Invocation)”时AOP失效的问题

3. 代理行为的一致性和类型转换问题


引言

       动态代理模式允许我们在程序运行时,为一个或多个目标对象动态地生成一个代理对象,这个代理对象可以在调用真实方法前后“做手脚”,从而实现功能的增强。在Java生态中,实现动态代理最主流的两种方案便是:JDK动态代理CGLIB


一、JDK 动态代理 - 来自官方的正统实现

        JDK动态代理是Java官方在java.lang.reflect包中提供的原生支持。它的实现非常纯粹,但有一个核心前提:被代理的目标类必须实现至少一个接口

1. 核心原理

        JDK动态代理的核心是利用反射机制。它会在运行时创建一个新的代理类(例如 $Proxy0),这个代理类会实现目标类所实现的所有接口。当你通过代理对象调用接口方法时,这个调用会被转发到我们提供的一个InvocationHandler(调用处理器)的invoke方法中,从而给了我们执行增强逻辑的机会。

关键组件:

  • java.lang.reflect.Proxy:用于创建代理实例的核心工厂类。

  • java.lang.reflect.InvocationHandler:一个接口,我们需要实现它的invoke方法,所有对代理对象的调用都会被转发到这里。

2. 代码实战

让我们通过一个经典的“用户服务”场景来演示。

步骤1:定义接口

// IUserService.java
public interface IUserService {
    void addUser(String username);
}

步骤2:创建目标实现类

// UserServiceImpl.java
public class UserServiceImpl implements IUserService {
    @Override
    public void addUser(String username) {
        System.out.println("【核心业务】数据库中添加用户:" + username);
    }
}

步骤3:创建调用处理器 (InvocationHandler)

// JdkProxyInvocationHandler.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JdkProxyInvocationHandler implements InvocationHandler {
    
    // 被代理的目标对象
    private final Object target;

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

    /**
     * @param proxy 代理对象本身
     * @param method 正在被调用的方法
     * @param args 方法的参数
     */
    @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:测试

// JdkProxyTest.java
import java.lang.reflect.Proxy;

public class JdkProxyTest {
    public static void main(String[] args) {
        // 1. 创建目标对象
        IUserService target = new UserServiceImpl();
        
        // 2. 创建调用处理器
        InvocationHandler handler = new JdkProxyInvocationHandler(target);
        
        // 3. 使用Proxy.newProxyInstance()创建代理对象
        IUserService proxyInstance = (IUserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 类加载器
                target.getClass().getInterfaces(),  // 目标类实现的接口数组
                handler                             // 调用处理器
        );
        
        // 4. 通过代理对象调用方法
        proxyInstance.addUser("CSDN");
        
        // 打印代理对象的类型
        System.out.println("代理对象类型: " + proxyInstance.getClass().getName());
    }
}

输出结果:

【JDK代理-前置通知】处理事务开始...
【核心业务】数据库中添加用户:CSDN
【JDK代理-后置通知】处理事务结束...
代理对象类型: com.sun.proxy.$Proxy0

二、CGLIB 动态代理 - 突破接口的限制

        尽管JDK动态代理很方便,但它“必须实现接口”的限制在某些场景下显得很棘手。如果我们要代理一个没有实现任何接口的普通类怎么办?这时,CGLIB就登场了。

1. 核心原理

        CGLIB通过一个名为ASM的底层字节码处理框架,在运行时动态地为目标类生成一个子类。这个子类会重写父类(目标类)中所有非final的方法。当我们调用代理对象的方法时,实际上是调用了这个子类中重写的方法。CGLIB通过一个MethodInterceptor(方法拦截器)来拦截这些调用。

关键组件:

  • net.sf.cglib.proxy.Enhancer:创建代理对象的核心类。

  • net.sf.cglib.proxy.MethodInterceptor:类似于InvocationHandler,用于拦截方法调用。

2. 代码实战

        用一个例子来演示。

步骤1:添加依赖

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

步骤2:创建目标类(无接口)

// TrainStation.java
public class TrainStation {
    public void sellTicket() {
        System.out.println("【核心业务】火车站售票...");
    }
    
    public final void securityCheck() {
        System.out.println("【核心业务】安检... (final方法)");
    }
}

步骤3:创建方法拦截器

// CglibMethodInterceptor.java
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CglibMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("【CGLIB代理-前置通知】代售点收取服务费...");
        
        // 通过MethodProxy调用父类(目标类)的原始方法
        Object result = proxy.invokeSuper(obj, args);
        
        System.out.println("【CGLIB代理-后置通知】代售点完成售票...");
        return result;
    }
}

步骤4:测试

// CglibProxyTest.java
import net.sf.cglib.proxy.Enhancer;

public class CglibProxyTest {
    public static void main(String[] args) {
        // 1. 创建Enhancer对象
        Enhancer enhancer = new Enhancer();
        
        // 2. 设置父类(目标类)
        enhancer.setSuperclass(TrainStation.class);
        
        // 3. 设置回调(方法拦截器)
        enhancer.setCallback(new CglibMethodInterceptor());
        
        // 4. 创建代理对象
        TrainStation proxyStation = (TrainStation) enhancer.create();
        
        // 5. 调用方法
        proxyStation.sellTicket();
        
        System.out.println("--- 分割线 ---");
        // 调用final方法,观察是否会被代理
        proxyStation.securityCheck();
        
        System.out.println("代理对象类型: " + proxyStation.getClass().getName());
    }
}

输出结果:

【CGLIB代理-前置通知】代售点收取服务费...
【核心业务】火车站售票...
【CGLIB代理-后置通知】代售点完成售票...
--- 分割线 ---
【核心业务】安检... (final方法)
代理对象类型: com.example.cglib.TrainStation$$EnhancerByCGLIB$$...

        从结果可以看出,sellTicket被成功代理,而final方法securityCheck则直接调用了原始逻辑,未被拦截。


三、全方位对决:JDK Proxy vs. CGLIB

现在,我们用一张表格来清晰地总结它们之间的核心区别。

特性JDK 动态代理CGLIB 动态代理
实现原理基于反射机制,动态实现接口基于字节码增强(ASM),动态生成子类
代理前提目标类必须实现至少一个接口目标类不能是final,否则无法继承
代理范围只能代理接口中定义的方法可以代理类中所有非final的方法
外部依赖,是Java原生功能需要引入cglib第三方库
性能创建代理对象较快,方法调用涉及反射,早期版本较慢创建代理对象较慢,方法调用通过FastClass机制,早期版本更快
现代性能在JDK 1.8+,反射调用性能已大幅优化,与CGLIB差距微乎其微,甚至在某些场景下反超优势不再明显,性能差异通常不是选择的关键
Spring AOP如果目标类实现了接口,默认使用JDK代理如果目标类没有实现接口,或配置了proxy-target-class=true,则使用CGLIB

总结

        JDK动态代理和CGLIB是Java动态代理技术中两座不可逾越的高山,它们各自的特点决定了其不同的适用场景。

  • JDK动态代理:作为Java的原生技术,它稳定且无需引入额外依赖。当你设计的系统遵循“面向接口编程”的最佳实践时,它无疑是首选。

  • CGLIB:它打破了接口的限制,使得代理普通类成为可能,极大地扩展了AOP的应用范围。这也是为什么在Spring Boot 2.x之后,即使有接口也默认优先使用CGLIB的原因之一,因为它能避免一些复杂的类型转换问题。


四、AOP(尤其是Spring AOP)为何倾向于选择CGLIB?

        在现代的Spring Boot应用中CGLIB已经成为事实上的默认和推荐选择。即便你的类实现了接口,Spring Boot也倾向于使用CGLIB。

这背后主要有以下几个关键原因:

1. 最大的优势:无需接口,为开发提供更大的灵活性

        这是CGLIB最根本的优势。在大型项目中,并非所有的类都需要或适合抽象出接口。有时候一个类只是一个具体的实现,并没有多态的需求。如果强制要求所有需要AOP增强的类都必须实现接口,会给开发带来不必要的负担和额外的代码量。CGLIB让AOP的应用场景变得更加广泛和灵活。

2. 解决了“方法自调用(Self-Invocation)”时AOP失效的问题

        这是一个非常重要且在面试中经常被问到的陷阱。

问题场景: 假设你有一个被代理的Service类,其中一个方法methodA()调用了同一个类中的另一个方法methodB(),并且methodB()上也配置了AOP通知。

@Service
public class MyService {
    
    @Transactional // 假设这是一个AOP切面
    public void methodB() {
        // ... 数据库操作 ...
    }

    public void methodA() {
        System.out.println("Executing methodA...");
        // 重点看这里!
        this.methodB(); // 或者直接调用 methodB()
    }
}
  • 如果使用JDK动态代理:外部调用proxy.methodA()时,AOP生效。但当methodA()内部执行this.methodB()时,这里的this指向的是原始的目标对象,而不是代理对象。因此,这个调用是一个普通的内部方法调用,它绕过了代理,导致methodB()上的@Transactional切面完全失效

  • 如果使用CGLIB代理:CGLIB创建的是目标类的子类。代理对象本身就是增强后的对象。当methodA()执行时,this指向的是CGLIB创建的子类实例。因此,调用this.methodB()时,调用的是子类中重写后的methodB(),这个调用依然会被AOP拦截,从而保证了切面逻辑的正确执行。

        为了避免这种令人困惑的行为,并使AOP的作用更加一致和可预测,Spring Boot默认采用CGLIB,从根本上解决了自调用失效的问题。

3. 代理行为的一致性和类型转换问题

        使用JDK代理时,从Spring容器中获取的Bean只能被强制类型转换为它所实现的接口类型。如果你试图将它转换为它的实现类类型,会抛出ClassCastException

// MyServiceImpl implements IMyService
IMyService service = context.getBean(IMyService.class); // 正确
MyServiceImpl serviceImpl = (MyServiceImpl) service; // 使用JDK代理时会抛出异常!

        而使用CGLIB,由于代理对象是目标类的子类,所以它既可以被转换为接口类型,也可以被安全地转换为其父类(即目标实现类)的类型。默认使用CGLIB可以保证应用中所有代理对象的行为一致,避免了这类潜在的类型转换风险。

结论: Spring AOP之所以选择并默认推荐CGLIB,是因为它更强大、更灵活、行为更一致,并且解决了JDK代理中令人头疼的“自调用失效”问题,使得AOP的应用更加健壮和符合直觉。

更多推荐