代理模式以及常见字节码操作工具
2024-09-30 08:00:49 # Technical # JavaBase

简单梳理下 Java 中的代理模式

静态代理

原理很简单,定义一个接口,被代理类和代理类都实现这个接口,最后调用代理类即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface MyProxyI {
void doProxy();
}

public class A implements MyProxyI {
@Override
public void doProxy() {
System.out.println("this is A");
}
}

public class AA implements MyProxyI {
private final A a;

public AA(A a) {
this.a = a;
}

@Override
public void doProxy() {
System.out.println("Before::");
a.doProxy();
System.out.println("After::");
}
}

优缺点很明显,实现简单是最大的优点,其余比如:代码冗余、扩展性差、维护困难、运行效率低、功能局限等等都是缺点

动态代理

这里主要介绍 jdk 的动态代理与 cglib 的动态代理

jdk 动态代理

先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DynamicProxy implements InvocationHandler {

private final Object target;

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before::");
Object obj = method.invoke(target, args);
System.out.println("After::");
return obj;
}

public Object getProxyObj() {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
}

class DynamicProxyTest {
public static void main(String[] args) {
DynamicProxy dProxy = new DynamicProxy(new A());
MyProxyI proxyObj = (MyProxyI) dProxy.getProxyObj();
proxyObj.doProxy();
}
}

关键点:

  • 被代理类需要实现接口
  • 代理类需要实现 Invocationhandler 接口,重写 invoke 方法
  • Proxy.newProxyInstance 方法中的参数需要通过被代理类去获取,不能通过接口去获取

查看代理对象

启动时配置 jdk 动态代理参数,打开代理生成文件保存 -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true java8 之前的版本配置:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

