Spring in Action
学习笔记
—
第三章
创建切面
Spring的AOP(Aspect Oriented Programming)框架允许你将分散在系统中的功能放到一个地方—切面。依赖Spring的强大切入点机制,何时何地在系统中采用切面你有很多种选择。在本章中作者向我们介绍并且展示了Spring AOP基础的方方面面。
一、
AOP
介绍
AOP提供了另外一种思考程序结构的角度,弥补了OOP(面向对象编程)的不足。大家可以参看夏昕编写的《Spring开发指南》中的AOP部分,有简单明晰的对比和解释。就像刚开始理解OO概念一样,对于新手来说AOP也是非常抽象难以理解的,不能仅从一个概念上去定义AOP。假如我们有一个系统,分为好多个模块,每个模块都负责处理一项重要的功能。但是每个模块都需要一些相似的辅助功能如安全、日志输出等等。这就是一种交叉业务,而这种“交叉”非常适合用AOP来解决。(在AOP刚出现的时候被大家翻译成面向方面,而后来一部分人翻译成面向切面。谁更正确?也许在真正理解了AOP以后才能判断出来。)
1
.AOP
术语
l 切面(Aspect):切面是你要实现的交叉功能。它是应用系统模块化的一个切面领域。
l 连接点(Join point):连接点是应用程序执行过程中插入切面的地点(在程序执行过程中某个特定的点)。在Spring AOP中一个连接点代表一个方法的执行。这个点可以是方法调用,异常抛出或者是要修改的字段。通过申明一个
import
org.aspectj.lang.JoinPoint
类型的参数可以使通知(
Advice
)的主体部分获得连接点信息。
l
通知
(
Advice
):通知切面的实际实现(参考手册:Action taken by an aspect at a particular join point.
)。它通知应用系统新的行为。通知包括好多种类,在后面单独列出。(
Advice
一词在夏昕的《
Spring
开发指南》中被翻译为
“
处理逻辑
”
)
l
切入点(
Pointcut
):切入点定义了通知应该应用在哪些连接点。通知(
Advice
)可以应用到
AOP
的任何连接点。通知(
Advice
)将和一个切入点表达式关联,并在满足这个连接点的切入点上运行(例如:在执行一个特定名称的方法时)切入点表达式如何和连接点匹配是
AOP
的核心
Spring
使用缺省的
AspectJ
切入点的语法。
l
引入(
Introduction
):(也被叫做内部类型声明
“inter-type declaration”
)引入允许你为已经存在的类添加新的方法和属性。
Spring
允许引入新的接口(以及一个对应的实现)到任何被代理的对象。
l
目标对象(
Target Object
):目标对象是被通知对象。
Spring AOP
是运行时代里实现的,所以这个对象永远是一个被代理对象。
l
AOP
代理(
AOP Proxy
):代理是将通知(
Advice
)应用到目标对象后创建的对象。在
Spring2.0
中对于使用最新引入的基于模式(
schema-based
)风格和
@AspectJ
风格切面声明的用户来说,代理的创建是透明的。(要理解这里的代理
“Proxy”
,需要理解代理
“Proxy”
模式:
http://blog.youkuaiyun.com/qutr/archive/2006/07/27/987253.aspx
)
l
织入(
Weaving
):把切面(Aspect)连接到其它的应用程序类型或者对象上来创建一个被通知(advised)的对象。可以在编译时做这件事(例如使用AspectJ编译器),也可以在类加载或运行时完成。 Spring和其他纯Java AOP框架一样, 在运行时完成织入。
通知(
Advice
)类型:
l 前置通知(Before Advice):在一个连接点之前执行的通知,但这个通知不能阻止连接点钱的执行(除非它抛出异常)
l 返回后通知(After returning advice):在一个连接点正常完成后执行的通知。例如:一个方法正常返回,没有抛出任何异常。
l 抛出后通知(After throwing advice):在一个方法抛出异常时执行的通知。
l Finally后通知(After finally advice):当某连接点退出的时候执行的通知(不论是正常返回还是抛出异常)。
l 环绕通知(Around advice):包围一个连接点的通知,就像方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回他们自己的返回值域或抛出异常来结束执行。
2
.Spring
的AOP
实现
在Spring中所有的通知都以Java类的形式编写。所以你可以像普通Java开发那样在IDE(Eclipse或NetBeans)中开发切面。而且,定义在什么地方应用通用的切入点通常在Spring的配置文件中配置。
Spring有两种代理创建方式:
(1) 如果目标对象
(
Target Object
)实现了一个或多个接口暴露的方法,
Spring
将使用
JDK
的
java.lang.reflect.Proxy
类创建代理。
(2)
如果目标对象
(
Target Object
)没有实现任何接口,
Spring
使用
CGLIB
(
http://cglib.sourceforge.net/
)库生成目标对象的子类。在创建这个子类的时候,
Spring
将通知织入,并且将对目标对象的调用委托给这个子类。注意用这种方式创建代理时需要将
Spring
发行包中
lib/cglib
文件加下的
jar
文件加载到工程文件中。使用这种代理时注意两点:
l 对接口创建代理优于对类创建代理,这样会产生更加松耦合的系统。
l 标记为final的方法不能被通知。
通过上面的一大堆名词解释应该对AOP以及Spring中的AOP有了一个大概的了解了。下面进一步详细解释Spring中的AOP。
二、创建通知
1
.前置通知(Before Advice
)
创建前置通知需要实现
org.springframework.aop.MethodBeforeAdvice
接口,该接口只有
before(Method method, Object[] args, Object target)
throws
Throwable
这个方法。我们可以这样理解前置通知:比如你到一个比较高档的饭店去吃饭,进门时会有礼仪小姐给你开门并且向您问好,这个礼仪小姐的行为就可以视为
“
前置通知
”
。
package
springinaction.chapter03.createadvice;
import
java.lang.reflect.Method;
import
org.springframework.aop.MethodBeforeAdvice;
public
class
BeforeAdvice
implements
MethodBeforeAdvice
{
//
实现
MethodBeforeAdvice
的
before
方法
public
void
before(Method method, Object[] args, Object target)
{
System.
out
.println(
"befor advice"
);
}
//end before
}
//end class BeforeAdvic
e
2
.返回后通知(After returning advice
)
创建返回后通知需要实现
org.springframework.aop.AfterReturningAdvice
接口,该接口也只有一个方法:
void
afterReturning(Object returnValue, Method method, Object[] args, Object target)
throws
Throwable
。同样我们也可以这样理解After returning advice:当你在这家饭店用餐完后,礼仪小姐还会为您开门,并且欢迎您下次再来。这时礼仪小姐的行为就是“After returning advice”。
package
springinaction.chapter03.createadvice;
import
java.lang.reflect.Method;
import
org.springframework.aop.AfterReturningAdvice;
public
class
AfterReturningAdviceImp
implements
AfterReturningAdvice
{
public
void
afterReturning(Object returnValue, Method method, Object[] arg2, Object target)
throws
Throwable
{
System.
out
.println(
"afterReturning"
);
}
}
3
.环绕通知(Around advice
)
创建环绕通知需要实现
org.aopalliance.intercept.MethodInterceptor
接口,同样要实现一个
invoke
方法,该方法有一个
MethodInvocation
类型的参数。
MethodInterceptor
能够控制目标方法是否真的被调用。通过调用
MethodInterceptor.proceed()
方法来调用目标方法。
MethodInterceptor
让你可以控制返回的对象,你可以返回一个与
proceed()
方法返回对象完全不同的对象。我们可以这样理解环绕通知:当你用餐完后,饭店要确保你已经付款了,在付款后停止再出售食物给你。
package
springinaction.chapter03.createadvice;
import
org.aopalliance.intercept.MethodInterceptor;
import
org.aopalliance.intercept.MethodInvocation;
public
class
AroundAdvice
implements
MethodInterceptor
{
public
Object invoke(MethodInvocation mi)
{
Object obj =
null
;
//do something....
return
obj;
}
//end invoke(...)
}
//end class AroundAdvice(
)
4
.抛出后通知(After throwing advice
)
创建抛出后通知需要实现
org.springframework.aop.ThrowsAdvice
接口,该接口没有任何方法,但是要实现这个接口的类必须实现
afterThorwing(Throwable throwable)
或者
afterThrowing(Method method, Object[] args, Object target)
形式的一种,根据抛出的异常的类型恰当的方法将被调用。如果你在该饭店用餐后没有付钱就走或者多给钱他们没有找给你大概异常通知也就发生了。
J
package
springinaction.chapter03.createadvice;
import
java.lang.reflect.Method;
import
org.springframework.aop.ThrowsAdvice;
public
class
AfterThorwsAdvice
implements
ThrowsAdvice
{
public
void
afterThorwing(Throwable throwable)
{
//do something....
}
public
void
afterThrowing(Method method, Object[] args, Object target)
{
//do something....
}
}
一般来说环绕通知是用的最广泛的一个通知类型,但是他们(Spring小组)鼓励我们用最合适的通知。例如我们有一个简单的验证身份的功能,那么我们只需要前置通知就可能实现我们要求的功能了。
三、定义切入点
在上面我们定义了各种通知。可以看到一个通知是要被执行的一个方法。光把通知定义出来不行,我们还要确定这些通知在我们的系统什么地方应用,否则通知是毫无用处的。这就是切入点的用处。切入点决定了一个特定的类的特定方法是否满足一条特定规则。如果确实符合,通知就应用到该方法上。
Spring根据需要织入通知的类和方法来定义切入点。Advice根据他们的特性织入目标类和方法。Spring的切入点框架的核心接口是Pointcut。
public
interface
Pointcut
{
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
ClassFilter
接口决定了一个类是否符合通知的要求:
package
org.springframework.aop;
public
interface
ClassFilter
{
boolean
matches(Class clazz);
ClassFilter
TRUE
= TrueClassFilter.
INSTANCE
;
}
实现这个接口的类决定了以参数传入进来的类是否应该被通知。
ClassFilter.
TRUE
是规范的的适合任何类的
ClassFilter
实例,他适用于创建只根据方法决定时候符合要求的切入点。
ClassFilter
接口利用类过滤切面,
MethodMactcher
接口可以通过方法过滤切面:
package
org.springframework.aop;
import
java.lang.reflect.Method;
public
interface
MethodMatcher
{
boolean
matches(Method method, Class targetClass);
boolean
isRuntime();
boolean
matches(Method method, Class targetClass, Object[] args);
MethodMatcher
TRUE
= TrueMethodMatcher.
INSTANCE
;
}
MethodMactcher
接口有三个方法。
matches(Method, Class)
根据目标类和方法决定一个方法是否该被通知。
isRuntime()
方法被调用来决定
MethodMatcher
的类型。有两种类型:静态类型和动态类型。静态
pointcut
的意思是
Advice
总是被执行,此时的
isRuntime()
方法返回
false
。动态
pointcut
根据运行时方法的参数值决定通知是否需要执行,
isRuntime()
方法返回
true
。
有一个非常重要的名词
Advisor
:大多数切面是由定义切面行为的通知和定义切面在什么地方执行的切入点组合而成的。在
Spring
中把通知和切入点组合到一个对象中。
PointcutAdvisor
提供这些功能。
package
org.springframework.aop;
public
interface
PointcutAdvisor
extends
Advisor
{
Pointcut getPointcut();
}
Spring
中的
RegexpMethodPointcut
让你利用正则表达式来定义切入点。如果你对正则表达式不了解的话,赶快去学学吧,非常重要的内容。(开个玩笑的说,用了正则表达式能使你的代码上档次
J
)
,
下面列出定义切入点时经常使用的符号:
符号
|
描述
|
示例
|
·
|
(英文点“.”)匹配任何单个字符
|
setFoo.匹配setFooB,但不匹配setFoo或setFooBar
|
+
|
匹配前一个字符一次或多次
|
setFoo.+匹配setFooB或setFooBar,但不匹配setFoo
|
*
|
匹配前一个字符0次或多次
|
setFoo.*匹配setFooB、setFooBar和setFoo
|
/
|
匹配任何正则表达式符号
|
/.setFoo.+匹配setFoo,但不匹配setFoo
|
下面引用《精通Srping》中的例子,这个例子讲的还是比较清晰的。如果前面的Ioc一章理解的很好的话,理解这个例子不是很难。难点在于
ProxyFactoryBean
这个类。
package
jingtongspring;
import
org.apache.commons.logging.Log;
import
org.apache.commons.logging.LogFactory;
import
org.springframework.aop.MethodBeforeAdvice;
import
java.lang.reflect.Method;
public
class
LoggingBeforAdvice
implements
MethodBeforeAdvice
{
protected
static
final
Log
log
= LogFactory.getLog(LoggingBeforAdvice.
class
);
public
void
before(Method arg0, Object[] arg1, Object arg2)
throws
Throwable
{
log
.info(
"before: The invocation of getContent()"
);
}
}
下面是配置文件:
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<!
DOCTYPE
beans
PUBLIC
"-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd"
>
<
beans
>
<
bean
id
=
"helloworldbean"
class
=
"org.springframework.aop.framework.ProxyFactoryBean"
>
<
property
name
=
"proxyInterfaces"
>
<
value
>
jingtongspring.IHelloWord
</
value
>
</
property
>
<
property
name
=
"target"
>
<
ref
local
=
"helloworldbeanTarget"
/>
</
property
>
<
property
name
=
"interceptorNames"
>
<
list
>
<
value
>
loggingBerforeAdvisor
</
value
>
</
list
>
</
property
>
</
bean
>
<
bean
id
=
"helloworldbeanTarget"
class
=
"jingtongspring.HelloWord"
/>
<
bean
id
=
"loggingBeforAdbisor"
class
=
"org.springframework.aop.support.RegexpMethodPointcutAdvisor"
>
<
property
name
=
"advice"
>
<
ref
local
=
"loggingBeforeAdvice"
/>
</
property
>
<
property
name
=
"pattern"
>
<
value
>
.*
</
value
>
</
property
>
</
bean
>
<
bean
id
=
"loggingBeforeAdvice"
class
=
"jingtongspring.LoggingBeforeAdvice"
/>
</
beans
>
四、创建引入
在前面我们列出了
Spring
的四种通知类型,并且有相应的简单例子。但是引入(
Introduction
)通知与其他类型的通知有所不同,它影响整个类。通过给需要消息的类添加方法和属性来实现。(看上去很神气,因为说道类那就是已经封装好了的,给类添加方法除非是继承该类。
Spring
中怎样实现?)
Spring
通过一个特殊的方法拦截器接口
IntroductionInterceptor
来实现引入。这个接口添加一个方法:
boolean implementsInterface(Class intf)
这个方法对于引入如何工作非常关键。如果
IntroductionInterceptor
是为了实现指定接口,那么方法
implementsInterface
应该返回
true
。也就是说,对用这个接口声明的方法的任何调用将被委托给
IntroductionInterceptor
的
invoke()
方法。
invoke()
方法负责实现这个方法,不能调用
MethodInvocation.proceed()
。它引入了新的借口,调用目标对象是没有用的。(注意:在本书中提到了一个
IntroductionMethodInterceptor
接口,我查看
Spring2.0 rc3
的源代码并没有该类,可能是在
Spring1.x
中存在的,
IntroductionInterceptor
接口的定义形式是这样的:
public
interface
IntroductionInterceptor
extends
MethodInterceptor, DynamicIntroductionAdvice {}
,
invoke()
方法估计是在它的两个父接口之一中存在。
)
下面看例子:
先实现一个接口:
package
springinaction.chapter03.createadvice;
import
java.util.Date;
public
interface
Auditable
{
void
setLastModifiedDate(Date date);
Date getLastModifiedDate();
}
再实现一个类:
package
springinaction.chapter03.createadvice;
import
java.util.Date;
import
org.aopalliance.intercept.MethodInvocation;
import
org.springframework.aop.IntroductionInterceptor;
public
class
AuditableMixin
implements
IntroductionInterceptor, Auditable
{
private
Date
lastModifiedDate
;
public
boolean
implementsInterface(Class intf)
{
return
intf.isAssignableFrom(Auditable.
class
);
}
//end implementsInterface(...)
public
Object invoke(MethodInvocation m)
throws
Throwable
{
if
(
this
.implementsInterface(m.getMethod().getDeclaringClass())){
return
m.getMethod().invoke(
this
, m.getArguments());
}
else
{
return
m.proceed();
}
}
//end invoke(...)
public
Date getLastModifiedDate()
{
return
this
.
lastModifiedDate
;
}
//end getLastModifiedDate()
public
void
setLastModifiedDate(Date lastModifiedDate)
{
this
.
lastModifiedDate
= lastModifiedDate;
}
}
//end class AuditableMixi
n
在
AuditableMixin
类中实现了
IntroductionInterceptor
接口和业务接口
Auditable
。如果被申明调用方法的类型是
Auditable
类型,
implementsInterface
方法返回
true
。这说明,对于
Auditable
的两个方法,拦截器必须提供实现,在
invoke
方法中实现。对于任何
Auditable
接口方法的调用,调用我们的拦截器。对于其他方法的调用我们让
Method Invocation
处理。
Spring
提供了一个一个方便的类来处理我们的大多数应用,这个类是:
DelegatingIntroductionInterceptor
应用这个类我们不再需要实现
invoke
这个方法了因为在
DelegatingIntroductionInterceptor
类中已经实现好了(可以参看一下源代码)。看下面的例子:
package
springinaction.chapter03.createadvice;
import
java.util.Date;
import
org.springframework.aop.support.DelegatingIntroductionInterceptor;
public
class
ImmutableMixin
extends
DelegatingIntroductionInterceptor
implements
Auditable
{
private
Date
lastModifiedDate
;
public
Date getLastModifiedDate()
{
return
this
.
lastModifiedDate
;
}
//end getLastModifiedDate()
public
void
setLastModifiedDate(Date lastModifiedDate)
{
this
.
lastModifiedDate
= lastModifiedDate;
}
//end setLastModifiedDate(...)
}
//end class ImmutableMixi
n
DelegatingIntroductionInterceptor
类也要实现你的混合类暴露的任何方法,并且将任何对这些方法的调用委托给这个混合类。因为
ImmutableMixin
类实现了
Auditable
接口,对这个接口的方法的所有调用都将调用我们的拦截器。任何其他方法委托给目标对象。如果你的拦截器要实现一个接口,你不想把它暴露成一个混合体,那么只要简单的将这个接口传递给
DelegatingIntroductionInterceptor
类的
suppressInterface()
方法就可以了(该方法在
DelegatingIntroductionInterceptor
类的父类
IntroductionInfoSupport
中实现)。还有一点:如果你的混合体要改变任何目标对象方法的行为的话,你就需要实现
invoke()
方法。
有了自己的引入通知后,我们需要创建
Advisor
。因为引入通知之应用在类层次上,所以引入有他们自己的
Advisor
:
IntroductionAdvisor
。
Spring
也提供了一个适合大多数情况的缺省实现。名为:
DefaultIntroductionAdvisor
,它有一个以
IntroductionInterceptor
作为参数的构造函数。
在本章中使用了大量的
ProxyFactoryBean
来创建一个被通知的类。当你想明确的控制你的通知类如何组合时,这种是方法最好、最简单的选择。
ProxyFactoryBean
有很多控制行为的属性。在书中作者列出了一个详细的表格进行了说明,这里就不罗列了。详细的
ProxyFactoryBean
大家也可以看一下它的源代码。
五、小结
写到这里我不得不打住了,再写下去我都不知道写的是什么了,对于新手来说AOP确实是非常抽象的概念,要想真正理解AOP并且编写出AOP模块,在深入理解概念的同时还要进行大量的实践才行,从实践中去真正的体会AOP。那么AOP有三个最核心的概念,这三个概念是:Advice、Pointcut和Advisor。Advice是你想向其他程序内部不同地方注入的代码。Pointcut定义了需要注入Advisor的位置,这个位置通常是某个特定的类的一个public方法。Advisor是Pointcut和Advice的装配器,是将Advice注入程序中预定义位置的代码。按照这样的主线再去解读AOP的各个概念,再通过实践理解AOP应该是比较容易的。
如果你对AOP的概念和内涵难以理解,那就多找几本书或其他资料看看。最主要的还是看别人写的代码和自己大量的实践。下面我列出几个参考大家可以找来看看:
1. 最重要的还是应该参考官方文档(也就是Spring参考手册)。已经在Spring官方网站上组织翻译了。看他们的回帖说是正在进行翻译审定,可能是以最新的Rc4为最后标准了。最终的Spring 2.0发布版本在9月26号出来。链接:(
http://www.jactiongroup.net/reference2/html/ )
2. 夏昕编写的OpenDoc《Spring开发指南》,虽然没有全方位的解释AOP但是文档中的内容还是将一些概念讲解的很清晰。链接:(
http://wiki.redsaga.com/confluence/homepage.action 需要自己查找)
3. 《精通Spring》,里面关于AOP应用配置的例子还是容易懂的。链接:(
http://www.china-pub.com/computers/common/info.asp?id=24483 )
4. 《Pro Spring》中文版已经出版,这本书我在书店里翻着看了一阵,翻译的还是比较不错的,书有些厚(和板砖一样厚,但是比板砖大
J)但确实讲的不错。希望想买本书学习Spring的人建议买这本,看中文版的学习速度比较快,毕竟没有语言障碍。如果你的英文能力强,建议直接看英文原文,因为网上到处都有下载的。我看得就是英文版,比较好理解。链接:(
http://www.china-pub.com/computers/common/info.asp?id=29859 )
Trackback: http://tb.blog.youkuaiyun.com/TrackBack.aspx?PostId=1286953