Skip to main content

JDK代理和CGLib代理的区别?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术​

JDK 代理和 CGLib 代理都是 Spring 默认支持的代理模式,它们的区别如下:

  • 代理对象:JDK 代理只支持面向接口代理,而 CGLib 代理除了接口外,也可以面向普通的类进行代理。
  • 实现原理:JDK 代理是生成接口的匿名实现类,而 CGLib 则还可以生成目标类的子类。
  • 拦截方法:JDK 代理只支持拦截接口中的公共抽象方法,而 CGLib 支持拦截任何非私有的实例方法。
  • 内部调用支持:JDK 代理不支持代理内部调用,而 CGLib 支持,理由同上一点。

在默认情况下,Spring 会优先使用 JDK 代理,不过如目标类没有实现一个公共接口,那就会基于 CGLib 进行代理。此外,还有一种特殊情况,那就是基于 @Configuration 的配置类,在 Full 模式下,总是固定使用 CGLib 代理。

问题详解​

1. JDK 代理​

除了 SpringAOP 外,JDK 代理也被广泛的运用于各种框架乃至 JDK 内部,比如 Java 的注解其实也是基于 JDK 代理实现的(AnnotationInvocationHandler)。因此,可以说 JDK 代理是我们日常中最经常打交道的代理方式。

image.png

1.1. 实现原理​

JDK 代理的特征是面向接口代理,它的完整执行流程大致如下:

  1. 提供接口:当你生成代理时,你需要提供一个用于代理的接口;
  2. 创建调用拦截器:你需要提供一个实现了InvocationHandler 接口的调用拦截器,它是真正用于实现代理逻辑的类;
  3. 创建代理对象: 通过 Proxy.newProxyInstance静态工厂方法基于接口生成一个匿名实现类并加载到 JVM,然后再基于该实现类创建一个实例,也就是我们所说的代理对象;
  4. 把调用委托给调用拦截器:代理对象内部会持有方法拦截器 InvocationHandler,当调用该代理对象的方法时,该方法调用将会被委托给拦截器,然后拦截器再真正的执行业务逻辑。

一个典型的 JDK 代理的例子如下:

// 提供一个接口用于代理
public interface Example {
public void sayHello();
}

