粗看JDK动态代理与CGLIB的设计方式

    去年这个时候,因为项目需要,编写了一些Annotation。每一个使用这些注解的类,都是通过CGLIB动态生成增强类(或者说是代理类),而注解的处理逻辑,则是在增强类的回调函数处实现的。

     另一方面,JDK从1.5开始也提供了动态代理功能。

     翻阅网上对此2者的比较,看到最多的是以下这个结论

    JDK动态代理只支持拥有接口的类。而CGLIB功能更强大,可代理无接口的类。CGLIB通过动态生成子类字节码的方式进行实现代理。

    问题当然不止于此。我想知道的是JAVA动态代理的实现方式是什么? 既然是通过动态生成字节码的方式,则当代理接口或者被代理类不变的情况下,是否会重复构造(毕竟这开销很大)?   

    一、代理的实现

首先看第一个问题,CGLIB的是动态生成子类的方式,并且不需要被代理的类实现代理接口,而JAVA必须需要接口。于是你第一个反应是什么?

熟悉代理设计模式的人都知道,实现静态代理有两种实现方式,一是通过继承,二是通过组合。CGLIB类似于继承,而JDK动态代理会不会是组合? 

     组合:组合是标准的代理模式,类图如下

       
粗看JDK动态代理与CGLIB的设计方式
 

可以看见,组合方式必须要依赖一个共同的接口实现。 

继承: 继承严格来说都不算是代理模式,就是通过继承父类,在子类中覆写父类的方法进行前置或后置的操作。

 本来继承的应用非常受限,但如果通过CGLIB动态生成字节码,那可算是咸鱼翻身了。

   

     再来看下JDK动态代理的调用方式。

        

         InvocationHandler testHandler = new TestHandler(this.adaptee)             
          //生成代理类的字节码加载器   
          ClassLoader classLoader = proxyObject.getClass().getClassLoader();  
          //需要代理的接口,被代理类实现的多个接口都必须在这里定义   
          Class<?>[] proxyInterface = proxyObject.getClass().getInterfaces();             
          //生成代理类     
         return Proxy.newProxyInstance(classLoader,proxyInterface, testHandler);  

   

 handlerinvoke方法中,无法获得运行状态下的被代理对象(adaptee)的实例。handler的invoke的接口定义如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

   是不是本能反应便是将proxy当做被代理对象进行方法调用? 那你就陷入死循环了,因为proxy是增强后的对象。。。   完全搞不懂JDK为毛这么设计。不过既然如此,那只能在建立handler实例的时候,将被代理对象保存在hanler中了。即InvocationHandler testHandler = new TestHandler(this.adaptee) 

 

     言归正传,上述代码块中的调用方式是不是很像标准的代理模式?不过这和标准的代理模式还是有些区别的,除了要把被代理对象用组合的方式传入,还需要把代理操作(即回调函数)也传入。不过这也是废话,因为这是动态代理啊~

     猜测归猜测,还是需要看一下JDK动态代理的具体实现.

/**
 * loader:类加载器
 * interfaces:目标对象实现的接口
 * h:InvocationHandler的实现类
 */
public static Object newProxyInstance(ClassLoader loader,
					  Class<?>[] interfaces,
					  InvocationHandler h)
	throws IllegalArgumentException
    {
	if (h == null) {
	    throw new NullPointerException();
	}

	/*
	 * Look up or generate the designated proxy class.
	 */
	Class cl = getProxyClass(loader, interfaces);

	/*
	 * Invoke its constructor with the designated invocation handler.
	 */
	try {
            // 调用代理对象的构造方法(也就是$Proxy0(InvocationHandler h))
	    Constructor cons = cl.getConstructor(constructorParams);
            // 生成代理类的实例并把MyInvocationHandler的实例传给它的构造方法
	    return (Object) cons.newInstance(new Object[] { h });
	} catch (NoSuchMethodException e) {
	    throw new InternalError(e.toString());
	} catch (IllegalAccessException e) {
	    throw new InternalError(e.toString());
	} catch (InstantiationException e) {
	    throw new InternalError(e.toString());
	} catch (InvocationTargetException e) {
	    throw new InternalError(e.toString());
	}
    }

 

    以上18行可见,对于固定的代理接口,获得的代理对象的Class总是固定的。再到24行,具体的代理操作和被代理对象是动态传入的。也就是说,若代理接口不变,则传入不同的

 

Handler并不会产生新的代理类的Class对象。这证实了之前的猜测。

       

二、效率问题

      JDK动态代理:

     由上文中Class cl = getProxyClass(loader, interfaces);可知,对于固定的classloader和接口,其生成的Class对象内容应该是固定的(其实阅读该方法的源码,可知把目标类实现的接口名称作为缓存(Map)中的key 。是的,显然JDK没那么蠢~            

    用Jprofile测试了下,当每次new不同的InvocationHandler无限循环时, JVM中的方法区的使用空间并没有波动。测试代码如下:

    

    public static void main(String args[]) throws InterruptedException {
		for(long i=0;i<9999999;i++) {
			System.out.println(i);
			AdapteeInterface proxy=(AdapteeInterface) Proxy.newProxyInstance(Adaptee.class.getClassLoader(),
					Adaptee.class.getInterfaces(), new InvocationHandler() {
						
						@Override
						public Object invoke(Object proxy, Method method, Object[] args)
								throws Throwable {
							return null;
						}
					});
		}
}

  

 

CGLIB代理:

      CGLIB中可以通过AbstractClassGenerator.setUseCache(boolean useCache)设置是否使用代理对象的缓存。经过测试,当setUseCache(false)时,方法区很快发生了内存溢出。

而设置为true时,方法区内存使用率并没有明显增加。测试代码如下:      

public static void main(String[] args) {
		for(long i=0;;i++){
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				@Override
				public Object intercept(Object obj, Method method,
						Object[] args, MethodProxy proxy) throws Throwable {
					return null;
				}
			});
			OOMObject proxy = (OOMObject) enhancer.create();
			map.put(i,proxy );
		}
	}

 

 

     对于JDK动态代理而言,相同代理接口的代理类的字节码,始终不会重复生成。而cglib通过参数来控制是否重复构建代理的CLASS

     关于效率问题,额外提一句。当不考虑缓存的作用时,JDK由于只是新建一个接口对象,而CGLIB需要完全继承父类并增加N多字节码内容用以增强特性。因此新建一个JDK代理对象比CGLIB快一些(但还是在一个数量级上)。从执行时间来看大概是1:5。但是执行效率的差距明显大得多,由于JDK动态代理在回调handler中,只能通过反射的方式调用原方法,而CGLIB由于继承的关系,可以通过MethodProxy.invokeSuper(Object arg0, Object[] arg1)直接调用父类方法。  具体的性能测试,网上还是挺多的。

 

 

你可能感兴趣的:(设计模式,jvm,jdk)