AOP介绍

 AOP(Aspect Oroented Programming,面向切面编程)是消除代码重复的一种方法。

      AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向方面编程。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。

举例:假设有在一个应用系统中,有一个共享的数据必须被并发同时访问,首先,将这个数据封装在数据对象中,称为Data Class,同时,将有多个访问类,专门用于在同一时刻访问这同一个数据对象。

为了完成上述并发访问同一资源的功能,需要引入锁Lock的概念,也就是说,某个时刻,当有一个访问类访问这个数据对象时,这个数据对象必须上锁Locked,用完后就立即解锁unLocked,再供其它访问类访问。

使用传统的编程习惯,我们会创建一个抽象类,所有的访问类继承这个抽象父类,如下:

java 代码
 
  1. abstract class Worker{  
  2.   abstract void locked();  
  3.   abstract void accessDataObject();  
  4.   abstract void unlocked();  
  5. }  
AOP的缺点
 
      accessDataObject()方法需要有“锁”状态之类的相关代码。

      Java只提供了单继承,因此具体访问类只能继承这个父类,如果具体访问类还要继承其它父类,比如另外一个如Worker的父类,将无法方便实现。
重用被打折扣,具体访问类因为也包含“锁”状态之类的相关代码,只能被重用在相关有“锁”的场合,重用范围很窄。

      仔细研究这个应用的“锁”,它其实有下列特性:
      “锁”功能不是具体访问类的首要或主要功能,访问类主要功能是访问数据对象,例如读取数据或更改动作。
      “锁”行为其实是和具体访问类的主要功能可以独立、区分开来的。
      “锁”功能其实是这个系统的一个纵向切面,涉及许多类、许多类的方法。如下图:
 

因此,一个新的程序结构应该是关注系统的纵向切面,例如这个应用的“锁”功能,这个新的程序结构就是aspect(方面)

在这个应用中,“锁”方面(aspect)应该有以下职责:

提供一些必备的功能,对被访问对象实现加锁或解锁功能。以保证所有在修改数据对象的操作之前能够调用lock()加锁,在它使用完成后,调用unlock()解锁。

AOP应用范围

很明显,AOP非常适合开发J2EE容器服务器,目前JBoss 4.0正是使用AOP框架进行开发。
具体功能如下:
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging  调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence  持久化
Resource pooling 资源池
Synchronization 同步

Transactions 事务AOP有必要吗?


当然,上述应用范例在没有使用AOP情况下,也得到了解决,例如JBoss 3.XXX也提供了上述应用功能,但是没有使用AOP。

但是,使用AOP可以让我们从一个更高的抽象概念来理解软件系统,AOP也许提供一种有价值的工具。可以这么说:因为使用AOP结构,现在JBoss 4.0的源码要比JBoss 3.X容易理解多了,这对于一个大型复杂系统来说是非常重要的。

从另外一个方面说,好像不是所有的人都需要关心AOP,它可能是一种架构设计的选择,如果选择J2EE系统,AOP关注的上述通用方面都已经被J2EE容器实现了,J2EE应用系统开发者可能需要更多地关注行业应用方面aspect。

AOP具体实现

AOP是一个概念,并没有设定具体语言的实现,它能克服那些只有单继承特性语言的缺点(如Java),目前AOP具体实现有以下几个项目:

AspectJ (TM): 创建于Xerox PARC. 有近十年历史,成熟
缺点:过于复杂;破坏封装;需要专门的Java编译器。

动态AOP:使用JDK的动态代理API或字节码Bytecode处理技术。

基于动态代理API的具体项目有:
JBoss 4.0 JBoss 4.0服务器
nanning 这是以中国南宁命名的一个项目,搞不清楚为什么和中国相关?是中国人发起的?

基于字节码的项目有:
aspectwerkz 
spring ?

定义和概念

AOP像大多数编程范式一样,有她自己的词汇表。下表定义了许多在阅读AOP相关内容或者应用AOP工作时可能会遇到的词汇和短语。这些定义不是Spring特有的。
 