public ExampleInvocationHandler implements InvocationHandler {

/**
* 执行调用逻辑
*
* @param obj 代理对象
* @param method 被调用的方法
* @param objs 调用的参数
*/
@Override
public Object invoke(Object obj, Method method, Object[] objs) {
throws Throwable {
// 如果调用是的 sayHello 方法,则输出 “Hello world”
if ("sayHello".equals(method.getName())) {
System.out.println("Hello world!");
}
// 其他方法这里不做处理,直接返回一个 null
return null;
}
}

public void run() {
Example proxy = (HelloInterface) Proxy.newProxyInstance(
Example.class.getClassLoader(), // 代理类的类加载器
new Class[]{ Example.class }, // 要代理的接口
handler // 方法拦截器
);
proxy.sayHello(); // = "Hello world!"
}

1.2. 使用限制​

根据 JDK 代理的实现方式,我们不难看出它的一些使用限制:

  • 必须有接口才能代理:也就是说,如果目标类没有实现一个统一的接口,那么就无法代理。
  • 只能代理公共方法:由于本质上是生成了一个接口实现类,因此只要不是接口中的公共方法(更高版本的 JDK 中接口还会有私有方法),全部都无法代理。
  • 只能代理实例方法:理由同上,静态方法属于类而非实例,因此代理对象无法拦截静态方法。
  • 无法处理内部调用:由于只有通过代理对象发起的调用才能被委托到调用拦截器,因此如果被代理对象通过 this 调用内部的方法,那么将无法被拦截。

由于我们在实际项目中的 Service 基本都是基于接口的 JDK 代理,因此老生常谈的 Spring 声明式事务失效的问题本质上就是代理为什么不生效的问题,比如加了 final 修饰符导致事务失效,或者通过 this 调用内部加了事务方法却无法开启事务……等等,本质上都因为没有成功代理调用。

关于 Spring 声明式事务失效的几种场景和原因,请参见:Spring 声明式事务失效的场景?

1.3. 使用场景​

从严格意义上来说,JDK 代理有两种用法:

  • 无中生有:即只有接口而没有实现类,代理相当于凭空为接口创造实现类,比如我们 Myabtis 的 Mapper 接口,Feign 还有 Dubbo 这类框架的 RPC 接口都是如此。
  • 移花接木:即已经有了实现类,但是需要通过代理来实现一些额外的增强,比如我们项目中的 Service 上的声明式事务或声明式重试机制便是如此。

不过,在大多数情况下,JDK 的动态代理还是用于实现第一种需求,在 Spring 中,这种效果往往基于 FactoryBean 实现。

关于 FactoryBean,请参见:✅ 什么是 FactoryBean?

2. CGLIB 代理​

CGLib 代理也是 Spring 的默认代理方式之一,它的和 JDK 代理最大的区别就在于它通过 ASM 操作字节码,从而动态创建目标类的子类来实现代理的效果,可以在目标对象没有实现接口的情况进行代理。

此外,除了 CGLib,目前流行的字节码增强库基本都支持这种模式,比如 ByteBuddy,或者 Javassist。

image.png

2.1. 实现原理​

CGLib 代理与 JDK 代理有一些不同,它同样需要一个调用拦截器 InvocationHandler (在 CGLib 中叫 Callback),但是却基于 Enhancer 生成代理。

比如:

// 提供一个类用于代理
public abstract class Example {
public abstract void sayHello();
}

// 注意,这里的接口是 org.springframework.cglib.proxy.InvocationHandler
public ExampleInvocationHandler implements InvocationHandler {

/**
* 执行调用逻辑
*
* @param obj 代理对象
* @param method 被调用的方法
* @param objs 调用的参数
*/
@Override
public Object invoke(Object obj, Method method, Object[] objs) {
throws Throwable {
// 如果调用是的 sayHello 方法,则输出 “Hello world”
if ("sayHello".equals(method.getName())) {
System.out.println("Hello world!");
}
// 其他方法这里不做处理,直接返回一个 null
return null;
}
}

public static void run() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Foo.class);
// 注意,这里的类是 org.springframework.cglib.proxy.InvocationHandler,
// 它本质上是 org.springframework.cglib.proxy.Callback
enhancer.setCallback((InvocationHandler) (proxy, method, args1) -> {
// 如果调用是的 sayHello 方法,则输出 “Hello world”
if ("sayHello".equals(method.getName())) {
System.out.println("Hello world!");
}
// 其他方法这里不做处理,直接返回一个 null
return null;
});
((Example)enhancer.create()).sayHello();
}

当然,CGLib 同样可以基于接口创建代理,在这种情况下它与 JDK 代理并无差异。

2.2. 与 JDK 代理的使用区别​

由于代理方式发生了变化,因此它在使用上与 JDK 代理也有了区别:

  • 可以基于接口或类进行代理:CGLib 支持基于接口代理,但是也可以基于类代理,在后一种情况下它将会生成目标类的子类。
  • 代理类需要可以继承:由于需要基于代理类,因此代理类不可以被 final 修饰,否则就没法继承了。
  • 可以代理任何非私有方法:由于 JDK 的方法重写可以重新父类中除了私有方法外的所有方法(除非被 final 修饰),因此与 JDK 代理不同,它可以代理被 protected 或被默认修饰符修饰的方法。
  • 可以代理内部调用:由于相当于直接重写了内部方法,因此你通过 this 调用非私有方法时,仍然可以触发代理。

2.3. 基于 @Configuration 的配置类代理​

CGLib 这种基于子类的代理无疑比 JDK 代理要强大很多——尤其是可以代理所有非私有方法和支持内部调用这一点。

也正因如此,Spring 对基于 @Configuration 注解的配置类,当在 Full 模式总是会固定使用 CGLib 进行代理,目的就是保证工厂方法在互相调用的情况下,依然能够从 Spring 容器优先获取已经创建的 Bean。

关于 Spring 基于 @Configuration 的 Full 配置模式,请参见:✅ @Configuration 和@Component 有什么区别?