概述
【IT168 专稿】在《基于@AspectJ配置 Spring AOP(之一)》的文章中,我们讲解基于@AspectJ Spring AOP的基础知识,在本文中,我们将继续学习@AspectJ一些高级的知识。@AspectJ可以使用逻辑运算符对切点进行复合运算得到复合的切点;为 了在切面中重用切点,我们还可以对切点进行命名,以便在其它的地方引用定义过的切点;当一个连接点匹配多个切点时,需要考虑织入顺序的问题;此外,一个重 要的问题是如何在增强中访问连接点上下文的信息。
切点复合运算
使用切点复合运算符,我们将拥有强大而灵活的切点表达能力,以下是一个使用了复合切点的切面:
代码清单 7 TestAspect:切点复合运算
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public
class
TestAspect
...
{
@After("within(com.baobaotao.*) "
+ " && execution(* greetTo(..)))") ①与运算
public void greeToFun() ...{
System.out.println("--greeToFun() executed!--");
}
@Before(" !target(com.baobaotao.NaiveWaiter) "+
"&& execution(* serveTo(..)))") ②非与运算
public void notServeInNaiveWaiter() ...{
System.out.println("--notServeInNaiveWaiter() executed!--");
}
@AfterReturning("target(com.baobaotao.Waiter) || "+
" target(com.baobaotao.Seller)") ③或运算
public void waiterOrSeller()...{
System.out.println("--waiterOrSeller() executed!--");
}
}
在①处,我们通过&&运算符定义了一个匹配com.baobaotao包中所有greetTo方法的切点;在②处,我们通过! 和&&运算符定义了一个匹配所有serveTo()方法并且该方法不位于NaiveWaiter目标类的切点;在③处,我们通过||运算 符定义了一个匹配Waiter和Seller接口实现类所有连接点的切点。
命名切点
在前面所举的例子中,切点直接声明在增强方法处,这种切点声明方式称为匿名切点,匿名切点只能在声明处使用。如果希望在其它地方重用一个切点,我们可以通过@Pointcut注解以及切面类方法对切点进行命名,以下是一个具体的实例:
代码清单 8 TestNamePointcut
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Pointcut;
public
class
TestNamePointcut
...
{
@Pointcut("within(com.baobaotao.*)") ①通过注解方法inPackage()对该切点进行命名,方法可视域
修饰符为private,表明该命名切点只能在本切面类中使用。
private void inPackage()...{}
@Pointcut("execution(* greetTo(..)))") ②通过注解方法greetTo()对该切点进行命名,方法可视域
修饰符为protected,表明该命名切点可以在当前包中的切面
类、子切面类中中使用。
protected void greetTo(){}
@Pointcut("inPackage() and greetTo()") ③引用命名切点定义的切点,本切点也是命名切点,
它对应的可视域为public
public
void
inPkgGreetTo()
...
{}
}
我们在代码清单 8中定义了3个命名切点,命名切点的使用类方法作为切点的名称,此外方法的访问修饰符还控制了切点的可引用性,这种可引用性和类方法的可访问性相同,如 private的切点只能在本类中引用,public的切点可以在任何类中引用。命名切点仅利用方法名及访问修饰符的信息,所以习惯上,方法的返回类型为 void,并且方法体为空。我们可以通过下图更直观地了解命名切点的结构:
图 8 命名切点结构
在③处,inPkgGreetTo()的切点引用了同类中的greetTo()切点,而inPkgGreetTo()切点可以被任何类引用。你还可以扩展TestNamePointcut类,通过类的继承关系定义更多的切点。
命名切点定义好后,就可以在定义切面时通过名称引用切点,请看下面的实例:
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public
class
TestAspect
...
{
@Before("TestNamePointcut.inPkgGreetTo()") ①
public void pkgGreetTo()...{
System.out.println("--pkgGreetTo() executed!--");
}
@Before("!target(com.baobaotao.NaiveWaiter) && TestNamePointcut.inPkgGreetTo()") ②
public void pkgGreetToNotNaiveWaiter()...{
System.out.println("--pkgGreetToNotNaiveWaiter() executed!--");
}
}
在①处,我们引用了TestNamePointcut.inPkgGreetTo()切点,而在②处,我们在复合运算中使用了命名切点。
增强织入的顺序
一个连接点可以同时匹配多个切点,切点对应的增强在连接点上的织入顺序到底是如何安排呢?这个问题需要分三种情况讨论:
如果增强在同一个切面类中声明,则依照增强在切面类中定义的顺序进行织入;
如果增强位于不同的切面类中,且这些切面类都实现了org.springframework.core.Ordered接口,则由接口方法的顺序号决定(顺序号小的先织入);
如果增强位于不同的切面类中,且这些切面类没有实现org.springframework.core.Ordered接口,织入的顺序是不确定的。
我们可以通过下图描述这种织入的规则:
图 9 增强织入顺序
切面类A和B都实现为Ordered接口,A切面类对应序号为1,B切面类对应序号为2,A切面类按顺序定义了3个增强,B切面类按顺序定义两个增强,这5个增强对应的切点都匹配某个目标类的连接点,则增强织入的顺序为图中虚线所示。
访问连接点信息
AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用 org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以 通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法:
1)JoinPoint
java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
Signature getSignature() :获取连接点的方法签名对象;
java.lang.Object getTarget() :获取连接点所在的目标对象;
java.lang.Object getThis() :获取代理对象本身;
2)ProceedingJoinPoint
ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法:
java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法;
java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。
让我们来看一个具体的实例:
代码清单 9 TestAspect:访问连接点对象
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public
class
TestAspect
...
{
@Around("execution(* greetTo(..)) && target(com.baobaotao.NaiveWaiter)") ①环绕增强
public void joinPointAccess(ProceedingJoinPoint pjp) throws Throwable...{ ②声明连接点入参
System.out.println("------joinPointAccess-------");
③ 以下两行访问连接点信息
System.out.println("args[0]:"+pjp.getArgs()[0]);
System.out.println("signature:"+pjp.getTarget().getClass());
pjp.proceed(); ④ 通过连接点执行目标对象方法
System.out.println("-------joinPointAccess-------");
}
}
在①处,我们声明了一个环绕增强,在②处增强方法的第一个入参声明为PreceedingJoinPoint类型(注意一定要在第一个位置),在③处,我们通过连接点对象pjp访问连接点的信息。在④处,我们通过连接点调用目标对象的方法。
执行以下的测试代码:
String configPath
=
"
com/baobaotao/aspectj/advanced/beans.xml
"
;
ApplicationContext ctx
=
new
ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter
=
(Waiter) ctx.getBean(
"
naiveWaiter
"
);
naiveWaiter.greetTo(
"
John
"
);
输出以下的信息:
------joinPointAccess-------
args[0]:John
signature:class com.baobaotao.NaiveWaiter
NaiveWaiter:greet to John... ①对应pjp.proceed();
-------joinPointAccess-------
绑定连接点方法入参
我们介绍切点函数时说过args()、this()、target()、@args()、@within()、@target()和@annotation()这7个函数除了可以指定类名外,还可以指定参数名将目标对象连接点上的方法入参绑定到增强的方法中。
其中args()用于绑定连接点方法的入参,@annotation()用于绑定连接点方法的注解对象,而@args()用于绑定连接点方法入参的注解。来看一个args()绑定参数的实例:
代码清单 10 TestAspect:绑定连接点参数
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class TestAspect {
①绑定连接点参数,首先args(name,num,..)根据②处的增强方法入参找到name和num对应的
类型,以得到真实的切点表达式:target(com.baobaotao.NaiveWaiter) && args(String,int,..)
在该增强方法织入到目标连接点时,增强方法可以通过num和name访问到连接点方法的入参。
@Before("target(com.baobaotao.NaiveWaiter) && args(name,num,..)")
public void bindJoinPointParams(int num,String name){②增强方法接受连接点的参数
System.out.println("----bindJoinPointParams()----");
System.out.println("name:"+name);
System.out.println("num:"+num);
System.out.println("----bindJoinPointParams()----");
}
}
在①处,我们通过args(name,num,..)进行连接点参数的绑定,和前面我们所讲述的方式不一样,当args()函数入参为参数名时,共包括两方面的信息:
1) 连接点匹配规则信息:连接点方法第一个入参是String类型,第二个入参是int类型;
2) 连接点方法入参和增强方法入参的绑定信息:连接点方法的第一个入参绑定到增强方法的name参数上,第二个入参绑定到增强方法的num入参上。
切 点匹配和参数绑定的过程是这样的:首先args()根据参数名称在增强方法中查到名称相同的入参并获知对应的类型,这样就知道匹配连接点方法的入参类型。 其次连接点方法入参类型所在的位置则由参数名在args()函数中声明的位置决定。代码清单 10中的args(name,num)只匹配第一个入参是String第二个入参是int的目标类方法,如smile(String name,int times)而不匹配smile(int times ,String anme)。我们可以通过以下示意图详细了解这一有趣的匹配过程:
图 11 绑定参数和切点匹配过程
和args()一样,其它可以绑定连接点参数的切点函数(如@args()和target()等),当指定参数名时,就同时具有匹配切点和绑定参数的双重功能。
运行下面的测试:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
NaiveWaiter naiveWaiter = (NaiveWaiter) ctx.getBean("naiveWaiter");
naiveWaiter.smile("John",2);
我们将看到以下的输出信息:
----bindJoinPointParams()----
name:John
num:2
----bindJoinPointParams()----
NaiveWaiter:smile to John2times...
可见,增强方法按预期绑定了NaiveWaiter.smile(String name,int times)方法的运行期入参。
提 示 为了保证实例能成功执行,必须启用CGLib动态代理:
,因为该实例需要对NaiveWaiter类进行代理(因为NaiveWaiter#simle()方法不是Waiter接口的方法),所以必 须使用CGLib生成子类的代理方法。
我们知道方法的入参名无法通过反射机制获取,所以Spring按以下方式
1
绑定代理对象
使用this()或target()可绑定被代理对象实例,在通过类实例名绑定对象时,还依然具有原来连接点匹配的功能,只不过类名是通过增强方法中同名入参的类型间接决定罢了。这里我们通过this()来了解对象绑定的用法:
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.baobaotao.Waiter;
@Aspect
public class TestAspect {
@Before("this(waiter)") ①通过②处查找出waiter对应的类型为Waiter,因而切点表达式
为this(Waiter),当增强方法织入目标连接点时,增强方法通过waiter
入参可以引用到代理对象的实例。
public void bindProxyObj(Waiter waiter){ ②
System.out.println("----bindProxyObj()----");
System.out.println(waiter.getClass().getName());
System.out.println("----bindProxyObj()----");
}
}
① 处的切点表达式首先按类变量名查找②处增强方法的入参列表,进而获取类变量名对应的类为com.baobaotao.Waiter,这样就知道了切点的定 义为this(com.baobaotao.Waiter),即所有代理对象为Waiter类的所有方法匹配该切点。②处的增强方法通过waiter入参 绑定目标对象。
可见NaiveWaiter的所有方法匹配①处的切点,运行以下的测试代码:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
可以看到如下的输出信息:
----bindProxyObj()----
com.baobaotao.NaiveWaiter$$EnhancerByCGLIB$$6758891b
----bindProxyObj()----
NaiveWaiter:greet to John...
以按相似的方法使用target()进行绑定。
绑定类注解对象
@within()和@target()函数可以将目标类的注解对象绑定到增强方法中,我们通过@within()演示注解绑定的操作:
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.baobaotao.Monitorable;
@Aspect
public class TestAspect {
@Before("@within(m)") ①通过②处查找出m对应Monitorable类型的注解,
因而真实的切点表达式为@within (Monitorable),当增强方法织入目标
连接点时,增强方法通过m入参可以引用到连接点处的注解对象。
public void bindTypeAnnoObject(Monitorable m){ ②
System.out.println("----bindTypeAnnoObject()----");
System.out.println(m.getClass().getName());
System.out.println("----bindTypeAnnoObject()----");
}
}
NaiveWaiter类中标注了@Monitorable注解,所有NaiveWaiter Bean匹配切点,其Monitorable注解对象将绑定到增强方法中。运行以下代码,我们即可以查看到绑定注解对象:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
((NaiveWaiter)naiveWaiter).greetTo("John");
运行以上代码,输出以下信息:
----bindTypeAnnoObject()----
$Proxy3
----bindTypeAnnoObject()----
NaiveWaiter:greet to John...
从输出信息中,我们还发现了一个秘密,即使用CGLib代理NaiveWaiter时,其类的注解Monitorable对象也被代理了。
绑定返回值
在后置增强中,可以通过returning绑定连接点方法的返回值:
@AfterReturning(value
=
"
target(com.baobaotao.SmartSeller)
"
,returning
=
"
retVal
"
) ①
public
void
bingReturnValue(
int
retVal)
...
{ ②
System.out.println("----bindException()----");
System.out.println("returnValue:"+retVal);
System.out.println("----bindException()----");
}
①处和②处的名字必须相同,此外②处retVal的类型必须和连接点方法的返回值类型匹配。运行下面的测试代码:
String configPath
=
"
com/baobaotao/aspectj/advanced/beans.xml
"
;
ApplicationContext ctx
=
new
ClassPathXmlApplicationContext(configPath);
SmartSeller seller
=
(SmartSeller) ctx.getBean(
"
seller
"
);
seller.sell(
"
Beer
"
,
"
John
"
);
可以看到以下的输出信息:
SmartSeller: sell Beer to John...
----bingReturnValue()----
returnValue:100
----bingReturnValue()----
可见目标连接点Seller#sell()方法所返回的入参被成功绑定到增强方法中。
绑定抛出的异常
和通过切点函数绑定连接点信息不同,连接点抛出的异常必须使用AfterThrowing注解的throwing成员进行绑定:
代码清单 11 TestAspect:绑定异常对象
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public
class
TestAspect
...
{
@AfterThrowing(value="target(com.baobaotao.SmartSeller)",throwing="iae") ①
public void bindException(IllegalArgumentException iae)...{ ②
System.out.println("----bindException()----");
System.out.println("exception:"+iae.getMessage());
System.out.println("----bindException()----");
}
}
①处throwing指定的异常名和②处入参的异常名相同,这个异常增强只在连接点抛出的异常instanceof IllegalArgumentException才匹配,增强方法通过iae参数可以访问抛出的异常对象。
我们在SmartSeller中添加一个抛出异常的测试方法:
package com.baobaotao;
public
class
SmartSeller implements Seller
...
{
public void checkBill(int billId)...{
if(billId = = 1) throw new IllegalArgumentException("iae Exception");
else throw new RuntimeException("re Exception");
}
}
当billId为1时抛出IllegalArgumentException,否则抛出RuntimeException。运行以下测试代码:
String configPath
=
"
com/baobaotao/aspectj/advanced/beans.xml
"
;
ApplicationContext ctx
=
new
ClassPathXmlApplicationContext(configPath);
SmartSeller seller
=
(SmartSeller) ctx.getBean(
"
seller
"
);
seller.checkBill(
1
); ① 运行该方法将引发IllegalArgumentException
我们将看到以下的输出信息:
----bindException()----
exception:iae Exception
----bindException()----
Exception in thread "main" java.lang.IllegalArgumentException: iae Exception
…
可见当seller.checkBill(1)抛出异常后,异常增强起效,处理完成后,再向外抛出IllegalArgumentException。如 果将①处的代码调整为seller.checkBill(2)后,再运行代码,将只看到异常输出的信息,异常增强没有任何动作,这是因为 RuntimeException 不按类型匹配于 IllegalArgumentException,切点不匹配。
小结
通过切点复合运算,你可以定义出各种复杂的切点,使切点表达式的能力进一步提升。你可以直接使用切点复合运算符对切点函数进行运算,也可以通过切点名引用 其它命名切点。当对同一个连接点织入多个增强时,你必须考虑让切面类实现Ordered接口,此外还必须合理计划同一个切面类中增强方法的声明顺序,因为 这些信息都会影响到增强的织入顺序。
在@AspectJ的切点表达式中,大多数的切点函数都可以绑定连接点方法的入参,以便增强方法访问连接点信息。此外,你也可以简单地将增强方法的第一个入参定义为JoinPoint访问连接点的上下文。