论坛首页 Java企业应用论坛

Domain injection with AOP

浏览 31786 次
该帖已经被评为精华帖
作者 正文
   发表时间:2005-08-30  
几个月前在JavaEye上讨论得如火如荼的domain object问题似乎已经硝烟散尽。在那个经典的贴子 里,robbin为domain object总结了三种模型,其中的模型二好象完美地解决了domain object的所有疑问。但现实的情况却并不象理想中的那么简单,在贴子的末尾七彩狼、frankensteinlin等都提出了相关的疑问。虽然我们的domain object里只包含业务逻辑,我们用了O/R Mapping,现实中的复杂逻辑仍然需要我们去调用DAO或者其它的辅助类(更何况我们的企业级应用很多都不使用O/R Mapping工具),具体的用例我就不再详细地列出。
这里真正的问题是:由于domain object的生命周期的原因无法被IOC容器所管理,它无法享受其它bean在IOC容器的特权(其中最重要的当然是它无法将IOC容器里的定义interface注入到domain object里),因此domain object的威力大大受到了限制。本文采用AOP的方法,将容器中定义的接口无缝地inject到domain object里。由于业界似乎还没有类似的实现或想法,因此xiecc取了一个新名词,称之为domain injection。[/url]
   发表时间:2005-08-30  
一、问题的提出
domain object的生命周期是与容器中的对象不同的,它也许是由O/R Mapping工具通过查询得到,也许是在程序中被new出来。而容器中的对象则是在程序初始化的时候由于IOC容器里一次性生成的,典型的Web应用则是利用Servlet容器在调用Servlet Listner时生成好的(也许我们可以用Spring的prototype,但prototype的生命周期与Domain model仍然是不同的)。
不管是哪种情况,由于我们都没有办法控制生成domain object时的配置,我们失去了向domain object注入接口的机会。如图所示:
0 请登录后投票
   发表时间:2005-08-30  
二、两种有问题的解决方案
虽然我们无法让容器中将interface注入到domain object中,我们仍然还是有机会利用手工的方式注入domain object的。我们先来看一下两种有问题的解决方案:
1、让domain object知道ApplicationContext
这种解决方案还算简单,假如我们在一个Domain Object里使用一个DAO,我们甚至不需要定义一个DAO的instance,我们直接采用(现实中也许会用一个工厂包装一下,但没有本质区别):
private LcyjDAO getLcyjDAO() {
return (LcyjDAO)getApplicationContext().getBean("lcyjDAO");
}
这种解决方案有两个致命的问题:
(1) 我们的domain object必须要知道ApplicationContext,这与Spring里出的non-invasive完全不符合。
(2) 我们必须手工地new出一个ApplicationContext,在Web应用里会导致系统里要load两次ApplicationContext,使整个对象的Singleton都失效了,因为IOC容器中的singleton是在ApplicationContext范围的。
发现上面这个论断是错的,我们可以不用再new一个applicationContext的,请看 http://www.digizenstudio.com/blog/2005/06/06/global-references-to-applicationcontexts-unintrusively/

2、使用人工的injecting
这意味着我们在第一次获得对象的时候,直接将接口手工注入到Domain object中,典型的例子如下:
Disease disease=new Disease();
disease.setDiseaseDao(getDiseaseDao());
这种方法避免了上一种方法生成两个ApplicationContext的尴尬,也让Domain Object无需知道ApplicationContext。但是却会带来另外的问题:
(1)手工的注入方式不但工作量大,而且出现bug的机率也大大增加了。
工作量大是显然的,我上面的演示代码里只用了一个对象,注入了一个接口,因此相对还比较简单。但现实却不是这样的,假如我是从hibernate里查出几百个对象,我必须通过一个循环对每个对象都进行注入。而注入的接口也会不止一个,这也增加好多的代码量。
而bug的增加也是显然的,我创建好一个对象后,我必须要清楚地知道这个对象需要注入哪些接口。而维护起来更是麻烦,假如在某个domain object里增加了某个接口,就意味着我必须要找到所以注入这个domain object的代码,往里增加一行注入代码。

(2)创建Domain Object里要调用的接口必须要在创建它的那个类里也有。
比如我在DiseaseService类里创建了Disease对象,但DiseaseService里是不需要DiseaseDao接口的,但是仅仅为了将DiseaseDao注入到Disease对象中,我必须要在DiseaseService增加对DiseaseDao的定义。这样做也是非常的invasive的。
0 请登录后投票
   发表时间:2005-08-30  
