Java中的动态代理

代理模式

代理模式(Proxy)是设计模式中结构型模式的一种,用以实现对目标访问的控制。

结构如下:

当目标接口需要额外的操作才能访问,或是想要对目标访问进行控制时,都可以使用代理模式。
如图,RealSubjectProxy派生自接口SubjectRealSubjectSubject的一个真实的实现,Proxy的对应接口是实际是对RealSubject接口的一个包装,可以在进行RealSubject接口调用前后执行额外操作,如进行设置环境、权限控制、资源控制等,甚至是修改结果。

从这点上来讲,代理模式、装饰器模式(Decorator, Wrapper)和适配器模式(Adapter)非常相似,不过这里就不做比较了。

Java的动态代理

代理模式可以帮助我们解决很多问题。不过,每有一个接口需要进行代理,就需要写一个XXProxy类,然后实现XXFoo方法。在项目中可能会有非常多的接口需要代理,而他们的代理的逻辑可能是一致的,那么就需要做很多大量重复工作了。当然我们也可以编写脚本或者工具自动生成代码,比如icegrpc这些的rpc框架就是这么干的。

但是,Java作为拥有反射这种上帝视角的语言当然不会止步于此,于是动态代理出现辣!动态代理可以在运行时生成Proxy类,码农(我)在编码时只需要实现一个通用的代理函数就好了。spring框架广泛运用了动态代理技术。

使用动态代理

Java的动态代理有jdk原生和cglib两种方式。

首先定义测试接口和实现如下:

1
2
3
4
5
6
7
8
interface SampleIntf{
void hello();
}
class SampleImpl implements SampleIntf {
public void bye(){
// do nothing
}
}

JDK动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SampleProxy implements InvocationHandler {
SampleIntf s;
public SampleProxy(SampleIntf s) {
this.s = s;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
// do some setup here
Object ret = method.invoke(s, objects);
// do some tearup here
return ret;
}
}

// create proxy
SampleIntf jdk_intf(){
return (SampleIntf) Proxy.newProxyInstance(Object.class.getClassLoader(), new Class[]{SampleIntf.class}, new SampleProxy(new SampleImpl()));
}

通过这种方式,创建了一个代理类,派生自所有设定的接口。

CGLIB动态代理

cglib可以用过两种方式生成动态代理,两种方式略有差异,留在后面再进行比较。

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
// method1
SampleIntf cglib_intf(){
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(Object.class.getClassLoader());
enhancer.setSuperclass(SampleImpl.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
return methodProxy.invokeSuper(obj, args);
});
return (SampleIntf) enhancer.create();
}

// method2
class MyInterceptor implements MethodInterceptor {
SampleIntf s;
public MyInterceptor(SampleIntf s){
this.s = s;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib proxying: " + method.getName());
return method.invoke(s, objects);
}
}
SampleIntf cglib_intf2(){
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(Object.class.getClassLoader());
enhancer.setSuperclass(SampleIntf.class);
enhancer.setCallback(new MyInterceptor(new SampleImpl()));
return (SampleIntf) enhancer.create();
}

cglib创建动态代理的方式1是直接使代理类派生自SampleImpl,所以也可以通过代理访问不属于SampleIntf接口的方法。
例如,在SampleImpl中添加方法:

1
2
3
4
5
6
class SampleImpl implements SampleIntf {
// ...
public void bye(){
// do nothing
}
}

然后进行调用:

1
2
SampleImpl impl = (SampleImpl) cglib_intf2();
impl.bye();

得到输出:

1
cglib proxying: bye

SampleImpl构造函数可能需要参数,如SampleImpl(int v)。创建动态代理时则需要提供参数(SampleIntf) enhancer.create(new Class[]{int.class}, new Object[]{1});

第二种方式则与jdk一致。enhancer.setSuperclass(SampleIntf.class);实际是内部调用this.setInterfaces(new Class[]{superclass});

可以看出,通过调用setSuperclass直接设置类的方式,达到代理任意类的效果。而jdk则只能代理接口。

我不是我?!

先考虑一下如下代码:

1
System.out.println(a.equals(a));

对于动态代理,这段代码输出应该是false。还是继续上代码:

1
2
3
4
5
6
SampleIntf jdk = jdk_intf();
SampleIntf cglib1 = cglib_intf();
SampleIntf cglib2 = cglib_intf2();
System.out.println(jdk.equals(jdk)); # false
System.out.println(cglib1.equals(cglib1)); # true
System.out.println(cglib2.equals(cglib2)); # false

为了看得更清楚,重写SampleImplequals方法:

1
2
3
4
5
@Override
public boolean equals(Object obj) {
System.out.println("impl equal: " + this.getClass().getName() + " - " + obj.getClass().getName());
return super.equals(obj);
}

输出如下:

1
2
3
4
5
6
impl equal: test.DPPerformance$SampleImpl - test.$Proxy0
false
impl equal: test.DPPerformance$SampleImpl$$EnhancerByCGLIB$$6e4640e6 - test.DPPerformance$SampleImpl$$EnhancerByCGLIB$$6e4640e6
true
impl equal: test.DPPerformance$SampleImpl - test.DPPerformance$SampleIntf$$EnhancerByCGLIB$$3c001bbd
false

那么来分析原因。首先,Object类的equals是直接比较对象,如果两个对象是同一引用,则返回true
这里用jdkcg1cg2impl标注涉及到的类,其中jdkjdk动态代理,内部调用implcg1cglib第一种动态代理,直接派生自SampleImplecg2cglib第二种动态代理,内部调用impl

  1. JDK

    调用jdk.equals(jdk)的流程应该是,equals进入SampleProxy拦截,转发至内部的impl,即impl.equals(jdk)impljdk是两个不同的类,故返回false

  2. CGLIB1

    调用cg1.equals(cg1)的流程则是,equals进入MethodInterceptor拦截,调用invokeSuper,也就是StorageImpl::equals。但是cg1派生自StorageImpl,故最终还是调用cg1.equals(c1),故返回true

  3. CGLIB2

    此流程与jdk完全相同。

这样的问题很容易出现在各种地方,例如:

1
2
3
4
LinkedList<SampleIntf> alist = new LinkedList<>();
alist.add(a);
alist.remove(a); // removed by some reason
alist.forEach(...); // still contains a!

目前我的解决办法是,在拦截器中转发equals调用到自己,再进行比较。以jdk代理为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SampleProxy implements InvocationHandler {
SampleIntf s;
public SampleProxy(SampleIntf s) {
this.s = s;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if(method.getName().equals("equals")) return equals(objects[0]);
return method.invoke(s, objects);
}

@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj instanceof SampleProxy) return s.equals(((SampleProxy) obj).s);
return obj.equals(this);
}
}

if(obj instanceof SampleProxy) return s.equals(((SampleProxy) obj).s);这里将对同一实现的不同代理类看作相等。

性能对比

测试代码

编写测试代码测试10w~1e量级下,不同类型代理的创建和访问性能。为了对比,加入原生SampleImpl

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
public static long benchmark(Runnable pro, int batch){
long start = System.currentTimeMillis();
for(int i = 0; i < batch; ++i) pro.run();
long end = System.currentTimeMillis();
return end - start;
}
public static long benchmark(SampleIntf intf, int batch){
return benchmark((Runnable) () -> intf.hello(), batch);
}
public static void test_foo_call() {
for(int b = 100000; b <= 100000000; b *= 10){
SampleIntf origin = origin_intf();
SampleIntf jdk = jdk_intf();
SampleIntf cg1 = cglib_intf();
SampleIntf cg2 = cglib_intf2();

System.out.println("batch: " + b);
System.out.printf("origin: %d\n", benchmark(origin, b));
System.out.printf("jdk : %d\n", benchmark(jdk, b));
System.out.printf("cg1 : %d\n", benchmark(cg1, b));
System.out.printf("cg2 : %d\n", benchmark(cg2, b));
}
}
public static void test_create() {
for(int b = 100000; b <= 100000000; b *= 10){
System.out.println("batch: " + b);
System.out.printf("origin: %d\n", benchmark((Runnable)()->{origin_intf();}, b));
System.out.printf("jdk : %d\n", benchmark((Runnable)()->{jdk_intf();}, b));
System.out.printf("cg1 : %d\n", benchmark((Runnable)()->{cglib_intf();}, b));
System.out.printf("cg2 : %d\n", benchmark((Runnable)()->{cglib_intf2();}, b));
}
}

测试结果

测试环境:

1
2
3
4
$ java --version
openjdk 15.0.2 2021-01-19
OpenJDK Runtime Environment (build 15.0.2+7)
OpenJDK 64-Bit Server VM (build 15.0.2+7, mixed mode)
函数调用
vol origin jdk cg1 cg2
10w 4 14 20 12
100w 1 18 11 25
1000w 12 141 108 194
1e 77 1067 763 1300
代理创建
vol origin jdk cg1 cg2
10w 4 32 80 56
100w 5 39 190 186
1000w 18 159 1508 1810
1e 388 1714 14482 18005

总结

  1. 就像我不是我一样,使用动态代理可能会出现一些有趣的问题。在实际编码过程中一定要注意,并仔细测试。
  2. 从性能来看,三种动态代理调用成本大差不离,但是相比直接调用还是差了几个数量级,这在加入复杂的代理逻辑后会更甚之。不过随着时代的发展,这部分性能损耗完全可以被硬件升级或良好的架构带来的提升所掩盖。并且,相比动态代理带来的这部分损耗,它提供的能力和价值更加重要。
  3. 测试结果的内容仅限于我的硬件环境。就结果来看,jdkcglib性能差异很小,1e量级时才有0.3s差异,大可忽略不计。虽然创建的消耗cglib远大于jdk,不过一般几乎不可能达到这样大的量级,也可以忽略不计。所以如果不是过分苛求性能,完全根据需要选择使用哪种就好了。