jdk 会生成一个名为 $Proxy0 的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public final class $Proxy0 extends Proxy implements MyProxyI {
private static final Method m0;
private static final Method m1;
private static final Method m2;
private static final Method m3;

public $Proxy0(InvocationHandler var1) {
super(var1);
}

public final int hashCode() {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final boolean equals(Object var1) {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String toString() {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void doProxy() {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
ClassLoader var0 = $Proxy0.class.getClassLoader();

try {
m0 = Class.forName("java.lang.Object", false, var0).getMethod("hashCode");
m1 = Class.forName("java.lang.Object", false, var0).getMethod("equals", Class.forName("java.lang.Object", false, var0));
m2 = Class.forName("java.lang.Object", false, var0).getMethod("toString");
m3 = Class.forName("net.venom.core.MyProxyI", false, var0).getMethod("doProxy");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}

private static MethodHandles.Lookup proxyClassLookup(MethodHandles.Lookup var0) throws IllegalAccessException {
if (var0.lookupClass() == Proxy.class && var0.hasFullPrivilegeAccess()) {
return MethodHandles.lookup();
} else {
throw new IllegalAccessException(var0.toString());
}
}
}

可以看出,jdk 不仅代理了 MyProxyI 接口的方法,还代理了常见的 hashCodeequalstoString 方法

于是乎,就出现了一下一段非常戏剧性的结果了

1
2
3
4
5
6
7
8
A a = new A();
DynamicProxy dProxy = new DynamicProxy(a);
MyProxyI proxyObj = (MyProxyI) dProxy.getProxyObj();
System.out.println("a == proxyObj ? " + (a == proxyObj) +
"\na:hashCode = " + a.hashCode() +
"\nproxyObj:hashCode = " + proxyObj.hashCode() +
"\na.equals(proxyObj) ? " + a.equals(proxyObj) +
"\nproxyObj.equals(a) ? " + proxyObj.equals(a));

out:

1
2
3
4
5
6
7
8
9
Before::
After::
Before::
After::
a == proxyObj ? false
a:hashCode = 1130478920
proxyObj:hashCode = 1130478920
a.equals(proxyObj) ? false
proxyObj.equals(a) ? true
  • a == proxyObjfalse 是因为 aproxyObj 是两个不同的对象,它们在内存中的地址不同
  • a.hashCode()proxyObj.hashCode() 是相同的,这是因为 Proxy 类的 hashCode 方法被设计为调用其代理的 hashCode 方法,proxyObj.hashCode() 实际上等于 a.hashCode()
  • a.equals(proxyObj) = false 因为 a 并没有重写 equals 方法,所以本质是 a == proxyObj
  • proxyObj.equals(a) = true 同第二点,代理对象的操作委托给被代理对象,所以实际上等于 a.equals(a)

Java 中,equals() 方法的对称性要求是:如果 x.equals(y) 返回 true,那么 y.equals(x) 也应该返回 true。然而,这个对称性的要求是在实现 equals() 方法时需要遵守的约定,Java 运行时系统并不会强制这个约定

CGLIB 动态代理

引入 cglib

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

创建一个被代理类

1
2
3
4
5
class B {
public void say() {
System.out.println("Hello CGLIB");
}
}

创建一个增强类增强被代理方法,与 jdk 代理类似,需要实现一个 cglib 的接口 MethodInterceptor

1
2
3
4
5
6
7
8
9
10
public class CGLIBProxyTest implements MethodInterceptor {

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("「CGLIB」Before::");
Object obj = methodProxy.invokeSuper(o, objects);
System.out.println("「CGLIB」After::");
return obj;
}
}

最后构建代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "./code");
// 通过CGLIB动态代理获取代理对象的过程
Enhancer enhancer = new Enhancer();
// 设置enhancer对象的父类
enhancer.setSuperclass(B.class);
// 设置enhancer的回调对象
enhancer.setCallback(new CGLIBProxyTest());
// 创建代理对象
B proxyB = (B) enhancer.create();
// 通过代理对象调用目标方法
proxyB.say();
}

out:

1
2
3
4
CGLIB debugging enabled, writing to './code'
「CGLIB」Before::
Hello CGLIB
「CGLIB」After::

如果执行出现异常:Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @,需要配置运行参数 VM options
–add-opensjava.base/java.lang=ALL-UNNAMED
–add-opensjava.base/java.lang.invoke=ALL-UNNAMED
–add-opens java.base/java.math=ALL-UNNAMED
–add-opens java.base/java.util=ALL-UNNAMED
–add-opens java.base/java.nio=ALL-UNNAMED
–add-opens java.base/sun.nio.ch=ALL-UNNAMED
–add-opens java.base/java.io=ALL-UNNAMED
–add-opens java.rmi/sun.rmi.transport=ALL-UNNAMED

按需配置即可

然后可以发现生成 code 文件夹

cglib proxy file

生成了 3 个被代理类 B 的代理类

  • B$$EnhancerByCGLIB$$63acc24b:这是主要的代理类,它继承自被代理类,并覆盖了被代理类中的所有非 final 和非 private 方法。当调用这个代理类的方法时,它会调用 MethodInterceptor.intercept 方法,然后在 intercept 方法中,可以添加前置、后置处理逻辑,以及决定是否调用原始方法
  • B$$FastClassByCGLIB$$8b204fb5:这是CGLib为被代理类创建的 FastClass。FastClass 是 CGLib 的一个特性,它可以提高方法调用的效率,因为它避免了 Java 反射的开销。FastClass 包含了被代理类的所有方法的索引,以及调用这些方法的逻辑
  • B$$EnhancerByCGLIB$$63acc24b$$FastClassByCGLIB$$9190102d:这是CGLib为动态代理类(即B$EnhancerByCGLIB\$63acc24b)创建的 FastClass。这个类继承自 FastClass,包含了调用动态代理类的方法的逻辑。当调用动态代理类的方法时,实际上是通过这个 FastClass 的派生类来调用的

TODO:这里仅作简单了解,不深究其原理

invoke 与 invokeSuper

上面增强类中使用的是 invokeSuper 来增强被代理方法。cglib 中还存在另一个方法也具有相同功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CGLIBProxyTest implements MethodInterceptor {

private final Object target;

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

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("「CGLIB」Before::");
Object obj = methodProxy.invoke(target, objects);
// Object obj = methodProxy.invokeSuper(o, objects);
System.out.println("「CGLIB」After::");
return obj;
}
}

