代理模式(Proxy
)是设计模式中结构型模式的一种,用以实现对目标访问的控制。
结构如下:
当目标接口需要额外的操作才能访问,或是想要对目标访问进行控制时,都可以使用代理模式。
如图,RealSubject
和Proxy
派生自接口Subject
,RealSubject
是Subject
的一个真实的实现,Proxy
的对应接口是实际是对RealSubject
接口的一个包装,可以在进行RealSubject
接口调用前后执行额外操作,如进行设置环境、权限控制、资源控制等,甚至是修改结果。
从这点上来讲,代理模式、装饰器模式(Decorator
, Wrapper
)和适配器模式(Adapter
)非常相似,不过这里就不做比较了。
代理模式可以帮助我们解决很多问题。不过,每有一个接口需要进行代理,就需要写一个XXProxy
类,然后实现XXFoo
方法。在项目中可能会有非常多的接口需要代理,而他们的代理的逻辑可能是一致的,那么就需要做很多大量重复工作了。当然我们也可以编写脚本或者工具自动生成代码,比如ice
、grpc
这些的rpc
框架就是这么干的。
但是,Java
作为拥有反射这种上帝视角的语言当然不会止步于此,于是动态代理出现辣!动态代理可以在运行时生成Proxy类,码农(我)在编码时只需要实现一个通用的代理函数就好了。spring
框架广泛运用了动态代理技术。
Java
的动态代理有jdk
原生和cglib
两种方式。
首先定义测试接口和实现如下:
interface SampleIntf{
void hello();
}
class SampleImpl implements SampleIntf {
public void bye(){
// do nothing
}
}
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
可以用过两种方式生成动态代理,两种方式略有差异,留在后面再进行比较。
// 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
,不过一般几乎不可能达到这样大的量级,也可以忽略不计。所以如果不是过分苛求性能,完全根据需要选择使用哪种就好了。原文:https://www.cnblogs.com/o--o/p/14769347.html