论坛首页 Java企业应用论坛

通过CGLIB实现AOP的浅析(顺便简单对比了一下JDK的动态代理)

浏览 5223 次
精华帖 (8) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-12-03   最后修改:2008-12-03
问了两个问题,还跑到论坛里回贴追着问,最终都得到的是结论性的贴子,没有得到我想要的分析。
功夫不负有心人,我终于弄明白了。

现象和问题请参照下面两个网页:
http://www.iteye.com/problems/7876
http://www.iteye.com/problems/7987

讨论的帖子:
http://www.iteye.com/topic/259458?page=2

先说一下结论
在类没有实现任何接口,并且没有默认构造函数的情况下,通过构造函数注入时,目前的Spring是无法实现AOP切面拦截的。
此时控制台会报异常的,具体异常如同http://www.iteye.com/problems/7876所记述。

错误摘录一点,便于搜索引擎直接搜索到。
 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in ServletContext resource [/WEB-INF/appcontext/UserAppContext.xml]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class cn.ipcat.service.UserService]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given   


原因分析

【前提】本文提到的没有实现接口,使用构造函数注入的类是Service层的类,并且注入的是Dao对象。

http://www.iteye.com/problems/7876这个问答里有朋友回答:加个默认函数试试。
我把Service类中加了默认构造函数,测试了一下,果然成功了,但是我就纳闷了,为什么加个默认构造函数就行了呢?如果是真的使用了这个默认构造函数生成类对象,那么起码Dao是根本就没有被注入到Service中的,而我在实际的测试中,读写数据库一切正常,显然意味着Dao对象是已经注入到Service对象里了。

按照Spring in Action书中所述:
如果目标对象没有实现任何接口,Spring使用CGLIB库生成目标对象的子类。在创建这个类的时候,Spring将通知织入,并且将对目标对象的调用委托给这个子类。

这段叙述不足以让大家理解到底Spring是如何借助于CGLIB来实现AOP拦截的,在Debug Spring代码过程中,我惊奇的发现,实际上,Spring确实借助于CGLIB生成了个子类,但这个子类只是一个壳,只是原来目标类对象的一个代表,当我们调用目标类的方法时,从表面上看,是被CGLIB生成的子类接受到了,调的是子类的方法,而实际上当子类对象的方法被调用时,又回调了目标类对象的方法。当然在回调时,可以在目标对象的方法被调用前后加点切面处理。

这样一看,CGLIB生成的目标对象的这个子类,还真是个名副其实的代理(只是代表着目标对象的样子,实际上一有处理就转交目标对象处理了),我们只要想办法搞出这个子类的对象就可以了,由于他是直接回调的目标对象,所以,即使我们所必须的Dao没有被注入到子类对象也是没有关系的。

然而,最大的不幸是在下面。
在AOP切进来之前,实际上目标类的对象已经被注满了东西,也被初始化完毕了,然后,才将AOP切进来。
在AOP切进来时,对于实现了接口的类,直接用了JDK的动态代理,把目标对象扔给JDK的Proxy,拿到代理对象就完事大吉了。
然而对于没有实现接口的类,就麻烦了,当然肯定的借助于CGLIB来实现代理。
不幸就在这里,当Spring在制造完了目标对象后,并没有将在生产目标对象进所用的注入构造函数的参数对象传给AOP处理这部分,也就是说当到了AOP这块处理时,目标对象是传过来了,但当时生产它时候所用的构造函数的参数对象并没有一起传过了。
作为使用构造函数注入的目的之一:保证注入属性的不可变性。自然,我们也不会给目标类再安上构造函数注入属性的set/get方法。

于是乎,当到了DefaultAopProxyFactory类下面的处理时,Spring根本无法将目标对象的构造函数参数对象传给Cglib2AopProxy对象。Cglib2AopProxy类的setConstructorArguments方法也只能是眼巴巴的看着异常的发生。因为到了CGLIB在制造代理对象时,ReflectUtils类的getConstructor方法根本就找不到默认的构造函数,于时异常最终发生了。

DefaultAopProxyFactory类中的内部CglibProxyFactory
	private static class CglibProxyFactory {

		public static AopProxy createCglibProxy(AdvisedSupport advisedSupport) {
			return new Cglib2AopProxy(advisedSupport);
		}
	}


