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 有什么区别?