表9.1 AOP定义

Term 术语
Definition 定义
Concern A particular issue
(关注特定问题)
感兴趣应用的特定问题、概念、范围。例如,事务管理、持久化、日志、安全等。
Crosscutting Concern
(横切关注点)
在关注点实现中贯穿了很多类,这在面向对象(OOP)中通常很难实现和维护。
Aspect
(切面)
模块化的横切关注点,通过代码的聚合和隔离实现。
Join Point
(连接点)
在程序或者类执行时的一个点。在Spring的AOP实现中,连接点总是一个方法调用。其他的例子包括访问字段(包括实例中字段的读写),变量和异常处理。
Advice
(通知)
特定连接点所采取的动作。Spring有几种不同类型的通知,包括around、before、throws和after returning。在这几种类型的通知中,around是最强大的,在方法调用的前后都有执行一些操作的机会。之前用到的TraceInterceptor就是around类型的通知,它实现了AOP联盟的MethodInterceptor接口。通过实现下面的Spring接口可以使用其他类型的通知:
Ø         MethodBeforeAdvice
Ø         ThrowsAdvice
Ø         AfterReturningAdvice
Pointcut
(切入点)
连接点的集合,这些连接点确认何时一个通知将会触发。切入点通常使用正则表达式或者是通配符语法。
Introduction
(引入)
添加字段或者方法到一个通知类。Spring允许你在任何通知对象上引入新的接口。例如,你可以使用引入以便让任意对象实现IsModified接口,来简化缓存。
Weaving
(组织)
装配切面来创建一个被通知对象。可以在编译期间完成(像AspectJ做的那样)也可以在运行时完成。本章后面的组织策略部分详细讨论不同的组织策略(也就是实现AOP)。
Interceptor
(拦截器)
一种AOP实现策略,对与特点的连接点,可能存在一个拦截器链。
AOP Proxy
(AOP代理)
AOP框架创建的对象,包括通知。在Spring中,一个AOP代理可能是JDK动态代理或者是CGLIB代理。
Target Object
(目标对象)
包含连接点的对象。在使用拦截的框架中,它是在拦截器链末端的对象实例。也叫做被通知对象或者被代理对象。

 
下个部分是关于切入点的,切入点是应用通知的规则。因为Spring的AOP是基于拦截器的,所以我将会用拦截器来代替通知说明问题。
 

切入点

切 入点是AOP的重要部分。他们能让你确认在何时何地调用拦截器。在某种意义上,他们通常都像是声明式的确认,但是相比确认要验证的字段,你更应该确认要检 查的方法。在上面的表格中,切入点被定义为:确认何时一个通知(拦截器)将触发的一组连接点的集合。由于Spring只支持方法调用连接点,所有在 Spring中切入点也就是应用拦截器的方法的声明。
 
在Spring的AOP中定义切入点的最简单方法是,在context文件中使用正则表达式。下面的例子为数据处理操作定义了一个切入点。
 
<bean id="dataManipulationPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"></bean>
<property name="patterns"></property>
<list></list>
<value></value>.*save.*
<value></value>.*remove.*
 
这个切入点告诉我们,拦截方法的方法名应该以save或者remove开始。
 
 注意
上例的JdkRegexpMethodPointcut类,需要J2SE1.4,它内置了正则表达式支持。也可以改用Perl5RegexpMethodPointcut,它需要Jakarta ORO包(已经在MyUsers里包括了)。
 
大多数情况下,你不必像上面一样定义单独的切入点。Spring提供了一个advisor的类,它在同一个bean中封装了拦截器和切入点。
 
对正则表达式定义的切入点来说,可以使用RegexpMethodPointcutAdvisor这个advisor。下面是一个RegularExpressionPointcutAdvice的例子,它在用户信息被保存的时候触发一个NotificationInterceptor
 
<bean id="notificationAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"></bean>
<property name="advice"></property>
<ref bean="notificationInterceptor"></ref>
<property name="pattern"></property>
<value></value>.*saveUser
 
