原文地址:http://tech.it168.com/j/2007-08-30/200708302209432.shtml
概述
在低版本Spring中定义一个切面是比较麻烦的,需要实现特定的接口,并进行一些较为复杂的配置,低版本Spring AOP的配置是被批评最多的地方。Spring听取这方面的批评声音,并下决心彻底改变这一现状。在Spring2.0中,Spring AOP已经焕然一新,你可以使用@AspectJ注解非常容易的定义一个切面,不需要实现任何的接口。
Spring2.0采用@AspectJ注解对POJO进行标注,从而定义一个包含切点信息和增强横切逻辑的切面,Spring 2.0可以将这个切面织入到匹配的目标Bean中。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。在你学习基于@AspectJ的切面技术后,恐怕你就再也没有兴趣使用低版本Spring AOP的实现技术了,毕竟马落桃花马前雪,两者的易用性、便捷性是不可同日而语的。
着手使用@AspectJ
我们知道在低版本的Spring AOP中,你必须使用Pointcut和Advice接口描述切点和增强,并用Advisor组合两者描述一个切面,@AspectJ则采用JDK 5.0的注解技术描述切点和增强类型,而增强的横切逻辑就在被标注的POJO中定义。
使用前的准备
在使用@AspectJ之前,首先你得保证你所使用的JDK的版本是5.0及以上版本,否则无法使用注解技术。 Spring在处理@Aspect注解表达式时,需要使用位于<SPRING_HOME>/lib/asm/目录下asm的类包:asm-2.2.2.jar、asm-commons-2.2.2.jar和asm-util-2.2.2.jar。asm是轻量级的字节码处理框架,因为Java的反射机制无法获取入参名,Spring就利用asm处理@AspectJ中所描述的方法入参名。
此外,Spring采用AspectJ提供的@AspectJ注解类库及相应的解析类库,它位于<SPRING_HOME>/lib/aspectj目录下,将目录下的aspectjrt.jar和aspectjweaver.jar类包加入类路径中。
一个简单的例子
在做好上节中所提到的前置工作后,我们就可以开始编写一个基于@AspectJ的切面了,首先来看一个简单的例子,以便对@AspectJ有一个切身的认识。
下面,我们用@AspectJ注解对一个POJO进行标注,将使其成为一个切面类:
代码清单 1 PreGreetingAspect:切面
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect ①通过该注解将PreGreetingAspect标识为一个切面
public class PreGreetingAspect{
@Before("execution(* greetTo(..))") ②定义切点和增强类型
public void beforeGreeting(){③增强的横切逻辑
System.out.println("How are you");
}
}
我们“惊奇”地发现这个切面没有实现任何特殊的接口,它只是一个普通的POJO。它特殊的地方在于使用了@AspectJ注解。
首先,在PreGreetingAspect类定义处,标注了一个@Aspectj注解,第三方处理程序就可以通过类是否拥有@Aspectj注解判断其是否是一个切面,如①所示。
其次,在beforeGreeting()方法标签处,标注了@Before注解,并为该注解提供了成员值"execution(* greetTo(..))",如②所示。②处的注解提供了两个信息:@Before注解表示该增强是前置增强,而成员值通过@ApsectJ切点表达式语法定义切点:即在目标类的greetTo()方法上织入增强,greetTo()方法可以带任意的入参和任意的返回值。
最后,在③处的beforeGreeting()方法是增强的横切逻辑,该横切逻辑在目标方法前调用,我们通过下图描述这种关系:
图 1 切面的信息构成
PreGreetingAspect类通过注解和代码,将切点、增强类型和增强的横切逻辑揉合到一个类中,使切面的定义浑然天成。如果在低版本Spring AOP中,你必须同时创建增强类,切点类以及切面类,并使三者联合表达相同的信息。
NaiveWaiter是一个Bean,它拥有一个greetTo()的方法,这个方法连接点匹配于上面我们通过@AspectJ所定义的切点,为了方便后续的说明,我们给出NaiveWaiter的代码:
public class NaiveWaiter implements Waiter {
public void greetTo(String clientName) {
System.out.println("NaiveWaiter:greet to "+clientName+"...");
}
public void serveTo(String clientName){
System.out.println("NaiveWaiter:serving "+clientName+"...");
}
}
下面,我们通过org.springframework.aop.aspectj.annotation.AspectJProxyFactory为NaiveWaiter生成织入PreGreetingAspect切面的代理,如代码清单 2所示:
代码清单 2 AspectJProxyTest
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import com.baobaotao.NaiveWaiter;
import com.baobaotao.Waiter;
publicclass AspectJProxyTest {
Waiter target =new NaiveWaiter();
AspectJProxyFactory factory =new AspectJProxyFactory();
factory.setTarget(target); ① 设置目标对象
factory.addAspect(PreGreetingAspect.class); ②添加切面类
Waiter proxy = factory.getProxy(); ③ 生成织入切面的代理对象
proxy.greetTo("John");
proxy.serveTo("John");
}
}
Spring使用AspectJProxyFactory织入基于@AspectJ切面的工作。在①处,设置了目标对象,在②处添加一个切面类,该类必须是带@AspectJ注解的类,在③处,我们就可以获取织入切面的代理对象了。
接下来,我们直接通过代理对象调用greetTo()和serveTo()代码,它们产生以下的输出信息:
How are you ①表示greetTo()方法被成功地织入了切面
greet to John...
serving John...
通过①处的输出信息我们可以知道代理对象的greetTo()方法已经织入了切面类所定义的增强逻辑了。
通过配置织入@AspectJ切面
虽然可以通过编程的方式织入切面,但一般情况下,我们还是使用Spring的配置自动完成创建代理织入切面的工作。
②使用了@AspectJ注解的切面类
<bean class="com.baobaotao.aspectj.example.PreGreetingAspect"/>
③自动代理创建器,自动将@AspectJ注解切面类织入到目标Bean中
<bean class="org.springframework.aop.aspectj.annotation.
AnnotationAwareAspectJAutoProxyCreator"/>
AnnotationAwareAspectJAutoProxyCreator能够将@AspectJ注解切面的自动织入到目标Bean中。这里,PreGreetingAspect是使用了@AspectJ注解描述的切面类,而NaiveWaiter是匹配切点的目标类。
如果使用基于Schema的aop命名空间进行配置,事情就更简单了:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" ①
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop ②
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:aspectj-autoproxy /> ③基于@AspectJ切面的驱动器
<bean id="waiter"class="com.baobaotao.NaiveWaiter"/>
<bean class="com.baobaotao.aspectj.example.PreGreetingAspect"/>
</beans>
首先,在配置文件中引入aop命名空间,如①、②处所示,然后通过aop命名空间的<aop:aspectj-autoproxy />声明自动为Spring容器中那些匹配@AspectJ切面的Bean创建代理,织入切面。当然,Spring在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了。
<aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用JDK动态代理织入增强,当配置为<aop:aspectj-autoproxy proxy-target-class="true" />时,表示使用CGLib动态代理技术织入增强。不过即使proxy-target-class设置为false,如果目标类没有声明接口,则Spring将自动使用CGLib动态代理。
@AspectJ语法基础
@AspectJ使用JDK 5.0注解和正规的AspectJ 5的切点表达式语言描述切面,由于Spring只支持方法的连接点,所以Spring仅支持部分AspectJ的切点语言。在这节时,我们将对AspectJ切点表达式语言进行必要的学习。
切点表达式函数
AspectJ 5的切点表达式由关键字和操作参数组成,如execution(* greetTo(..))的切点表达式,“execute”为关键字,而“* greetTo(..)”为操作参数。在这里,execute代表目标类执行某一方法,而“* greetTo(..)”是描述目标方法的匹配模式串,两者联合起来所表示的切点匹配目标类greetTo()方法的连接点。为了描述方便,我们将execution()称作函数,而将匹配串“* greetTo(..)”称作函数的入参。
Spring支持9个@ApsectJ切点表达式函数,它们用不同的方式描述目标类的连接点,根据描述对象的不同,可以将它们大致分为4种类型:
方法切点函数:通过描述目标类方法信息定义连接点;
方法入参切点函数:通过描述目标类方法入参的信息定义连接点;
目标类切点函数:通过描述目标类类型信息定义连接点;
代理类切点函数:通过描述目标类的代理类的信息定义连接点;
这4种类型的切点函数,通过表 1进行说明:
表 1 切点函数
类别
|
函数
|
入参
|
说明
|
方法切点函数
|
execution()
|
方法
匹配模式串
|
表示满足某一匹配模式的所有目标类方法连接点。如execution(* greetTo(..))表示所有目标类中的greetTo()方法。
|
@annotation()
|
方法注
解类名
|
表示标注了特定注解的目标方法连接点。如@annotation(com.baobaotao.anno.NeedTest)表示任何标注了@NeedTest注解的目标类方法。
|
|
方法入参切点函数
|
args()
|
类名
|
通过判别目标类方法运行时入参对象的类型定义指定连接点。如args(com.baobaotao.Waiter)表示所有有且仅有一个按类型匹配于Waiter的入参的方法。
|
@args()
|
类型注
解类名
|
通过判别目标方法的运行时入参对象的类是否标注特定注解来指定连接点。如@args(com.baobaotao.Monitorable)表示任何这样的一个目标方法:它有一个入参且入参对象的类标注@Monitorable注解。
|
|
目标类切点函数
|
within()
|
类名匹配串
|
表示特定域下的所有连接点。如within(com.baobaotao.service.*)表示com.baobaotao.service包中的所有连接点,也即包中所有类的所有方法,而within(com.baobaotao.service.*Service)表示在com.baobaotao.service包中,所有以Service结尾的类的所有连接点。
|
target()
|
类名
|
假如目标类按类型匹配于指定类,则目标类的所有连接点匹配这个切点。如通过target(com.baobaotao.Waiter)定义的切点,Waiter、以及Waiter实现类NaiveWaiter中所有连接点都匹配该切点。
|
|
@within()
|
类型注解类名
|
假如目标类按类型匹配于某个类A,且类A标注了特定注解,则目标类的所有连接点匹配这个切点。
如@within(com.baobaotao.Monitorable)定义的切点,假如Waiter类标注了@Monitorable注解,则Waiter以及Waiter实现类NaiveWaiter类的所有连接点都匹配。
|
|
@target()
|
类型注解类名
|
目标类标注了特定注解,则目标类所有连接点匹配该切点。如@target(com.baobaotao.Monitorable),假如NaiveWaiter标注了@Monitorable,则NaiveWaiter所有连接点匹配切点。
|
|
代理类切点函数
|
this()
|
类名
|
代理类按类型匹配于指定类,则被代理的目标类所有连接点匹配切点。这个函数比较难理解,这里暂不举例,留待后面详解。
|
@AspectJ除上表中所列的函数外,还有call()、initialization()、 preinitialization()、 staticinitialization()、 get()、 set()、handler()、 adviceexecution()、 withincode()、 cflow()、 cflowbelow()、 if()、 @this()以及@withincode()等函数,这些函数在Spring中不能使用,否则会抛出IllegalArgumentException异常。在不特别声明的情况下,本书中所讲@AspectJ函数均指表 1中所列的函数。
的控制流。After注解类拥有2个成员:
value:该成员用于定义切点;
argNames:如前所述。
@DeclareParents
引介增强,相当于IntroductionInterceptor,DeclareParents注解类拥有2个成员:
value:该成员用于定义切点,它表示在哪个目标类上添加引介增强;
defaultImpl:默认的接口实现类。
除引介增强外,其它增强都很容易理解,我们将在本文后续内容中统一讲述,但引介增强的使用比较特别,因为我们特别在下节中为其准备了一个实例。
引介增强用法
请看以下两个接口及其实现类,如图 2所示:
图 2 Waiter和Seller
假设我们希望NaiveWaiter能够同时充当售货员的角色,即通过切面技术为NaiveWaiter新增Seller接口的实现。我们可以使用@AspectJ的引介增强来实现这一功能。
代码清单 3 EnableSellerAspect
package com.baobaotao.aspectj.basic;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import com.baobaotao.Seller;
import com.baobaotao.SmartSeller; @Aspect
public class EnableSellerAspect {
①为NaiveWaiter添加接口实现
@DeclareParents(value="com.baobaotao.NaiveWaiter",
defaultImpl=SmartSeller.class) ② 默认的接口实现类
public Seller seller; ③要实现的目标接口
}
在EnableSellerAspect切面中,我们通过@DeclareParents为NaiveWaiter添加了一个需要实现的Seller接口,并指定其默认实现类为SmartSeller,然后通过切面技术将SmartSeller融合到NaiveWaiter中,这样NaiveWaiter就实现Seller接口了。
在Spring配置文件中配置好切面和NaiveWaiter Bean:
<bean id="waiter"class="com.baobaotao.NaiveWaiter"/>
<bean class="com.baobaotao.aspectj.basic.EnableSellerAspect"/>
运行以下测试代码:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.baobaotao.Seller;
import com.baobaotao.Waiter;
public class DeclaredParentsTest ...{
public static void main(String[] args) ...{
String configPath = "com/baobaotao/aspectj/basic/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter waiter = (Waiter)ctx.getBean("waiter");
waiter.greetTo("John");
Seller seller = (Seller)waiter; ①可以成功进行强制类型转换
seller.sell("Beer", "John");
}
}
代码成功执行,并输出以下信息:
NaiveWaiter:greet to John...
SmartSeller: sell Beer to John...
可见,NaiveWaiter已经成功地新增了Seller接口的实现。
切点函数详解
切点函数是AspectJ表达式语言的核心,是使用@AspectJ进行切面定义的难点,本节我们通过具体实例对切点函数进行深入学习。为了方便讲解,我们假设目标类包括以下7个类,这些目标类都位于com.baobaotao.*包中:
图 3 Waiter和Seller类图
这些类中,除了SmartSeller#showGoods()方法是protected外,其它的方法都是public。
@annotation()
@annotation表示标注了某个注解的所有方法。我们通过一个实例说明@annotation()的用法,TestAspect定义了一个后置增强切面,该增强将应用到标注有NeedTest的目标方法中:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
publicclass TestAspect {
@AfterReturning("@annotation(com.baobaotao.anno.NeedTest)") ①后置增强切面
publicvoid needTestFun(){
System.out.println("needTestFun() executed!");
}
}
假设NaughtyWaiter#greetTo()方法标注了@NeedTest注解,而NaiveWaiter#greetTo()方法没有标注@NeedTest注解,如代码清单 4所示:
代码清单 4 标注了NeedTest注解的NaughtyWaiter
import com.baobaotao.anno.NeedTest;
publicclass NaughtyWaiter implements Waiter {
@NeedTest
publicvoid greetTo(String clientName) {
System.out.println("NaughtyWaiter:greet to "+clientName+"...");
}
…
}
通过Spring配置自动应用切面:
<aop:aspectj-autoproxy />
<bean id="naiveWaiter" class="com.baobaotao.NaiveWaiter" />
<bean id="naughtyWaiter" class="com.baobaotao.NaughtyWaiter" />
<bean class="com.baobaotao.aspectj.fun.TestAspect" />
运行以下的代码:
代码清单 5 PointcutFunTest:测试代码
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.baobaotao.Seller;
import com.baobaotao.Waiter;
publicclass PointcutFunTest{
publicstaticvoid main(String[] args) {
String configPath ="com/baobaotao/aspectj/fun/beans.xml";
ApplicationContext ctx =new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
Waiter naughtyWaiter = (Waiter) ctx.getBean("naughtyWaiter");
naiveWaiter.greetTo("John"); ①该方法未被织入增强
naughtyWaiter.greetTo("Tom");②该方法被织入增强
}
}
输出以下信息:
NaiveWaiter:greet to John...
NaughtyWaiter:greet to Tom... ①对应NaughtyWaiter的greetTo()
needTestFun() executed!
从以上的信息中,我们可以获知切面被正确地织入到NaughtyWaiter#greetTo()方法中。
execution()
execution()是最常用的切点函数,其语法如下所示:
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。与其直接讲解该方法的使用规则,还不如通过一个个具体的例子进行理解。下面,我们给出各种使用execution()函数实例。
1)通过方法签名定义切点
execution(public * *(..))
匹配所有目标类的public方法,但不匹配SmartSeller和protected void showGoods()方法。第一个*代表返回类型,第二个*代表方法名,而..代表任意入参的方法;
execution(* *To(..))
匹配目标类所有以To为后缀的方法。它匹配NaiveWaiter和NaughtyWaiter的greetTo()和serveTo()方法。第一个*代表返回类型,而*To代表任意以To为后缀的方法;
2)通过类定义切点
execution(* com.baobaotao.Waiter.*(..))
匹配Waiter接口的所有方法,它匹配NaiveWaiter和NaughtyWaiter类的greetTo()和serveTo()方法。第一个*代表返回任意类型,com.baobaotao.Waiter.*代表Waiter接口中的所有方法;
execution(* com.baobaotao.Waiter+.*(..))
匹配Waiter接口及其所有实现类的方法,它不但匹配NaiveWaiter和NaughtyWaiter类的greetTo()和serveTo()这两个Waiter接口定义的方法,同时还匹配NaiveWaiter#smile()和NaughtyWaiter#joke()这两个不在Waiter接口中定义的方法。
3)通过类包定义切点
在类名模式串中,“.*”表示包下的所有类,而“..*”表示包、子孙包下的所有类。
execution(* com.baobaotao.*(..))
匹配com.baobaotao包下所有类的所有方法;
execution(* com.baobaotao..*(..))
匹配com.baobaotao包、子孙包下所有类的所有方法,如com.baobaotao.dao,com.baobaotao.servier以及com.baobaotao.dao.user包下的所有类的所有方法都匹配。“..”出现在类名中时,后面必须跟“*”,表示包、子孙包下的所有类;
execution(* com..*.*Dao.find*(..))
匹配包名前缀为com的任何包下类名后缀为Dao的方法,方法名必须以find为前缀。如com.baobaotao.UserDao#findByUserId()、com.baobaotao.dao.ForumDao#findById()的方法都匹配切点。
4)通过方法入参定义切点
切点表达式中方法入参部分比较复杂,可以使用“*”和“ ..”通配符,其中“*”表示任意类型的参数,而“..”表示任意类型参数且参数个数不限。
execution(* joke(String,int)))
匹配joke(String,int)方法,且joke()方法的第一个入参是String,第二个入参是int。它匹配NaughtyWaiter#joke(String,int)方法。如果方法中的入参类型是java.lang包下的类,可以直接使用类名,否则必须使用全限定类名,如joke(java.util.List,int);
execution(* joke(String,*)))
匹配目标类中的joke()方法,该方法第一个入参为String,第二个入参可以是任意类型,如joke(String s1,String s2)和joke(String s1,double d2)都匹配,但joke(String s1,double d2,String s3)则不匹配;
execution(* joke(String,..)))
匹配目标类中的joke()方法,该方法第一个入参为String,后面可以有任意个入参且入参类型不限,如joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都匹配。
execution(* joke(Object+)))
匹配目标类中的joke()方法,方法拥有一个入参,且入参是Object类型或该类的子类。 它匹配joke(String s1)和joke(Client c)。如果我们定义的切点是execution(* joke(Object)),则只匹配joke(Object object)而不匹配joke(String cc)或joke(Client c)。
args()和@args()
args()函数的入参是类名,@args()函数的入参必须是注解类的类名。虽然args()允许在类名后使用+通配符后缀,但该通配符在此处没有意义:添加和不添加效果都一样。
1)args()
该函数接受一个类名,表示目标类方法入参对象按类型匹配于指定类时,切点匹配,如下面的例子:
args(com.baobaotao.Waiter)
表示运行时入参是Waiter类型的方法,它和execution(* *(com.baobaotao.Waiter))区别在于后者是针对类方法的签名而言的,而前者则针对运行时的入参类型而言。如args(com.baobaotao.Waiter)既匹配于addWaiter(Waiter waiter),也匹配于addNaiveWaiter(NaiveWaiter naiveWaiter),而execution(* *(com.baobaotao.Waiter))只匹配addWaiter(Waiter waiter)方法;实际上,args(com.baobaotao.Waiter)等价于execution(* *(com.baobaotao.Waiter+)),当然也等价于args(com.baobaotao.Waiter+)。
2)@args()
该函数接受一个注解类的类名,当方法的运行时入参对象标注发指定的注解时,方法匹配切点。这个切点函数的匹配规则不太容易理解,我们通过以下示意图对此进行详细讲解:
图 4 @arg(M)匹配示意图(1)
T0、T1、T2、T3具有如图所示的继承关系,假设目标类方法的签名为fun(T1 t),它的入参为T1,而切面的切点定义为@args(M),T2类标注了@M。当fun(T1 t)传入对象是T2或T3时,则方法匹配@args(M)所声明定义的切点;
再看下面的情况,假设方法签名是fun(T1 t),入参对于T1,而标注@M的类是T0,当funt(T1 t)传入T1、T2、T3的实例时,均不匹配切点@args(M)。
图 5 @arg(M)匹配示意图(2)
在类的继承树中,①点为方法签名中入参类型在类继承树中的位置,我们称之为入参类型点,而②为标注了@M注解的类在类继承树中位置,我们称之为注解点。判断方法在运行时是否匹配@agrs(M)切点,可以根据①点和②点在类继承树中的相对位置来判别:
1) 如果在类继承树中注解点②高于入参类型点①,则该目标方法不可能匹配切点@args(M),如图 5所示;
2) 如果在类继承树中注解点②低于入参类型点①,则注解点所在类及其子孙类作为方法入参时,该方法匹配@args(M)切点,如图 4所示。
下面举一个具体的例子,假设我们定义这样的切点:@args(com.baobaotao.Monitorable) ,如果NaiveWaiter标注了@Monitorable,则对于WaiterManager#addWaiter(Waiter w)方法来说,如果入参是NaiveWaiter或其子类对象,该方法匹配切点,如果入参是NaughtyWaiter对象,不匹配切点。如果Waiter标注了@Monitorable,但NaiveWaiter未标注@Monitorable,则WaiterManager#addNaiveWaiter(NaiveWaiter w)却不匹配切点,这是因为注解点(在Waiter)高于入参类型点(NaiveWaiter)。
within()
通过类匹配模式串声明切点,within()函数定义的连接点是针对目标类而言,而非针对运行期对象的类型而言,这一点和execetion()是相同的。但和execution()函数不同的是,within()所指定的连接点最小范围只能是类,而execution()所指定的连接点,可以大到包,小到方法入参。所以从某种意义上说,execution()函数的功能涵盖了within()函数的功能。within()函数的语法如下所示:
形如within(com.baobaotao.NaiveWaiter)是within()函数所能表达的最小粒度,如果试图用within()匹配方法级别的连接点,如within(com.baobaotao.NaiveWaiter.greet*)将会产生解析错误。
下面是一些使用within()函数的实例:
within(com.baobaotao.NaiveWaiter)
匹配目标类NaiveWaiter的所有方法。如果切点调整为within(com.baobaotao.Waiter),则NaiveWaiter和NaughtyWaiter中的所有方法都不匹配,而Waiter本身是接口不可能实例化,所以within(com.baobaotao.Waiter)的声明是无意义的;
within(com.baobaotao.*)
匹配com.baobaotao包中的所有类,但不包括子孙包,所以com.baobaotao.service包中类的方法不匹配这个切点;
within(com.baobaotao..*)
匹配com.baobaotao包及子孙包中的类,所以com.baobaotao.service、com.baobaotao.dao以及com.baobaotao.service.fourm等包中所有类的方法都匹配这个切点。
@within()和@target()
除@annotation()和@args()外,还有另外两个用于注解的切点函数,它们分别是@target()和@within(),和@annotation()及@args()函数一样,它们也只接受注解类名作为入参。其中@target(M)匹配任意标注了@M的目标类,而@within(M)匹配标注了@M的类及子孙类。
@target(M)切点的匹配规则如图 6所示:
图 6 @target(M)匹配目标类示意图
假设NaiveWaiter标注了@Monitorable,则其子类CuteNaiveWaiter没有标注@Monitorable,则@target(com.baobaotao.Monitorable)匹配NaiveWaiter类的所有方法,但不匹配CuteNaiveWaiter类的方法。
@within(M)切点的匹配规则如图 7所示:
图 7 @within(M)匹配目标类示意图
假设NaiveWaiter标注了@Monitorable,而其子类CuteNaiveWaiter没有标注@Monitorable,则@within(com.baobaotao.Monitorable)不但匹配NaiveWaiter类中的所有方法也匹配CuteNaiveWaiter类中的所有方法。
但有一个特别值得注意地方是,如果标注@M注解的是一个接口,则所有实现该接口的类并不匹配@within(M)。假设Waiter标注了@Monitorable注解,但NaiveWaiter、NaughtyWaiter及CuteNaiveWaiter这些接口实现类都没有标注@Monitorable,则@within(com.baobaotao.Monitorable)和@target(com.baobaotao.Monitorable)都不匹配NaiveWaiter、NaughtyWaiter及CuteNaiveWaiter。这是因为@within()、@target()以及@annotation()都是针对目标类而言,而非针对运行时的引用类型而言,这点区别需要在开发中特别注意。
target()的this()
target()切点函数通过判断目标类是否按类型匹配指定类决定连接点是否匹配,而this()则通过判断代理类是否按类型匹配指定类来决定是否和切点匹配。两者都仅接受类名的入参,虽然类名可以带“+”通配符,但对于这两个函数来说,使用与不使用+通配符,效果完全相同。
1)target()
target(M)表示如果目标类按类型匹配于M,则目标类所有方法匹配切点,我们通过一些例子理解target(M)的匹配规则:
target(com.baobaotao.Waiter)
NaiveWaiter、NaughtyWaiter以及CuteNaiveWaiter的所有方法都匹配切点,包括那些未在Waiter接口中定义的方法,如NaiveWaiter#simle()和NaughtyWaiter#joke()方法。
target(com.baobaotao.Waiter+)
和target(com.baobaotao.Waiter)是等价的。
2)this()
根据Spring的官方文档,this()函数判断代理对象的类是否按类型匹配于指定类,如果匹配,则代理对象的所有连接点匹配切点。但通过实验,我们发现实际情况和文档有出入,如我们声明一个this(com.baobaotao.NaiveWaiter)的切点,如果不使用CGLib代理,则生成的代理对象是Waiter类型,而非NaiveWaiter类型,这一点可以简单地通过instanceof操作符进行判断。但是,我们发现NaiveWaiter中所有的方法还是被织入了增强。
在一般情况下,使用this()和target()通过定义切点,两者是等效的:
1) target(com.baobaotao.Waiter) 等价于this(com.baobaotao.Waiter)
2) target(com.baobaotao.NaiveWaiter) 等价于 this(com.baobaotao.NaiveWaiter)
两者区别体现在通过引介切面产生的代理对象时的具体表现,如果我们通过本文前面的方法为NaiveWaiter引介一个Seller接口的实现,则this(com.baobaotao.Seller)匹配NaiveWaiter代理对象的所有方法,包括NaiverWaiter本身的greetTo()、serverTo()方法以及通过Seller接口引入的sell()方法。而target(com.baobaotao.Seller)不匹配通过引介切面产生的NaiveWaiter代理对象。
下面通过具体的实例来了解这一微妙的区别,EnableSellerAspect是为NaiveWaiter添加Seller接口实现的引介切面:
…
@Aspect
publicclass EnableSellerAspect{
@DeclareParents(value ="com.baobaotao.NaiveWaiter",
defaultImpl = SmartSeller.class)
publicstatic Seller seller;
}
TestAspect是通过判断运行期代理对象所属类型来定义切点的切面:
代码清单 6 TestAspect:通过this()指定切点
…
@Aspect
publicclass TestAspect {
@AfterReturning("this(com.baobaotao.Seller)") ①后置增强,织入到任何运行期对象为
Seller 类型的Bean中
publicvoid thisTest(){
System.out.println("thisTest() executed!");
}
}
在Spring中配置这两个切面和NaiveWaiter:
<aop:aspectj-autoproxy/>
<bean id="naiveWaiter" class="com.baobaotao.NaiveWaiter" />
<bean class="com.baobaotao.aspectj.fun.EnableSellerAspect"/>
<bean class="com.baobaotao.aspectj.fun.TestAspect" />
首先EnableSellerAspect切面为NaiveWaiter引介Seller接口产生一个实现Seller接口的代理对象,TestAspect在判断出NaiveWaiter这个代理对象实现Seller接口后,就将其切面织入到这个代理对象中,所以最终NaiveWaiter的代理对象其实共织入了两个切面。
运行以下的测试代码:
ApplicationContext ctx =new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
naiveWaiter.serveTo("John");
((Seller)naiveWaiter).sell("Beer", "John");
输出以下的代码:
thisTest() executed!
NaiveWaiter:serving John...
thisTest() executed!
SmartSeller: sell Beer to John...
thisTest() executed!
可见代理对象的3个方法都织入了代码清单 6通过this()所定义切面。
小结
使用@AspectJ定义切面比基于接口定义的切面更加直观、更加简洁,成为Spring所推荐的切面定义方式。掌握切点表达式语法和切点函数是学习@AspectJ的重心,我们分别对9个切点函数进行了详细的讲述。切点表达式非常灵活,拥有强大的切点表达能力,你可以使用通配符、切点函数以及切点运算符定义切点。
Spring 2.0在AOP上花费了很大的功夫,相比于低版本的Spring,我们看到了很大的改进。在掌握低版本Spring AOP相关知识的基础上,你会发现学习Spring 2.0 基于@AspectJ AOP的新知识不会有太多的门槛。
一个简单的例子
在做好上节中所提到的前置工作后,我们就可以开始编写一个基于@AspectJ的切面了,首先来看一个简单的例子,以便对@AspectJ有一个切身的认识。
下面,我们用@AspectJ注解对一个POJO进行标注,将使其成为一个切面类:
代码清单 1 PreGreetingAspect:切面
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect ①通过该注解将PreGreetingAspect标识为一个切面
public class PreGreetingAspect{
@Before("execution(* greetTo(..))") ②定义切点和增强类型
public void beforeGreeting(){③增强的横切逻辑
System.out.println("How are you");
}
}
我们“惊奇”地发现这个切面没有实现任何特殊的接口,它只是一个普通的POJO。它特殊的地方在于使用了@AspectJ注解。
首先,在PreGreetingAspect类定义处,标注了一个@Aspectj注解,第三方处理程序就可以通过类是否拥有@Aspectj注解判断其是否是一个切面,如①所示。
其次,在beforeGreeting()方法标签处,标注了@Before注解,并为该注解提供了成员值"execution(* greetTo(..))",如②所示。②处的注解提供了两个信息:@Before注解表示该增强是前置增强,而成员值通过@ApsectJ切点表达式语法定义切点:即在目标类的greetTo()方法上织入增强,greetTo()方法可以带任意的入参和任意的返回值。
最后,在③处的beforeGreeting()方法是增强的横切逻辑,该横切逻辑在目标方法前调用,我们通过下图描述这种关系:
图 1 切面的信息构成
PreGreetingAspect类通过注解和代码,将切点、增强类型和增强的横切逻辑揉合到一个类中,使切面的定义浑然天成。如果在低版本Spring AOP中,你必须同时创建增强类,切点类以及切面类,并使三者联合表达相同的信息。
NaiveWaiter是一个Bean,它拥有一个greetTo()的方法,这个方法连接点匹配于上面我们通过@AspectJ所定义的切点,为了方便后续的说明,我们给出NaiveWaiter的代码:
public class NaiveWaiter implements Waiter {
public void greetTo(String clientName) {
System.out.println("NaiveWaiter:greet to "+clientName+"...");
}
public void serveTo(String clientName){
System.out.println("NaiveWaiter:serving "+clientName+"...");
}
}
下面,我们通过org.springframework.aop.aspectj.annotation.AspectJProxyFactory为NaiveWaiter生成织入PreGreetingAspect切面的代理,如代码清单 2所示:
代码清单 2 AspectJProxyTest
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import com.baobaotao.NaiveWaiter;
import com.baobaotao.Waiter;
publicclass AspectJProxyTest {
Waiter target =new NaiveWaiter();
AspectJProxyFactory factory =new AspectJProxyFactory();
factory.setTarget(target); ① 设置目标对象
factory.addAspect(PreGreetingAspect.class); ②添加切面类
Waiter proxy = factory.getProxy(); ③ 生成织入切面的代理对象
proxy.greetTo("John");
proxy.serveTo("John");
}
}
Spring使用AspectJProxyFactory织入基于@AspectJ切面的工作。在①处,设置了目标对象,在②处添加一个切面类,该类必须是带@AspectJ注解的类,在③处,我们就可以获取织入切面的代理对象了。
接下来,我们直接通过代理对象调用greetTo()和serveTo()代码,它们产生以下的输出信息:
How are you ①表示greetTo()方法被成功地织入了切面
greet to John...
serving John...
通过①处的输出信息我们可以知道代理对象的greetTo()方法已经织入了切面类所定义的增强逻辑了。