三、利用AOP来解决无缝注入
虽然上面的方法2是一种很滥的方法,但是却给我们很大的启发,我们可以通过手工的方法将接口注入到domain object,但是由于这种方法的注入分散在不同类的里,使得开发和维护都很不方便。方法2最大的问题是:注入的关注点分散。

仔细想一下,“注入的关注点分散”这句话让我们想起了什么?对,是AOP,AOP的作用不就把分散的关注点集到一个aspect里吗?我们只要将这种domain object的注入逻辑集中在一个aspect里,我们代码量和可读性不都会变得很好了?而且除了这个aspect之外,所有的其它类都不需要知道domain object的接口是怎么注入的,这不是完全地实现我们的non-invasive的目的吗?如图所示:
0 请登录后投票
   发表时间:2005-08-30  
下面我用aspectJ实现的对domain object的自动注入。(Why not Spring AOP? Spring AOP的实现机制是dynamics proxy,这意味着返回的domain object是一个新创建代理对象。但是由于我们无法控制domain object的生命周期,如我们直接从hibernate里查出几百个对象,没有容器的支持我们根本没有机会将这些对象转化代理对象)。

1、 让aspect可以访问IOC容器

Spring对aspectJ提供了很好的支持,这意味着spring里配置好的Aspect可以直接访问容器的对象,以下是Spring里对aspectJ的配置:
<bean id="injectionAspect" class="edu.zju.tcm.aspects.InjectionAspect" factory-method="aspectOf"/>

然后在我们定义的aspect里implements BeanFactoryAware,如下所示:
public aspect InjectionAspect implements BeanFactoryAware{	
	BeanFactory beanFactory;	
	public void setBeanFactory(BeanFactory beanFactory); throws BeansException{
		this.beanFactory=beanFactory;
	}
}

这样我们的aspect就具备了访问Spring的BeanFactory的能力。
2、 定义pointcut
定义Domain Object的pointcut是比较难的事,因为我们无法控制domain object的生命周期,因此我们无法将pointcut定义在对象创建的时候。domain object除了Object外没有共同的父类,每个domain object又没有共同的方法,确实是一件头痛的事。
因此我做了一个唯一假设,这也是我们在使用aspect之后,我们正常代码唯一要遵从的事(如用了annotation就什么都不用了,但我觉得annotation反而增加了工作量):我们的Dao类或Service类有一个统一的后缀名,然后我们的domain object调用这些类时用的是get方法而不是instance变量。如:
private LcyjDAO lcyjDAO;
private LcyjDAO getLcyjDAO(); {
	return lcyjDAO;
}
public void setLcyjDAO(LcyjDAO lcyjDAO); {
	this.lcyjDAO = lcyjDAO;
}

我们以后在domain object里调用这个类时用getLcyjDAO(),而不是lcyjDAO。
事实这是很好的编程习惯,Rod Johnson在expert one on on J2EE design and development里甚至说任何时候都用前一种方法。而且这是完全non-invasive的。
这样我们的pointcut就很好定义了,我们要在Domain Object第一次使用Dao或Service时进行注入(这其实是一种lazy load,假如我们的domain object没有调用相关业务逻辑时系统甚至不会去inject,这对系统的性能也有帮助):
pointcut domainGet() : execution (* edu.zju.tcm.domain..*.get*DAO())||execution (* edu.zju.tcm.domain..*.get*Service());
以上将domain包里所有的类里有方法的开头是get末尾是DAO或Service的类都定义为pointcut。

3、 实现Advice
前面都讲清楚之后,Advice的实现倒反而很轻而易举了,以下是它的代码:
 Object around(Object domainObj); : domainGet(); && this(domainObj);{
   	  Object returnObj=proceed(domainObj);;
      if (null!=returnObj);{
      	return returnObj;
      }
      BeanWrapper beanWrapper=new BeanWrapperImpl();;
      beanWrapper.setWrappedInstance(domainObj);;
      MutablePropertyValues pvs=new MutablePropertyValues();;
      Field[] fields=domainObj.getClass();.getDeclaredFields();;
      for (int i=0;i<fields.length;i++);{
          String fieldName=fields[i].getName();;
          if (isDaoClass(fieldName); || isServiceClass(fieldName););{          	
            PropertyValue pv=new PropertyValue(fieldName,getBeanFactory();.getBean(fieldName););;
            pvs.addPropertyValue(pv);; 
          }
      }            
      beanWrapper.setPropertyValues(pvs);;
      return proceed(domainObj);;
   }