与上面不同的是,invoke 方法所需要的对象是 被代理对象

invokeSuper 使用的是 代理对象

这里放下 intercept 方法的说明:

1
2
3
4
5
6
7
8
9
10
All generated proxied methods call this method instead of the original method. The original method may either be invoked by normal reflection using the Method object, or by using the MethodProxy (faster).
形参:
obj – "this", the enhanced object
method – intercepted Method
args – argument array; primitive types are wrapped
proxy – used to invoke super (non-intercepted method); may be called as many times as needed
返回值:
any value compatible with the signature of the proxied method. Method returning void will ignore this value.
抛出:
Throwable – any exception may be thrown; if so, super method will not be invoked

所有生成的代理方法都调用此方法而不是原始方法。原始方法可以通过使用 Method 对象的正常反射来调用,也可以使用 MethodProxy (更快)来调用。
形参:
obj – “this”,增强对象
method – 拦截方法 args参数数组;原始类型被包装
proxy – 用于调用super(非拦截方法);可以根据需要多次调用
返回值:
与代理方法的签名兼容的任何值。返回 void 的方法将忽略该值。

小结

Java动态代理

优点:

  • 简单易用:Java 动态代理是 Java 语言自带的特性,使用起来相对简单,无需引入额外的依赖
  • 接口代理:Java 动态代理只能对接口进行代理,这强制开发者使用接口进行编程,有助于提高代码的可维护性和可测试性

缺点:

  • 只能代理接口:Java 动态代理只能对接口进行代理,不能对类进行代理,这在某些情况下可能会限制其使用
  • 性能较低:相比于 CGLib,Java 动态代理的性能略低,因为它通过反射来调用方法

CGLib动态代理

优点:

  • 可以代理类:CGLib 可以对类进行代理,不仅限于接口,这使得它的应用场景更广泛
  • 性能较高:CGLib 通过生成字节码来创建代理对象,因此其性能通常比 Java 动态代理要高

缺点:

  • 使用复杂:相比于 Java 动态代理,CGLib 的使用更为复杂,需要更多的学习成本
  • 不能代理final方法:由于 CGLib 是通过生成被代理类的子类来实现代理的,因此它不能代理 final 方法,因为 final 方法不能被子类覆盖

AspectJ

AspectJ 是一个面向切面编程(AOP)的框架,它提供了一种方法来使代码中的横切关注点(cross-cutting concerns)更容易地管理和维护。它是一个功能强大且灵活的框架,可以与 Java 语言一起使用,通过 在编译时或运行时修改字节码,实现在代码中插入横切关注点的功能

AspectJ 是一个能实现动态代理功能但 不属于动态代理 的一种 AOP 实现方式

AspectJ 提供了两种主要的织入方式:

  • 编译时织入(compile-time weaving)
  • 运行时织入(runtime weaving)

在这两种织入方式中,AspectJ 都会 直接修改字节码 来实现切面功能,而不是使用动态代理

相比之下,动态代理会在运行时 为目标对象生成一个代理对象,并将方法调用转发到目标对象,同时可以在方法调用前后插入横切逻辑

Spring AOP

Spring AOP 是在运行期间通过代理生成目标类,属于动态代理。默认如果使用接口的,用 JDK 动态代理实现,如果没有接口则使用 CGLIB 实现

Spring AOP 只是采用了 AspectJ 的注解,但是底层编译器和织入器并不是 Aspectj

由于 Spring AOP是基于动态代理来实现的,在容器启动时需要生成代理实例,在方法调用上也会增加栈的深度,使得 Spring AOP 的性能较 AspectJ 更差

ASM

ASM 是一个轻量级的 Java 字节码操作库,用于在 Java 字节码层面上进行动态生成和修改字节码。ASM 提供了一种灵活的方式来直接操作字节码,可以用于在运行时生成新的类、修改已有类的结构、以及增强现有类的功能

