AOP是OOP的延续,是Aspect Oriented Programming的缩写,意思是面向切面编程。AOP实际是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这种目标的一种实现。
举例:假设有在一个应用系统中,有一个共享的数据必须被并发同时访问,首先,将这个数据封装在数据对象中,称为Data Class,同时,将有多个访问类,专门用于在同一时刻访问这同一个数据对象。为了完成上述并发访问同一资源的功能,需要引入锁Lock的概念,也就是说,某个时刻,当有一个访问类访问这个数据对象时,这个数据对象必须上锁Locked,用完后就立即解锁unLocked,再供其它访问类访问。
使用传统的编程习惯,我们会创建一个抽象类,所有的访问类继承这个抽象父类,如下:
abstract class Worker{
abstract void locked();
abstract void accessDataObject();
abstract void unlocked();
}
这样做的缺点:
(1)accessDataObject()方法需要有“锁”状态之类的相关代码。
(2)Java只提供了单继承,因此具体访问类只能继承这个父类,如果具体访问类还要继承其它父类,比如另外一个如Worker的父类,将无法方便实现。
(3)重用被打折扣,具体访问类因为也包含“锁”状态之类的相关代码,只能被重用在相关有“锁”的场合,重用范围很窄。
仔细研究这个应用的“锁”,它其实有下列特性:
(1)“锁”功能不是具体访问类的首要或主要功能,访问类主要功能是访问数据对象,例如读取数据或更改动作。
(2) “锁”行为其实是和具体访问类的主要功能可以独立、区分开来的。
(3) “锁”功能其实是这个系统的一个纵向切面,涉及许多类、许多类的方法。如下图:
图1
因此,一个新的程序结构应该是关注系统的纵向切面,例如这个应用的“锁”功能,这个新的程序结构就是aspect(方面)。
在这个应用中,“锁”方面(aspect)应该有以下职责:提供一些必备的功能,对被访问对象实现加锁或解锁功能。以保证所有在修改数据对象的操作之前能够调用lock()加锁,在它使用完成后,调用unlock()解锁。
AOP应用范围
很明显,AOP非常适合开发Java EE容器服务器,例如JBoss 4.0之后的版本正是使用AOP框架进行开发。
具体应用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服务器(其子项目Javassist实现基于Bytecode的动态代理),Nanning(这是以中国南宁命名的一个项目,搞不清楚为什么和中国相关?是中国人发起的?),等等。
基于字节码的项目:有aspectwerkz、大名鼎鼎的Spring框架(使用Cglib库来实现字节码级别的动态代理),等等。
AOP定义和概念
AOP像大多数编程范式一样,有她自己的词汇表。下表定义了许多在阅读AOP相关内容或者应用AOP工作时可能会遇到的词汇和短语。这些定义不是Spring特有的。
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
(目标对象)
|
包含连接点的对象。在使用拦截的框架中,它是在拦截器链末端的对象实例。也叫做被通知对象或者被代理对象。
|
解释一下通知的类型:
-
前置通知(Before advice): 在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
-
返回后通知(After returning advice): 在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
-
抛出异常后通知(After throwing advice): 在方法抛出异常退出时执行的通知。
-
后通知(After (finally) advice): 当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
-
环绕通知(Around Advice): 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。
环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。
跟AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽量简单的通知类型来实现需要的功能。 例如,如果你只是需要用一个方法的返回值来更新缓存,虽然使用环绕通知也能完成同样的事情, 但是你最好使用After returning通知而不是环绕通知。 用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。 比如,你不需要调用JoinPoint(用于Around Advice)的proceed() 方法,就不会有调用的问题。
在Spring 2.0中,所有的通知参数都是静态类型,因此你可以使用合适的类型(例如一个方法执行后的返回值类型)作为通知的参数而不是使用一个对象数组。
切入点(pointcut)和连接点(join point)匹配的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。 切入点使得定位通知(advice)可独立于OO层次。 例如,一个提供声明式事务管理的around通知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。
下个部分是关于切入点的,切入点是应用通知的规则。因为Spring的AOP是基于拦截器的,所以我将会用拦截器来代替通知说明问题。切入点
.*save.*
.*remove.*
这个切入点告诉我们,拦截方法的方法名应该以save或者remove开始。
.*saveUser
目前,RegexpMethodPointcutAdvisor只支持Perl5的正则表达式规则,也就是说,如果你要用它,那么在你的classpath下必须要有jakarta-oro.jar 这个包。在org.springframework.aop.support包中有一个详细的切入点列表和他们对应的advisor。
组织策略
JDK动态代理
字节码动态生成
自定义类加载器
语言扩展
便于应用的代理bean
自动代理Bean
AOP实际应用的例子
事务
中间层缓存
另一个关于横切关注点的典型例子是缓存数据。增加缓存的主要目的是改进性能,特别是当获取数据是高代价操作的时候。大多数在最后一章讨论的框架都有他们自己的缓存解决方案,然而缓存是很实际的横切关注点。
由于你引入缓存的动机是提高性能,可以增加一个测试类UserManagerTest测试UserManagerImpl的性能。把下面的代码加到test/org/appfuse/service/UserManagerTest.java中。注意测试中的StopWatch是一个记录任务时间的实用类。
public void testGetUserPerformance() {
user = new User();
user.setFirstName("Easter");
user.setLastName("Bunny");
user = mgr.saveUser(user);
String name = "getUser";
StopWatch sw = new StopWatch(name);
sw.start(name); // 开始计时
log.debug("Begin timing of method '" + name + "'");
for (int i=0; i < 200; i++) { // 不停地获取用户ID
mgr.getUser(user.getId().toString());
}
sw.stop(); // 停止计时
log.info(sw.shortSummary()); //记录所耗时间
log.debug("End timing of method '" + name + "'");
}
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) { // 用HashMap做缓存
cache = new HashMap();
}
cache.put(key, value); // 把对象放入缓存
}
protected void removeFromCache(String key) {
if (cache != null) { // 从缓存中移除对象
cache.remove(key);
}
}
}
修改UserManagerImpl使它继承BaseManager,以使用缓存,然后在每个方法中增加对缓存的调用。下面是添加缓存调用之前UserManagerImpl的简单版本:
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) {
// 检查缓存中是否有指定user
User user = (User) cache.get(userId);
if (user == null) {
// 不在缓存中,则从数据库中获取
user = dao.getUser(Long.valueOf(userId));
super.putIntoCache(userId, user); // 放入缓存
}
return user;
}
public User saveUser(User user) {
dao.saveUser(user); // 保存到数据库
// 用保存的数据更新缓存
super.putIntoCache(String.valueOf(user.getId()), user);
return user;
}
public void removeUser(String userId) {
dao.removeUser(Long.valueOf(userId)); // 从数据库中删除user
// 从缓存中删除user
super.removeFromCache(userId);
}
执行ant test -Dtestcase=UserManagerTest,可以看出运行时间是9秒(快了1秒)。
使用AOP,可以去掉这些方法调用,检查那些需要使用缓存的方法。另外,从代码中提出缓存,可以让你更关注业务逻辑以实现工程。
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);
protected Map cache;
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 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);
}
}
}
这里的缓存例子很简单,更健壮的缓存实现可以参考Pieter Coucke的Spring AOP Cache或者using EHCache with Spring。
事件通知
protected void setUp() throws Exception {
String[] paths = {"/WEB-INF/applicationContext*.xml"};
ctx = new ClassPathXmlApplicationContext(paths);
mgr = (UserManager) ctx.getBean("userManager");
// Modify the mailSender bean to use Dumbster's ports
JavaMailSenderImpl mailSender = (JavaMailSenderImpl) ctx.getBean("mailSender");
mailSender.setPort(2525);
}
public void testAddAndRemoveUser() throws Exception {
user = new User();
user.setFirstName("Easter");
user.setLastName("Bunny");
// setup a simple mail server using Dumbster
SimpleSmtpServer server = SimpleSmtpServer.start(2525);
user = mgr.saveUser(user);
server.stop();
assertEquals(1, server.getReceievedEmailSize()); // spelling is correct
SmtpMessage sentMessage = (SmtpMessage) server.getReceivedEmail().next();
assertTrue(sentMessage.getBody().indexOf("Easter Bunny") != -1);
log.debug(sentMessage);
assertTrue(user.getId() != null);
}
2、在src/org/appfuse/aop下,用下面的代码新建NotificationInterceptor:
package org.appfuse.aop;
// organize imports using your IDE
public class NotificationInterceptor implements MethodInterceptor {
private final Log log = LogFactory.getLog(NotificationInterceptor.class);
private MailSender mailSender;
private SimpleMailMessage message;
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
public void setMessage(SimpleMailMessage message) {
this.message = message;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
User user = (User) invocation.getArguments()[0];
if (user.getId() == null) {
if (log.isDebugEnabled()) {
log.debug("detected new user...");
}
Object returnValue = invocation.proceed();
StringBuffer sb = new StringBuffer(100);
sb.append("A new account has been created for " + user.getFullName());
sb.append(".\n\nView this users information at:\n\t ");
sb.append("http://localhost:8080/myusers/editUser.html?id=" + user.getId());
message.setText(sb.toString());
mailSender.send(message);
}
return user;
}
}
.*saveUser
5、把notificationAdvisor添加到userManager这个bean中的preInterceptors属性中。建议注释掉其他的例子。