目前,RegexpMethodPointcutAdvisor只支持Perl5的正则表达式规则,也就是说,如果你要用它,那么在你的classpath下必须要有jakarta-oro.jar 这个包。在org.springframework.aop.support包中有一个详细的切入点列表和他们对应的advisor。
 

组织策略

组织(weaving)是将切面应用到目标对象的过程。下面的列出了实现AOP的基本策略,按从简单到复杂排列。
 
 注意
在这部分大多数信息都以J2EE without EJB中的信息为基础。
Ø         JDK动态代理
Ø         动态字节码生成
Ø         自定义类加载器
Ø         语言扩展
这些策略在各种不同的开源AOP框架中都有实现。
 
 
 注意
这些框架最出色的部分是他们对在API中使用共同的标准很感兴趣。为了支持这一想法,他们创建了AOP联盟计划,定义了大量用来实现的接口。可以阅读AOP联盟的成员列表,看看都哪些框架在这个计划中。
 
下面详细描述各种组织策略。
 
JDK动态代理
 
动 态代理是J2SE1.3以上版本的内置特性。它允许你凭空(on-the-fly)创建一个或更多接口的实现。动态代理内嵌在JDK中,排除了在各种环境 下奇怪行为带来的风险。JDK动态代理有个限制就是它只能代理接口不能代理类。当然如果你用接口很好的设计了你的应用,那这就不是一个问题。
 
使用动态代理的时候还要用一些反射机制,但在J2SE1.4以上的JVM中这点性能消耗可以忽律不计。在代理接口时,Spring默认使用JDK动态代理。dynaop这个项目在代理接口时使用这个策略。
 
更多关于动态代理的信息,可以查看Java 2 SDK文档。
 
字节码动态生成
 
在 代理类时,Spring采用字节码动态生成。CGLIB (Code Generation Library)是做这个的一个流行工具。它通过动态生成子类来拦截方法。这些生成的子类改写父类的方法,用钩子(hook)调用拦截器实现。 Hibernate广泛使用CGLIB,并且已经被证明是可靠的J2EE解决方案。
 
       一个限制是,动态生成的子类不能改写和代理final方法。
 
自定义类加载器
 
使用自定义的类加载器,可以让你通知所创建的实例。这十分强大,因为它提供了修改新操作行为的机会。Jboss AOP和AspectWerkz用的这种方式,根据在XML文件中定义的方式加载和组织类。
这种方式最主要的威胁存在于,J2EE服务器必须仔细地控制类加载层次,在一个服务器上工作很好可以在另一个服务器上就不能正常工作。
 
语言扩展
AspectJ是java AOP框架实现的排头兵。它包含了语言的扩展并且使用自带的编译器,而不是使用简单的策略进行切面的组织。
 
虽 然AspectJ是非常强大和成熟的AOP实现,它的语法还是有点复杂而且也不是很直观。然而,AOP本身就不是很直观,尝试用一种新的语言去实现更显困 难。这种方式的另外一个限制是学习一门新语言的学习曲线。但是,如果你想要完整的AOP功能,包括字段级的拦截,AspectJ可能会成为你最好的伙伴。 在本章的末尾,介绍了集成AspectJ和Spring。
 
在Spring的AOP实现中,采取务实的8-2原则,它解决了最常用的部分,把更专业的部分留给其他AOP框架,而不是试图解决所有的问题。
 

便于应用的代理bean

 
像 前面提到的,为了把通知应用到context文件里定义的bean上,这些bean必须要通过代理。Spring包含很多支撑类(或者说是Proxy Beans)来简化代理。首先是ProxyFactoryBean,它允许你指明要被代理的bean和要应用的拦截器。下面的例子使用了 ProxyFactoryBean来创建一个业务对象的代理。
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean"></bean>
<property name="target"></property>
<bean class="org.appfuse.service.BusinessObject"></bean>
<property name="interceptorNames"></property>
<list></list> <ref bean="loggingInterceptor"></ref>
 