ReflectUtils类的getConstructor方法
    public static Constructor getConstructor(Class type, Class[] parameterTypes) {
        try {
            Constructor constructor = type.getDeclaredConstructor(parameterTypes);
            constructor.setAccessible(true);
            return constructor;
        } catch (NoSuchMethodException e) {
            throw new CodeGenerationException(e);
        }
    }


Cglib2AopProxy类的setConstructorArguments方法
注:该方法,Spring的AOP处理中并没有使用。
	public void setConstructorArguments(Object[] constructorArgs, Class[] constructorArgTypes) {
		if (constructorArgs == null || constructorArgTypes == null) {
			throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' need to be specified");
		}
		if (constructorArgs.length != constructorArgTypes.length) {
			throw new IllegalArgumentException("Number of 'constructorArgs' (" + constructorArgs.length +
					") must match number of 'constructorArgTypes' (" + constructorArgTypes.length + ")");
		}
		this.constructorArgs = constructorArgs;
		this.constructorArgTypes = constructorArgTypes;
	}



Cglib2AopProxy类的getProxy方法片段
			// Generate the proxy class and create a proxy instance.
			Object proxy;
			if (this.constructorArgs != null) {
				proxy = enhancer.create(this.constructorArgTypes, this.constructorArgs);
			}
			else {
				proxy = enhancer.create();
			}


以上的分析就差不多结束了,恰恰是因为Spring通过CGLIB生成代理类对象时,并没有将目标对象的构造函数的参数及其类型进行设定,导致了CGLIB在生成代理类对象时,会使用默认的构造函数生成,结果目标对象类没有默认构造函数,CGLIB生成子类时,也没有加入默认构造函数,所以,异常的发生成为必然。

解决方案
有人说,我们都用接口就好了,为什么一定要不用接口呢。
正如CGLIB代理引入的初衷:遗留系统或无法实现接口的第三方类库同样可以得到通知。

以下的方案只是抛砖引玉,简单实现,如果您的条件更恶劣(情况更复杂),还需要进一步改造。

在上文我也提到,只要能生成出目标对象的子类对象,DAO注不注到子类对象里根据无关轻重,反而注入并不见得是好事。Spring借助目标对象的壳来实现代理功能,我们的目标也就是实现这个壳,让这个壳能顺利的生成出来。
明白了这个道理就好办多了。我们可以把org.springframework.aop.framework.DefaultAopProxyFactory类的源代码拷贝到我们的环境中,对其内部类CglibProxyFactory进行改造。
下面的代码中,我先获得目标对象的构造函数的类型,然后利于CGLIB生产一个这样的对象,通过c2aop.setConstructorArguments(new Object[] { enhancer.create() }, clazz);传给Cglib2AopProxy对象。这样,当CGLIB生成目标对象子类的对象时,就会正确的生产出来了。

注:我现在测试用的Service类的构造函数只有一个Dao参数。

改造前:
	private static class CglibProxyFactory {

		public static AopProxy createCglibProxy(AdvisedSupport advisedSupport) {
			return new Cglib2AopProxy(advisedSupport);
		}
	}

改造后:
	private static class CglibProxyFactory {

		public static AopProxy createCglibProxy(AdvisedSupport advisedSupport) {
			Cglib2AopProxy c2aop = new Cglib2AopProxy(advisedSupport);
			
			Object obj;
			try {
				obj = advisedSupport.getTargetSource().getTarget();
				if (null == obj) {
					throw new Exception("错误:找不到目标对象!");
				}
			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException(e);
			}

			Constructor[] cstructs = obj.getClass().getDeclaredConstructors();
			if (cstructs.length == 1) {
				Constructor cstruct = cstructs[0];
				Class[] clazz = cstruct.getParameterTypes();

				if (clazz.length == 1) {
					Enhancer enhancer = new Enhancer();
					enhancer.setSuperclass(clazz[0]);
					enhancer.setCallback(new MethodInterceptorImpl());

					c2aop.setConstructorArguments(new Object[] { enhancer.create() }, clazz);
				}
			}

			return c2aop;
		}
	}

	private static class MethodInterceptorImpl implements MethodInterceptor {
		public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
				throws Throwable {
			proxy.invokeSuper(obj, args);
			return null;
		}
	}


再说一点,实际上,如果能从CGLIB入手,在生产目标对象类的子类时,加上一个默认构造函数就帅了。
但是对CGLIB进行改造,难度不是一半点,所以,还是直接改Spring的类来的直接些。

结稿!


   发表时间:2008-12-03  
