认识 JDK 和 CGLIB 动态代理

发表于 2023-01-15

什么是动态代理

动态代理是一种创建代理对象的方式,它允许在运行时创建代理类和对象,而不是在编译时创建。这种方式通常用于 AOP 编程中,可以在不改变目标类的情况下给它添加额外的功能。

具体来说,它可以用来在目标类的方法调用前后执行额外的逻辑,例如权限检查、性能监控、事务处理等。另外,它还可以用来实现远程代理、缓存代理、智能引用等功能。

JDK 动态代理

示例

目标类需要实现一个接口,通过查看生成的 Class 文件内容就可以知道 JDK 动态代理的机制,是依赖于目标类的接口而实现的

// TargetInterface 接口是为了创建 JDK 代理
public class Target implements TargetInterface {
	public void method() {
		System.out.println("Target method");
	}
}

增强类需要实现 InvocationHandler 接口及方法,就可以在 invoke 方法中进行增强。在这里可以看到调用目标方法是通过反射的方式。

public class JdkProxy implements InvocationHandler {
	private final Object target;
	public JdkProxy(Object target) {
		this.target = target;
	}
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("JdkProxy invoke");
		return method.invoke(target, args);
	}
}

JDK 动态代理只需要直接调用 Proxy.newProxyInstance(...) 就可以创建动态代理。

// 输出动态代理类 Class 文件到 com/sun/proxy 但在生产环境中不建议使用。
// JDK1.8 前配置
//System.setProperties("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// JDK1.8 后配置
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
// 创建 Proxy
TargetInterface target = (TargetInterface) Proxy.newProxyInstance(
		Target.class.getClassLoader(),
		Target.class.getInterfaces(),
		new JdkProxy(new Target())
);
// 执行目标方法
target.method();

生成的 Class 文件是继承 Proxy 并实现了目标类实现的接口 TargetInterface

public Target$Proxy extends Proxy implements TargetInterface {
    public final void method() throws  {
        try {
            // h 是自定义实现了 InvocationHandler 的代理类
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
}

总结

JDK 动态代理是通过 Java 反射机制来实现的。它可以在运行时动态创建一个实现了一组给定接口的代理类的实例。

JDK 动态代理的优点是使用简单,只需要提供一个实现了 InvocationHandler 接口的类即可。缺点是只能代理接口,不能代理类,而且被代理类必须实现接口

  • 使用步骤
    • 创建一个实现 InvocationHandler 接口的类,它负责处理代理类上的方法调用
    • 使用 Proxy.newProxyInstance() 方法创建代理类实例
    • 通过代理类实例调用目标类的方法
  • 优点
    • 使用简单,只需要提供一个实现了 InvocationHandler 接口的类即可。
    • 可以在不改变目标类的情况下给它添加额外的功能
  • 缺点
    • 只能代理接口,不能代理类
    • 被代理类必须实现接口
    • 性能较差,因为需要反射机制

通过 JDK 动态代理实现 AOP 不需要额外的框架和库,是很简单的。但是如果需要高效的性能,或者需要支持类代理,或者需要织入源码的方式来实现 AOP,那么需要使用其它的 AOP 框架例如接下来说的 CGLIB。

CGLIB 动态代理

示例(Spring CGLIB)

目标类区别于 JDK 动态代理,CGLIB 不需要实现任何接口,因为 CGLIB 的动态代理是通过继承目标类实现的

public class Target {
	public void method() {
		System.out.println("Target method");
	}
}

增强类需要实现 MethodInterceptor 接口及方法,就可以在 intercept 方法中进行增强。

public class CglibProxy implements MethodInterceptor {
	@Override
	public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
		System.out.println("CglibProxy invoke");
		// 调用 invokeSuper 调用目标类
		return methodProxy.invokeSuper(o, objects);
	}
}

创建动态代理需要创建 Enhancer 对象,然后通过增强器配置目标类 Class、增强对象,由增强器来创建动态代理。

public static void main(String[] args) {
	// 配置输出 CGLIB 动态代理生成的 Class 文件
	System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./");
	Enhancer enhancer = new Enhancer();
	enhancer.setSuperclass(Target.class);
	// 设置增强类
	enhancer.setCallback(new CglibProxy());
	// 创建动态代理
	Target target = (Target) enhancer.create();
	System.out.println("Create JDK Proxy");
	target.method();
}

生成的 Class 文件包含多个,这里我们主要看继承目标类并实现接口 Factory 的增强 Class

public class Target$$EnhancerByCGLIB$$f6156be8 extends Target implements Factory {
    public final void method() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
            CGLIB$BIND_CALLBACKS(this);
            var10000 = this.CGLIB$CALLBACK_0;
        }

        if (var10000 != null) {
            var10000.intercept(this, CGLIB$method$0$Method, CGLIB$emptyArgs, CGLIB$method$0$Proxy);
        } else {
            // 从这里的调用可以知道,CGLIB 增强调用是通过子类调用父类的实现的形式
            super.method();
        }
    }
}

总结

CGLIB 动态代理是通过继承来实现的。它可以在运行时动态创建一个继承自目标类的子类来实现代理。

CGLIB动态代理的优点是可以代理任意类,不需要实现接口。缺点是生成的代理类不能被继承。

  • 使用步骤
    • 通过CGLIB Enhancer创建代理类
    • 通过代理类实例调用目标类的方法
  • 优点
    • 可以代理任意类,不需要实现接口
    • 性能比JDK动态代理好
  • 缺点
    • 生成的代理类不能被继承
    • 依赖ASM库

CGLIB 主要应用于 Spring 框架中,基于 ASM 的字节码操作库,可以直接对类进行代理,而不需要接口。 CGLIB 动态代理相对于 JDK 动态代理,性能更高,但是也有其局限性,例如不能对 final 类进行代理。

JDK 和 CGLIB 的动态代理的差异

JDK 和 CGLIB 动态代理是两种不同的代理方式,主要差异如下:

  • 实现方式: JDK 动态代理通过反射机制实现,而 CGLIB 动态代理则通过继承来实现。
  • 代理目标: JDK 动态代理只能代理接口,而 CGLIB 动态代理可以代理任意类(局限:不能对 final 类进行代理)。
  • 性能:
    • CGLIB 通过字节码技术实现的代理比 JDK 动态代理使用的反射执行性能更高
    • CGLIB 通过 ASM 字节码生成多个 Class 文件,所以启动时 CGLIB 的性能相对低于 JDK 动态代理

总的来说, JDK 动态代理适用于接口代理,它生成的代理类继承了 Proxy 类,而 CGLIB 动态代理适用于类代理,它生成的代理类继承了被代理类。