上面的例子在target属性上使用了内置bean(inner-bean)。内置bean是在代理中隐藏业务对象的一个简便方法,因此在从ApplicationContext中抽取出业务bean时,它总是值得推荐的方法。
 
TransactionProxyFactoryBean 是最有用和最常用的代理Bean。这个bean允许你使用AOP在目标对象上声明式地定义事务。事务特性在之前只能通过EJB容器管理的事务(CMT)来 获得。TransactionProxyFactoryBean的使用将会在AOP例子练习部分说明。
 
 
自动代理Bean
 
前述的代理类为单个bean提供了简单的操作,但是如果你想代理多个bean或者上下文中所有的bean时怎么办?Spring提高了两个类(在org.springframework.aop.framework.autoproxy包中)简化这种处理。
 
       第一个是BeanNameAutoProxyCreator,它允许你指明一个bean名称列表作为属性。这个属性支持字面(实际的bean名)和像*Manager的通配符。可以用interceptorNames属性设置拦截器。
<bean font="" id="managerProxyCreator"></bean> 
class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames"></property>
<value></value>*Manager
<property name="interceptorNames"></property>
<list></list>
<value></value>loggingInterceptor
 
       第二个,更通用的多bean代理创建者是DefaultAdvisorAutoProxyCreator。使用这个代理类,简单的在context文件中定义就可以。
<bean font="" id="autoProxyCreator"></bean>
class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
 
不像BeanNameAutoProxyCreator,你不能指明想用的拦截器。它将会检查context文件中的每一个advisor,指出他们的切入点是否可以用到其他的bean上。更多关于advisor的信息,请参阅Spring参考文档。
 
 
AOP实际应用的例子
 
这部分包括了几个使用AOP管理应用中横切关注点的例子,这些关注点包括事务、缓存、事件通知。
 

事务

 
指明操作应该在事务中进行,可能导致在DAO中大量的复制。使用事务时,传统的方式需要在数据操作方法中进行大量的tx.begin()tx.commit()调用。Spring和AOP带来的一个好消息是,可以通过在一个位置声明及配置的方法加强事务管理。
 
       可能还不了解,但是你已经在快速开始那一章,MyUsers这个应用中使用过AOP了。使用Spring的AOP和TransactionProxyFactoryBean,可以让你声明式地在userManager这个bean上指明事务属性。下面的代码显示了一个userManager重构过的版本,这里使用了事务模板bean内置bean

xml 代码
 
  1. class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">  
  2. PROPAGATION_REQUIRED  
  3. PROPAGATION_REQUIRED  
  4. PROPAGATION_REQUIRED,readOnly  

警告
 
       在1.1.2版本之前,可以使用lazy-init=”true”来代替abstract=”true”在1.1.2和之后的版本,Spring会抛出异常:java.lang.IllegalArgumentException: ‘target’ is required。1.1.1版本和之前的版本允许使用lazy-init=”true”来代替abstract=”true”属性。1.1版本添加abstract属性的目的是标记父层bean不会预先初始化。
 
更多关于声明式事务和处理相应异常回滚的信息在第十章会有所涉及。
 
 

中间层缓存

 
另一个关于横切关注点的典型例子是缓存数据。增加缓存的主要目的是改进性能,特别是当获取数据是高代价操作的时候。大多数在最后一章讨论的框架都有他们自己的缓存解决方案,然而缓存是很实际的横切关注点。
 
       由于你引入缓存的动机是提高性能,可以增加一个测试类UserManagerTest测试UserManagerImpl的性能。把下面的代码加到test/org/appfuse/service/UserManagerTest.java中。
 注意
