代理模式 代理模式(Proxy)是设计模式中结构型模式的一种,用以实现对目标访问的控制。
结构如下:
 
当目标接口需要额外的操作才能访问,或是想要对目标访问进行控制时,都可以使用代理模式。 如图,RealSubject和Proxy派生自接口Subject,RealSubject是Subject的一个真实的实现,Proxy的对应接口是实际是对RealSubject接口的一个包装,可以在进行RealSubject接口调用前后执行额外操作,如进行设置环境、权限控制、资源控制等,甚至是修改结果。
 
从这点上来讲,代理模式、装饰器模式(Decorator, Wrapper)和适配器模式(Adapter)非常相似,不过这里就不做比较了。
Java的动态代理 代理模式可以帮助我们解决很多问题。不过,每有一个接口需要进行代理,就需要写一个XXProxy类,然后实现XXFoo方法。在项目中可能会有非常多的接口需要代理,而他们的代理的逻辑可能是一致的,那么就需要做很多大量重复工作了。当然我们也可以编写脚本或者工具自动生成代码,比如ice、grpc这些的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 ()  {              } } 
 
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  {                  Object ret =  method.invoke(s, objects);                  return  ret;     } } 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 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(); } 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 ()  {              } } 
 
然后进行调用:
1 2 SampleImpl impl = (SampleImpl) cglib_intf2(); impl.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  
 
为了看得更清楚,重写SampleImpl的equals方法:
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。 这里用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完全相同。
 
 
这样的问题很容易出现在各种地方,例如:
1 2 3 4 LinkedList<SampleIntf> alist = new  LinkedList<>(); alist.add(a); alist.remove(a);  alist.forEach(...);  
 
目前我的解决办法是,在拦截器中转发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 
 
总结 
就像我不是我 一样,使用动态代理可能会出现一些有趣的问题。在实际编码过程中一定要注意,并仔细测试。 
从性能来看,三种动态代理调用成本大差不离,但是相比直接调用还是差了几个数量级,这在加入复杂的代理逻辑后会更甚之。不过随着时代的发展,这部分性能损耗完全可以被硬件升级或良好的架构带来的提升所掩盖。并且,相比动态代理带来的这部分损耗,它提供的能力和价值更加重要。 
测试结果 的内容仅限于我的硬件环境。就结果来看,jdk和cglib性能差异很小,1e量级时才有0.3s差异,大可忽略不计。虽然创建的消耗cglib远大于jdk,不过一般几乎不可能达到这样大的量级,也可以忽略不计。所以如果不是过分苛求性能,完全根据需要选择使用哪种就好了。