Java中的动态代理
代理模式
代理模式(Proxy
)是设计模式中结构型模式的一种,用以实现对目标访问的控制。
结构如下:
当目标接口需要额外的操作才能访问,或是想要对目标访问进行控制时,都可以使用代理模式。
如图,RealSubject
和Proxy
派生自接口Subject
,RealSubject
是Subject
的一个真实的实现,Proxy
的对应接口是实际是对RealSubject
接口的一个包装,可以在进行RealSubject
接口调用前后执行额外操作,如进行设置环境、权限控制、资源控制等,甚至是修改结果。
从这点上来讲,代理模式、装饰器模式(Decorator
, Wrapper
)和适配器模式(Adapter
)非常相似,不过这里就不做比较了。
Java的动态代理
代理模式可以帮助我们解决很多问题。不过,每有一个接口需要进行代理,就需要写一个XXProxy
类,然后实现XXFoo
方法。在项目中可能会有非常多的接口需要代理,而他们的代理的逻辑可能是一致的,那么就需要做很多大量重复工作了。当然我们也可以编写脚本或者工具自动生成代码,比如ice
、grpc
这些的rpc
框架就是这么干的。
但是,Java
作为拥有反射这种上帝视角的语言当然不会止步于此,于是动态代理出现辣!动态代理可以在运行时生成Proxy类,码农(我)在编码时只需要实现一个通用的代理函数就好了。spring
框架广泛运用了动态代理技术。
使用动态代理
Java
的动态代理有jdk
原生和cglib
两种方式。
首先定义测试接口和实现如下:
interface SampleIntf{
void hello();
}
class SampleImpl implements SampleIntf {
public void bye(){
// do nothing
}
}
JDK动态代理
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
可以用过两种方式生成动态代理,两种方式略有差异,留在后面再进行比较。
// 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
中添加方法:
class SampleImpl implements SampleIntf {
// ...
public void bye(){
// do nothing
}
}
然后进行调用:
SampleImpl impl = (SampleImpl) cglib_intf2();
impl.bye();
得到输出:
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
则只能代理接口。
我不是我?!
先考虑一下如下代码:
System.out.println(a.equals(a));
对于动态代理,这段代码输出应该是false
。还是继续上代码:
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
为了看得更清楚,重写SampleImpl
的equals
方法:
@Override
public boolean equals(Object obj) {
System.out.println("impl equal: " + this.getClass().getName() + " - " + obj.getClass().getName());
return super.equals(obj);
}
输出如下:
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
。
这里用jdk
、cg1
、cg2
和impl
标注涉及到的类,其中jdk
为jdk
动态代理,内部调用impl
;cg1
为cglib
第一种动态代理,直接派生自SampleImple
;cg2
为cglib
第二种动态代理,内部调用impl
。
-
JDK
调用
jdk.equals(jdk)
的流程应该是,equals
进入SampleProxy
拦截,转发至内部的impl
,即impl.equals(jdk)
。impl
和jdk
是两个不同的类,故返回false
。 -
CGLIB1
调用
cg1.equals(cg1)
的流程则是,equals
进入MethodInterceptor
拦截,调用invokeSuper
,也就是StorageImpl::equals
。但是cg1
派生自StorageImpl
,故最终还是调用cg1.equals(c1)
,故返回true
。 -
CGLIB2
此流程与
jdk
完全相同。
这样的问题很容易出现在各种地方,例如:
LinkedList<SampleIntf> alist = new LinkedList<>();
alist.add(a);
alist.remove(a); // removed by some reason
alist.forEach(...); // still contains a!
目前我的解决办法是,在拦截器中转发equals
调用到自己,再进行比较。以jdk
代理为例:
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
。
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));
}
}
测试结果
测试环境:
$ 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 |
总结
- 就像我不是我一样,使用动态代理可能会出现一些有趣的问题。在实际编码过程中一定要注意,并仔细测试。
- 从性能来看,三种动态代理调用成本大差不离,但是相比直接调用还是差了几个数量级,这在加入复杂的代理逻辑后会更甚之。不过随着时代的发展,这部分性能损耗完全可以被硬件升级或良好的架构带来的提升所掩盖。并且,相比动态代理带来的这部分损耗,它提供的能力和价值更加重要。
- 测试结果的内容仅限于我的硬件环境。就结果来看,
jdk
和cglib
性能差异很小,1e
量级时才有0.3s
差异,大可忽略不计。虽然创建的消耗cglib
远大于jdk
,不过一般几乎不可能达到这样大的量级,也可以忽略不计。所以如果不是过分苛求性能,完全根据需要选择使用哪种就好了。