CGLIB 基于 ASM 封装

作用:

  • 动态生成类:ASM 可以在运行时动态地生成新的类,包括接口、类和枚举
  • 修改类结构:ASM 可以直接操作已有类的字节码,包括添加新的字段、方法、修改方法体等
  • 增强类功能:ASM 可以在类加载时对已有类进行增强,例如在方法调用前后插入横切逻辑,实现 AOP、性能监控、日志记录等功能
  • 字节码转换:ASM 可以对现有的字节码进行转换,例如将 Java 1.5 字节码转换为 Java 1.4 字节码,或者对字节码进行优化等

特点:

  • 轻量级:ASM 是一个非常轻量级的字节码操作库,不依赖于其他库或框架,可以方便地嵌入到任何 Java 应用程序中
  • 高性能:ASM 的设计非常精简,性能非常高效,可以处理大规模的字节码操作,同时生成的字节码也很精简
  • 灵活性:ASM 提供了丰富的 API 和灵活的字节码操作方式,可以满足各种复杂的需求,包括直接操作字节码指令、使用栈操作、访问局部变量表等
  • 无侵入性:ASM 可以在不修改应用程序源代码的情况下,对应用程序进行增强,具有较强的无侵入性
  • 与字节码紧密集成:ASM 与字节码紧密集成,可以直接操作字节码指令,灵活地进行字节码操作和生成

Javaassist

Javassist(Java Programming Assistant)是一个用于在运行时操作字节码的 Java 库,它允许开发人员动态生成、修改和分析 Java 类的字节码。Javassist 提供了一种更高级别的 API,以 Java 代码的方式来操作字节码,而不需要直接操作复杂的字节码指令,这使得动态代码生成和修改变得更加容易和可维护

Javaassist 的功能与 ASM 类似,二者主要的区别在于

  • Javaassist 提供了高级别的抽象,例如类和方法的操作,使得开发人员可以更容易地理解和操作字节码
  • Javaassist 的 API 设计简单、直观,易于学习和使用,提供了丰富的功能和灵活的扩展点
  • 由于 Javaassist 提供了更高级别的抽象,因此在一些场景下,生成的字节码可能会比较冗余,性能相较 ASM 低
  • Javaassist 提供的功能相对受限,灵活性和定制性可能不如 ASM

Javaagent

Java Agent(Java 代理)是 Java 虚拟机(JVM)提供的一种机制,允许开发人员在应用程序启动时动态地修改已加载的类文件和字节码。Java Agent 主要用于监控、性能调优、代码增强等方面,它可以在应用程序运行期间,通过字节码注入的方式来修改类的行为

作用

  • 监控与诊断:Java Agent 可以用于监控应用程序的运行状态,收集性能指标、错误信息等,并进行诊断和分析
  • 性能调优:通过 Java Agent 可以实现对应用程序的性能调优,例如进行代码热替换、性能分析和优化
  • 代码增强:Java Agent 可以动态地修改类的字节码,实现对现有代码的增强或者添加新的功能,例如实现 AOP、日志记录等

特点:

  • 运行时修改类:Java Agent 可以在应用程序启动时,动态地修改已加载的类文件和字节码,而不需要重新编译和重新部署应用程序
  • 与 JVM 紧密集成:Java Agent 是 JVM 提供的一种官方机制,与 JVM 紧密集成,可以与 JVM 的类加载机制和字节码执行过程进行交互
  • 灵活性:Java Agent 提供了灵活的机制,允许开发人员以插件的形式自定义不同的 Agent,并且可以在运行时动态加载和卸载 Agent
  • 无侵入性:Java Agent 可以在不修改应用程序源代码的情况下,动态地对应用程序进行修改和增强,具有较强的无侵入性

Thanks

Java代理模式和字节码的探索 (qq.com)

CGLIB动态代理探索(ASM,Spring) - 掘金 (juejin.cn)