首先我们判断一下这个domain object有没有被注入过,也就是get回来的结果是否为空,如果不为空就不用inject啦。接下来用反射机制和Spring的BeanWrapper将BeanFactory里读出来的interface设置成这个domainObject的属性,注射完成之后返回就行了。
Domain object里定义的field名字必须与Spring配置文件里的bean名字一致。这根本不是限制条件,而是每个用Spring的人都必须遵从的习惯。
代码是如果出奇的简单,简直令xiecc都不敢相信,但是无论如何,它成功了啦!
0 请登录后投票
   发表时间:2005-08-30  
四、遗留问题

1、事务处理
用了aspectJ之后,在domain object里实现透明的事务处理也会变得非常容易。但是xiecc没有实现这一部分,因为xiecc仍然认为在domain object是不应该有存储逻辑的,更不要说参与事务处理了。但是也许有人真的想那么干,毕竟在Account里写个deposit之类的方法也不算错。而且在Martin Fowler的世界里似乎是不需要Service层的(它的Patterns of enterprise application Architecture里的Service Pattern那一章是请人写的,因为他自己是不用的)。

2、AspectJ的编译速度
以前我只在玩具项目里用过AspectJ,感觉好爽。这次把它加到我去年做的一个项目里,其实也只有9000多行代码,结果速度慢得要死,而且很容易out of memory。我每次保存一个aspect系统都要停半天。也许采用编译时织入的方法对开发效率还是有影响的,也许与AspectWerkz合并的Aspect5里会有更好的方法。

Welcome to my blog:  http://blog.itpub.net/xiecc
0 请登录后投票
   发表时间:2005-08-30  
xiecc 写道
由于业界似乎还没有类似的实现或想法,因此xiecc取了一个新名词,称之为domain injection。

check out
http://fisheye.cenqua.com/viewrep/~raw,r=1.5/springframework/spring/sandbox/src/org/springframework/orm/hibernate/support/DependencyInjectionInterceptorFactoryBean.java
http://fisheye.cenqua.com/viewrep/~raw,r=1.3/springframework/spring/sandbox/test/org/springframework/orm/hibernate/support/DependencyInjectionInterceptorFactoryBeanTests.java

可以去spring的论坛上搜索DependencyInjectionInterceptorFactoryBean,应该可以找到相关的讨论。
0 请登录后投票
   发表时间:2005-08-30  
good. 多谢楼上, 学习中。
不过Rod Johnson的interceptor只能解决hibernate的问题,我用AOP来解决则是通用的解决方法。
0 请登录后投票
   发表时间:2005-08-30  
刚去Spring论坛翻了一下,还真有人跟我的想法类似的:

http://forum.springframework.org/viewtopic.php?t=301&postdays=0&postorder=asc&highlight=dependencyinjectioninterceptorfactorybean&start=15

And yet there is a better solution in my opnion 

Take a look at AspectJ and Spring integration. It is in its infancy but can be pretty powerfull already.

Basically, the problem you are having is if your object graph is pretty big you would not want to let Spring create proxies for it (performance issues), on the other hand you would need the business objects to have dependencies injected somehow.

So the most optimal solution to the problem would be, if you somehow could use the dependent objects inside your business objects (that would be created by Hibernate), but inject dependencies into them say at construction time. This is where AspectJ and Spring integration comes in very handy. Basically you pre-compile your business objects with AspectJ and let Spring inject deps to the aspect itself. At construction time AspectJ's aspect would wire those dependents to your non-proxied BOs. It is pretty neat I must say. Read chapter 6 of the new Spring (1.1) docs for more info on this.

There are of course disadvantages:

1. AspectJ's aspects are not dynamic, so if you have multiple deployments of the BOs you would need to pre-compile for each deployment.

2. AFIKT there is a somewhat signifact problem where perthis, pertarget and percfolw aspects of AspectJ are used.
0 请登录后投票
   发表时间:2005-08-30  
:idea: 给出了一个详尽的使用aspectj对domain object做inject和事务支持的方法.

印象中Rod Johnson在以前的一些讨论重也提到了使用aspectj来做这种事,但没看到给出象你这么详尽的说明.

建议提交到spring的jira...

至于想法,在很早以前,就尝试使用动态增强来做这样的事,但后来也考虑用aspectj,,不知道啥时候java把oap增加进去.

1.假如按照让接口浮现的构建方式来搭建系统,什么情况会发展出service层?发展出service层后domain object的什么方法就需要迁移到service层吗?service层的存在需求是什么?假如从service提供较粗粒度的封装角度看,更多时候service只是对domain object的包装.
2.在非面向提供框架级的系统中,使用xx.getBean("")有何不可,(当然inject方式做测试是很方便,还有引入了一些字符,代码也多了,但这不是重点.)
0 请登录后投票
论坛首页 Java企业应用版

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