测试中的StopWatch是一个记录任务时间的实用类。
java 代码
 
  1. public void testGetUserPerformance() {  
  2.     user = new User();  
  3.     user.setFirstName("Easter");  
  4.     user.setLastName("Bunny");  
  5.   
  6.     user = mgr.saveUser(user);  
  7.   
  8.     String name = "getUser";  
  9.     StopWatch sw = new StopWatch(name);  
  10.     sw.start(name);  
  11.     log.debug("Begin timing of method '" + name + "'");  
  12.        
  13.     for (int i=0; i < 200; i++) {  
  14.         mgr.getUser(user.getId().toString());  
  15.     }  
  16.   
  17.     sw.stop();  
  18.     log.info(sw.shortSummary());  
  19.     log.debug("End timing of method '" + name + "'");  
  20. }  

执行ant test -Dtestcase=UserManagerTest(applicationContext.xml中没有配置拦截器)会得出类似图9.2的结果。在上面的测试中,获取相同的用户花费大概10秒钟。
 
 
警告
如果使用-Dtestcase=UserManager(没有“Test”后缀),会执行上一章创建的模拟测试。这些测试从Spring分离了UserManager,他们不能证明应用了在applicationContext.xml中定义的拦截器。
 
图9.2 执行ant test -Dtestcase=UserManagerTest的结果
 
       为了提升性能,在UserManagerImpl中添加一个简单的缓存(cache)。下面是缓存的基本需求
1、当需要从数据库取得对象时,把这些对象放入缓存(在本例中用简单的HashMap)。
2、当save()delete()方法调用时,从缓存中剔除对象。
 
       在MyUsers这个应用中,你可以通过添加BaseManager (在org.appfuse.service.impl 包中)实现基本的(非AOP)缓存解决方案,所有的管理方法都从中扩展。
package org.appfuse.service.impl;       // use your IDE to organize imports       public class BaseManager {       // Adding this log variable will allow children to re-use it       protected final Log log = LogFactory.getLog(getClass());       protected Map cache;          protected void putIntoCache(String key, Object value) {           if (cache == null) {               cache = new HashMap();           }           cache.put(key, value);       }          protected void removeFromCache(String key) {           if (cache != null) {               cache.remove(key);           }       }   }  
修改UserManagerImpl使它继承BaseManager,以使用缓存,然后在每个方法中增加对缓存的调用。下面是添加缓存调用之前UserManagerImpl的简单版本:
java 代码
public User getUser(String userId) {       return dao.getUser(Long.valueOf(userId));   }   public User saveUser(User user) {       dao.saveUser(user);       return user;   }   public void removeUser(String userId) {       dao.removeUser(Long.valueOf(userId));   }  
添加这些方法(注:缓存调用)之后,他们显得更详细:
public User getUser(String userId) {       // check cache for user       User user = (User) cache.get(userId);       if (user == null) {           // user not in cache, fetch from database           user = dao.getUser(Long.valueOf(userId));           super.putIntoCache(userId, user);       }       return user;   }       public User saveUser(User user) {       dao.saveUser(user);       // update cache with saved user       super.putIntoCache(String.valueOf(user.getId()), user);       return user;   }       public void removeUser(String userId) {       dao.removeUser(Long.valueOf(userId));       // remove user from cache       super.removeFromCache(userId);   }  

执行ant test -Dtestcase=UserManagerTest,在图9.3中可以看见,运行时间是9秒(快了1秒)。
图9.3 执行ant test -Dtestcase=UserManagerTest的结果

虽然在像MyUsers这样的应用中添加对来自缓存的add/remove调用很简单,但是在大一些的应用中它马上就变得困难起来。不得不记住在每个管理类中添加这些方法。你不应该过分关注缓存,因为它不是应用的核心关注点。

    使用AOP,可以去掉这些方法调用,检查应该那些需要使用缓存的方法。另外,从代码中提出缓存,可以让你更关注业务逻辑以实现工程。
To add a caching interceptor, perform the following steps:

按下面的步骤增加缓存拦截器:

1、用下面的代码在org.appfuse.aop这个包里创建一个CacheInterceptor类。

package org.appfuse.aop;      // use your IDE to organize imports      public class CacheInterceptor implements MethodInterceptor {       private final Log log = LogFactory.getLog(CacheInterceptor.class);          public Object invoke(MethodInvocation invocation) throws Throwable {       String name = invocation.getMethod().getName();           Object returnValue;              // check cache before executing method           if (name.indexOf("get") > -1 && !name.endsWith("s")) {               String id = (String) invocation.getArguments()[0];               returnValue = cache.get(id);               if (returnValue == null) {                   // user not in cache, proceed                   returnValue = invocation.proceed();                   putIntoCache(id, returnValue);                   return returnValue;               } else {                   //log.debug("retrieved object id '" + id + "' from cache");               }           } else {               returnValue = invocation.proceed();                  // update cache after executing method               if (name.indexOf("save") > -1) {                   Method getId = returnValue.getClass().getMethod("getId"new Class[]{});                   Long id = (Long) getId.invoke(returnValue, new Object[]{});                   putIntoCache(String.valueOf(id), returnValue);               } else if (name.indexOf("remove") > -1) {                   String id = (String) invocation.getArguments()[0];                   removeFromCache(String.valueOf(id));               }           }           return returnValue;       }          protected Map cache;          protected void putIntoCache(String key, Object value) {           if (cache == null) {               cache = new HashMap();           }           cache.put(key, value);       }          protected void removeFromCache(String key) {           if (cache != null) {               cache.remove(key);           }       }   }  

2、添加cacheInterceptor这个bean到applicationContext.xml (在web/WEB-INF目录下)。
 
<bean id="cacheInterceptor" class="org.appfuse.aop.CacheInterceptor"></bean>
 
3、在userManager这个bean的配置中,用cacheInterceptor代替loggingInterceptor
 
<property name="preInterceptors"></property>
<list></list>
<ref bean="cacheInterceptor"></ref>
 
 
执行ant test -Dtestcase=UserManagerTest,会发现在性能提升上颇为夸张的结果。
图9.4 执行ant test -Dtestcase=UserManagerTest的测试结果。
 
使用缓存切面(caching aspect),性能损耗减到0!性能上夸张提升的原因是,CacheInterceptor几乎总在UserManagerImpl的方法调用之前返回数据。
 
       这里的缓存例子很简单,更健壮的缓存实现可以参考Pieter Coucke的Spring AOP Cache或者using EHCache with Spring。
 
注:
Spring AOP Cache: http://www.onthoo.com/blog/programming/2004/09/spring-aop-cache.html
using EHCache with Spring: http://opensource.atlassian.com/confluence/spring/display/DISC/Caching+the+result+of+methods+using+Spring+and+EHCache
 

事件通知

 
如果正开放面向公众的web应用,你可能想了解新用户的注册。这在批准用户之前检验用户信息时特别有用。在下面的例子中,将会创建一个NotificationInterceptor并且把它配置成新用户注册时发送电子邮件。
 
1、修改UserManagerTest,以保证当新用户注册时发送消息。下面只包含了相关的部分。用下划线来区别类中新添加的代码。添加这些代码之后,运行ant test -Dtestcase=UserManagerTest 测试一下。
java 代码
 
  1. protected void setUp() throws Exception {  
  2.     String[] paths = {"/WEB-INF/applicationContext*.xml"};  
  3.     ctx = new ClassPathXmlApplicationContext(paths);  
  4.     mgr = (UserManager) ctx.getBean("userManager");  
  5.   
  6.     // Modify the mailSender bean to use Dumbster's ports  
  7.     JavaMailSenderImpl mailSender = (JavaMailSenderImpl) ctx.getBean("mailSender");  
  8.     mailSender.setPort(2525);  
  9. }  
  10.   
  11. public void testAddAndRemoveUser() throws Exception {  
  12.     user = new User();  
  13.     user.setFirstName("Easter");  
  14.     user.setLastName("Bunny");  
  15.   
  16.     // setup a simple mail server using Dumbster  
  17.     SimpleSmtpServer server = SimpleSmtpServer.start(2525);  
  18.   
  19.     user = mgr.saveUser(user);  
  20.   
  21.     server.stop();  
  22.     assertEquals(1, server.getReceievedEmailSize()); // spelling is correct  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值