动态代理是实现接口,在委托前后实现aop,cglib是继承父类,应该是通过asm将代码直接写到字节里了。动态代理用到了反射,实时性要求高的地方用不得。
0 请登录后投票
   发表时间:2008-12-03  
m8x6bmxb 写道
动态代理是实现接口,在委托前后实现aop,cglib是继承父类,应该是通过asm将代码直接写到字节里了。动态代理用到了反射,实时性要求高的地方用不得。

谢谢回复。
概括的讲,我也认为是这样。
0 请登录后投票
   发表时间:2008-12-04  
是自己手动做了一个setConstructorArguments,我也奇怪为什么Spring不这么做,那这个setConstructorArguments不是白写了吗?
0 请登录后投票
   发表时间:2008-12-04  
jiwenke 写道
是自己手动做了一个setConstructorArguments,我也奇怪为什么Spring不这么做,那这个setConstructorArguments不是白写了吗?

是啊,Spring本身在这个处理确实太不爽了。

我在Debug过程发现以目前Spring的框架,想改成用正常做法传递构造函数参数来实现CGLIB代理是很难做到的,因为生成完了目标对象后,Spring就把构造函数参数扔掉了(估计原来Spring只想着用JDK实现动态代理,要这个参数没什么用);如果想加上去,Spring架构本身改动量可能会非常大。

至于setConstructorArguments这个方法,可能对于想直接借且于CGLIB思想实现AOP控制的人还是有一定意义的,毕竟可以直接利用Cglib2AopProxy进行AOP控制。

另外,可能有些同志会说,我们给没有实现接口的类直接加个默认构造方法就好了,何必改框架的类呢,是啊,不过每个类都加个默认构造方法,也怪别扭的。
0 请登录后投票
   发表时间:2008-12-04  
netfork 写道
另外,可能有些同志会说,我们给没有实现接口的类直接加个默认构造方法就好了,何必改框架的类呢,是啊,不过每个类都加个默认构造方法,也怪别扭的。

也许这个方法的确是一个方法,这样目标类就有两个构造函数,一个无参,是为了CGLIB生成子类代理用的,另一个有参,是用来做注入的。这样是不是比较简单一些,不用动Spring的源代码。

0 请登录后投票
   发表时间:2008-12-04  
jiwenke 写道
netfork 写道
另外,可能有些同志会说,我们给没有实现接口的类直接加个默认构造方法就好了,何必改框架的类呢,是啊,不过每个类都加个默认构造方法,也怪别扭的。

也许这个方法的确是一个方法,这样目标类就有两个构造函数,一个无参,是为了CGLIB生成子类代理用的,另一个有参,是用来做注入的。这样是不是比较简单一些,不用动Spring的源代码。


嗯,这样确实简单,不过如果是新开发系统,还是有以下建议:
1、如果一定要用不实现接口的类。
  建议尽量用Set注入,尽可能不用构造方法注入。
2、最好还是定义个接口,然后实现这个接口。
  这样不管是什么注入,都没问题了。

我想只是为了实现Spring下的aop,不值得非定义一个对业务没有任务意义的默认构造函数。但这也算个办法吧。

不过,话又说回来,有时候需要把老系统中的业务处理纳入spring及其aop控制时,万一当时做的业务类没定义默认构造函数,这个时候,需要改Spring的源码可能性是会有的吧。

之前对一个运行多年的很庞大的线上系统追加新功能时,客户的编程规约里有一句大约说的是需要给业务类定义一个默认的构造函数,当时觉得很奇怪,一直没弄明白为什么来了这么一句。现在想想,还真有需要的时候。



0 请登录后投票
   发表时间:2008-12-05  
引用
之前对一个运行多年的很庞大的线上系统追加新功能时,客户的编程规约里有一句大约说的是需要给业务类定义一个默认的构造函数,当时觉得很奇怪,一直没弄明白为什么来了这么一句。现在想想,还真有需要的时候。

同意,把它认为是一个编程规范。就像使用get/set那种一样,要求需要有默认的类构造函数。
0 请登录后投票
   发表时间:2008-12-05  
m8x6bmxb 写道
动态代理是实现接口,在委托前后实现aop,cglib是继承父类,应该是通过asm将代码直接写到字节里了。动态代理用到了反射,实时性要求高的地方用不得。

是这样,动态代理效率很低
0 请登录后投票
   发表时间:2009-01-11  
楼上几位分析的实在是太深入了,小弟看的一头雾水。能不能简要说一下,spring aop的实现机制。不胜感激!!!
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics