一、在Advice方法中获取目标方法的参数
1、获取目标方法的信息
访问目标方法最简单的做法是定义增强处理方法时,将第一个参数定义为JoinPoint类型,当该增强处理方法被调用时,该JoinPoint参数就代表了织入增强处理的连接点。JoinPoint里包含了如下几个常用的方法:
Object[] getArgs:返回目标方法的参数
Signature getSignature:返回目标方法的签名
Object getTarget:返回被织入增强处理的目标对象
Object getThis:返回AOP框架为目标对象生成的代理对象
注意:当使用@Around处理时,我们需要将第一个参数定义为ProceedingJoinPoint类型,该类是JoinPoint的子类。
下面的切面类(依然放在com.abc.advice包中)中定义了Before、Around、AfterReturning和After 4中增强处理,并分别在4种增强处理中访问被织入增强处理的目标方法、目标方法的参数和被织入增强处理的目标对象等:
package com.abc.advice;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class AdviceTest {
@Around("execution(* com.abc.service.*.many*(..))")
public Object process(ProceedingJoinPoint point) throws Throwable {
System.out.println("@Around:执行目标方法之前...");
//访问目标方法的参数:
Object[] args = point.getArgs();
if (args != null && args.length > 0 && args[0].getClass() == String.class) {
args[0] = "改变后的参数1";
}
//用改变后的参数执行目标方法
Object returnValue = point.proceed(args);
System.out.println("@Around:执行目标方法之后...");
System.out.println("@Around:被织入的目标对象为:" + point.getTarget());
return "原返回值:" + returnValue + ",这是返回结果的后缀";
}
@Before("execution(* com.abc.service.*.many*(..))")
public void permissionCheck(JoinPoint point) {
System.out.println("@Before:模拟权限检查...");
System.out.println("@Before:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@Before:参数为:" + Arrays.toString(point.getArgs()));
System.out.println("@Before:被织入的目标对象为:" + point.getTarget());
}
@AfterReturning(pointcut="execution(* com.abc.service.*.many*(..))",
returning="returnValue")
public void log(JoinPoint point, Object returnValue) {
System.out.println("@AfterReturning:模拟日志记录功能...");
System.out.println("@AfterReturning:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@AfterReturning:参数为:" +
Arrays.toString(point.getArgs()));
System.out.println("@AfterReturning:返回值为:" + returnValue);
System.out.println("@AfterReturning:被织入的目标对象为:" + point.getTarget());
}
@After("execution(* com.abc.service.*.many*(..))")
public void releaseResource(JoinPoint point) {
System.out.println("@After:模拟释放资源...");
System.out.println("@After:目标方法为:" +
point.getSignature().getDeclaringTypeName() +
"." + point.getSignature().getName());
System.out.println("@After:参数为:" + Arrays.toString(point.getArgs()));
System.out.println("@After:被织入的目标对象为:" + point.getTarget());
}
}
在AdviceManager类中增加以下内容:
//将被AdviceTest的各种方法匹配
public String manyAdvices(String param1, String param2) {
System.out.println("方法:manyAdvices");
return param1 + " 、" + param2;
}
在com.abc.main.AOPTest中加入方法的调用,触发切点:
String result = manager.manyAdvices("aa", "bb");
System.out.println("Test方法中调用切点方法的返回值:" + result);
下面是执行结果:
@Around:执行目标方法之前...
@Before:模拟权限检查...
@Before:目标方法为:com.abc.service.AdviceManager.manyAdvices
@Before:参数为:[改变后的参数1, bb]
@Before:被织入的目标对象为:com.abc.service.AdviceManager@1dfc617e
方法:manyAdvices
@Around:执行目标方法之后...
@Around:被织入的目标对象为:com.abc.service.AdviceManager@1dfc617e
@After:模拟释放资源...
@After:目标方法为:com.abc.service.AdviceManager.manyAdvices
@After:参数为:[改变后的参数1, bb]
@After:被织入的目标对象为:com.abc.service.AdviceManager@1dfc617e
@AfterReturning:模拟日志记录功能...
@AfterReturning:目标方法为:com.abc.service.AdviceManager.manyAdvices
@AfterReturning:参数为:[改变后的参数1, bb]
@AfterReturning:返回值为:原返回值:改变后的参数1 、 bb,这是返回结果的后缀
@AfterReturning:被织入的目标对象为:com.abc.service.AdviceManager@1dfc617e
Test方法中调用切点方法的返回值:原返回值:改变后的参数1 、bb,这是返回结果的后缀
从结果中可以看出:在任何一个织入的增强处理中,都可以获取目标方法的信息。另外,Spring AOP采用和AspectJ一样的有限顺序来织入增强处理:在“进入”连接点时,最高优先级的增强处理将先被织入(所以给定的两个Before增强处理中,优先级高的那个会先执行);在“退出”连接点时,最高优先级的增强处理会最后被织入(所以给定的两个After增强处理中,优先级高的那个会后执行)。当不同的切面中的多个增强处理需要在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这些增强处理。如果应用需要指定不同切面类里的增强处理的优先级,Spring提供了如下两种解决方案:
让切面类实现org.springframework.core.Ordered接口:实现该接口只需要实现一个int getOrder()方法,该方法返回值越小,优先级越高
直接使用@Order注解来修饰一个切面类:使用这个注解时可以配置一个int类型的value属性,该属性值越小,优先级越高
优先级高的切面类里的增强处理的优先级总是比优先级低的切面类中的增强处理的优先级高。例如:优先级为1的切面类Bean1包含了@Before,优先级为2的切面类Bean2包含了@Around,虽然@Around优先级高于@Before,但由于Bean1的优先级高于Bean2的优先级,因此Bean1中的@Before先被织入。
同一个切面类里的两个相同类型的增强处理在同一个连接点被织入时,Spring AOP将以随机的顺序来织入这两个增强处理,没有办法指定它们的织入顺序。如果确实需要保证它们以固有的顺序被织入,则可以考虑将多个增强处理压缩为一个增强处理;或者将不同增强处理重构到不同切面中,通过在切面级别上定义顺序。
如果只要访问目标方法的参数,Spring还提供了一种更加简洁的方法:我们可以在程序中使用args来绑定目标方法的参数。如果在一个args表达式中指定了一个或多个参数,该切入点将只匹配具有对应形参的方法,且目标方法的参数值将被传入增强处理方法。下面辅以例子说明:
package com.abc.advice;
import java.util.Date;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AccessArgAdviceTest {
@AfterReturning(
pointcut="execution(* com.abc.service.*.access*(..)) && args(time, name)",
returning="returnValue")
public void access(Date time, Object returnValue, String name) {
System.out.println("目标方法中的参数String = " + name);
System.out.println("目标方法中的参数Date = " + time);
System.out.println("目标方法的返回结果returnValue = " + returnValue);
}
}
上面的程序中,定义pointcut时,表达式中增加了args(time, name)部分,意味着可以在增强处理方法(access方法)中定义time和name两个属性——这两个形参的类型可以随意指定,但一旦指定了这两个参数的类型,则这两个形参类型将用于限制该切入点只匹配第一个参数类型为Date,第二个参数类型为name的方法(方法参数个数和类型若有不同均不匹配)。
注意,在定义returning的时候,这个值(即上面的returning=”returnValue”中的returnValue)作为增强处理方法的形参时,位置可以随意,即:如果上面access方法的签名可以为
public void access(Date time, Object returnValue, String name)
也可以为
public void access(Object returnValue, Date time, String name)
还可以为
public void access(Date time, String name, Object returnValue)
只需要满足另外的参数名的顺序和pointcut中args(param1, param2)的顺序相同即可。我们在AdviceManager中定义一个方法,该方法的第一个参数为Date类型,第二个参数为String类型,该方法的执行将触发上面的access方法,如下:
//将被AccessArgAdviceTest的access方法匹配
public String accessAdvice(Date d, String n) {
System.out.println("方法:accessAdvice");
return "aa";
}
在AOPTest中增加调用这个accessAdvice方法并执行,下面是输出结果:
从执行结果可以看出,使用args表达式有如下两个作用:
提供了一种简单的方式来访问目标方法的参数
可用于对切入点表达式作额外的限制
除此之外,使用args表达式时,还可以使用如下形式:args(param1, param2, ..),注意args参数中后面的两个点,它表示可以匹配更多参数。在例子args(param1, param2, ..)中,表示目标方法只需匹配前面param1和param2的类型即可。
二、定义切入点和切入点指示符
1、定义切入点
在前文中,使用到的AdviceTest类中同一个切点(即* com.abc.service.*.advice*(..)
匹配的连接点)却重复定义了多次,这显然不符合软件设计的原则,为了解决这个问题,AspectJ和Spring都提供了切入点的定义。所谓定义切入点,其实质就是为一个切入点表达式起一个名称,从而允许在多个增强处理中重用该名称。
Spring AOP只支持以Spring Bean的方法执行组作为连接点,所以可以把切入点看作所有能和切入表达式匹配的Bean方法。切入点定义包含两个部分:
一个切入点表达式:用于指定切入点和哪些方法进行匹配
一个包含名字和任意参数的方法签名:将作为切入点的名称
在@AspectJ风格的AOP中,切入点签名采用一个普通的方法定义(方法体通常为空)来提供(方法名即为切点名),且该方法的返回值必须为void,切入点表达式需使用@Pointcut注解来标注。下面的代码片段定义了一个切入点,这个切入点将匹配任何名为transfer的方法的执行:
//使用@Pointcut注解时指定切入点表达式
@Pointcut("execution(* transfer(..))")
//使用一个返回值为void,方法体为空的方法来命名切入点,方法名即为切点名
private void myPointcut(){}
切入点表达式,也就是组成@Pointcut注解的值,是规范的AspectJ 5切入点表达式。如果想要了解更多的关于AspectJ切入点语言,请参见AspectJ编程指南。
一旦采用上面的代码片段定义了名为myPointcut的切入点之后,程序就可以多次重复使用该切点了,甚至可以在其他切面类、其他包的切面类里使用该切点,至于是否可以在其他切面类、其他包下使用这个切点,那就要看该方法前的访问控制修饰符了——本例中myPointcut使用private修饰,则意味着仅能在当前切面类中使用这个切点。
如果需要使用本切面类中的切点,则可在使用@Pointcut注解时,指定value属性值为已有的切入点,如下:
@AfterReturning(pointcut="myPointcut()", returning="returnValue")
public void log(String message, Object returnValue) {
//do something...
}
从指定pointcut来看,其语法非常类似于Java中调用方法——只是该方法代表一个切点,其实质是为该增强处理方法定义一个切入点表达式。如果需要使用其他类中定义的切点,则定义这些切点的方法的修饰符不能为private。现在假设在另一个类PointcutDefinition中定义了一个名为myPointcutTest的切点:
public class PointcutDefinition {
@Pointcut("execution(* something(..))")
//访问控制符为public,这个切点可以在其他任何地方引用
public void myPointcutTest(){}
}
则在引用的时候需要带上类名,例如:
@AfterReturning(
pointcut="PointcutDefinition.myPointcutTest() && args(message)",
returning="returnValue")
public void log(String message, Object returnValue) {
//do something...
}
2、切入点指示符
前面定义切点表达式时使用了大量的execution表达式,其中execution就是一个切入点指示符。Spring AOP仅支持部分AspectJ的切入点指示符,但Spring AOP还额外支持一个bean切入点指示符。不仅如此,因为Spring AOP只支持使用方法调用作为连接点,所以Spring AOP的切入点指示符仅匹配方法执行的连接点。
完整的AspectJ切入点语言支持大量切入点指示符,但是Spring并不支持它们。它们是:call,get,preinitialization,staticinitialization,initialization,handler,adviceexecution,withincode,cflow,cflowbelow,if,@this和@withincode。一旦在Spring AOP中使用这些切点指示符,就会抛出IllegalArgumentException。
Spring AOP支持的切入点指示符有如下几个:
- execution:用于匹配执行方法的连接点,这是Spring AOP中国最主要的切入点指示符。该切入点的用法也相对复杂,execution表达式的格式如下:
execution(modifier-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
上面的格式中,execution是不变的,用于作为execution表达式的开头,整个表达式中几个参数的详细解释如下:
modifier-pattern:指定方法的修饰符,支持通配符,该部分可以省略
ret-type-pattern:指定返回值类型,支持通配符,可以使用“*”来通配所有的返回值类型
declaring-type-pattern:指定方法所属的类,支持通配符,该部分可以省略
name-pattern:指定匹配的方法名,支持通配符,可以使用“*”来通配所有的方法名
param-pattern:指定方法的形参列表,支持两个通配符,“”和“..”,其中“”代表一个任意类型的参数,而“..”代表0个或多个任意类型的参数。
-
throw-pattern:指定方法声明抛出的异常,支持通配符,该部分可以省略
如下是几个execution表达式:
execution(public * * (..))//匹配所有public方法
execution(* set*(..))//匹配以set开始的方法
execution(* com.abc.service.AdviceManager.* (..))//匹配AdviceManager中任意方法
execution(* com.abc.service.. (..))//匹配com.abc.servcie包中任意类的任意方法
-
within:限定匹配特定类型的连接点,当使用Spring AOP的时候,只能匹配方法执行的连接点。下面是几个例子:
within(com.abc.service.*)//匹配com.abc.service包中的任意连接点
within(com.abc.service..*)//匹配com.abc.service包或子包中任意的连接点
-
this:用于指定AOP代理必须是指定类型的实例,用于匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。下面是个例子:
this(com.abc.service.AdviceManager)//匹配实现了AdviceManager接口的代理对象的所有连接点,在Spring中只是方法执行的连接点
-
target:用于限定目标对象必须是指定类型的实例,用于匹配该对象的所有连接点。当使用Spring AOP的时候,只能匹配方法执行的连接点。下面是个例子:
target(com.abc.servcie.AdviceManager)//匹配实现了AdviceManager接口的目标对象的所有连接点,在Spring中只是方法执行的连接点
-
args:用于对连接点的参数类型进行限制,要求参数的类型时指定类型的实例。同样,当使用Spring AOP的时候,只能匹配方法执行的连接点。下面是个例子:
args(java.io.Serializable)//匹配只接受一个参数,且参数类型是Serializable的所有连接点,在Spring中只是方法执行的连接点
注意,这个例子与使用execution(* *(java.io.Serializable))定义的切点不同,args版本只匹配运行时动态传入参数值是Serializable类型的情形,而execution版本则匹配方法签名只包含一个Serializable类型的形参的方法。
另外,Spring AOP还提供了一个名为bean的切入点提示符,它是Spring AOP额外支持的,并不是AspectJ所支持的切入点指示符。这个指示符对Spring框架来说非常有用:它将指定为Spring中的哪个Bean织入增强处理。当然,Spring AOP中只能使用方法执行作为连接点。
-
bean:用于指定只匹配该Bean实例内的连接点,实际上只能使用方法执行作为连接点。定义bean表达式时需要传入Bean的id或name,支持使用”*”通配符。下面是几个例子:
bean(adviceManager)//匹配adviceManager实例内方法执行的连接点
bean(*Manager)//匹配以Manager结尾的实例内方法执行的连接点
3、使用组合切点表达式
Spring支持使用如下三个逻辑运算符来组合切入点表达式:
&&:要求连接点同时匹配两个切点表达式
||:要求连接点匹配至少一个切入点表达式
!:要求连接点不匹配指定的切入点表达式
其实在之前介绍args的时候,已经用到了“&&”运算符:
pointcut("execution(* com.abc.service.*.*(..) && args(name))")
上面的pointcut由两个表达式组成,而且使用&&来组合这两个表达式,因此连接点需要同时满足这两个表达式才能被织入增强处理。
三、基于XML配置文件方式的AOP
除了前面介绍的基于JDK1.5的注解方式来定义切面,切入点和增强处理外,Spring AOP也允许直接使用XML配置文件来管理它们。在JDK1.5之前,只能使用配置文件的方式来管理,在Spring2.X后提供了一个新的aop命名空间来定义切面、切入点和增强处理。
相比之下,使用XML配置文件方式有如下优点:
如果没有使用JDK1.5以上版本,只能使用XML配置文件的方式
对早期的Spring用于来说更加习惯,而且这种方式允许使用纯粹的POJO来支持AOP
采用XML配置方式时,我们可以清晰的看到系统中存在哪些切面
同时,XML配置文件的方式也有如下缺点:
不能将切面,切入点和增强处理等封装到一个地方。当我们需要查看切面、切点和增强处理时,必须同时结合Java文件和XML配置文件
XML配置文件方式比@AspectJ方式有更多限制:仅支持“singleton”切面Bean,不能在XML中组合多个命名连接点的声明
除此之外,@AspectJ切面还有一个优点就是能被Spring AOP和AspectJ同时支持,如果有一天我们需要将应用改为AspectJ来实现AOP,使用@AspectJ将非常容易迁移。
在Spring的配置文件中,所有的切面、切点和增强处理都必须定义在<aop:config../>
元素内部。<beans../>
元素可以包含多个<aop:config../>
元素,一个<aop:config../>
可以包含pointcut、advisor和aspect元素,且这三个元素需要按照此顺序来定义。
注意:当我们使用<aop:config../>
方式进行配置时,可能与Spring的自动代理方式相互冲突,因此,建议要么全部使用<aop:config../>
配置方式,要么全部使用自动代理方式,不要把两者混合使用。
1、配置切面
配置<aop:config../>
元素时,实质是将已有的Spring Bean转换成切面Bean,所以需要先定义一个普通的Spring Bean。因为切面Bean可以当成一个普通的Spring Bean来配置,所以我们完全可以为该切面Bean配置依赖注入。当切面Bean的定义完成后,通过<aop:congig../>
元素中是哟个ref属性来引用该Bean,就可以将该Bean转换成切面Bean了。配置<aop:config../>
元素时可以指定如下三个属性:
id:该切面Bean的标识名
ref:指定将要被转换成切面Bean的的普通Bean的id
order:指定该切面Bean的优先级,值越小,优先级越高
如下配置片段定义了一个切面:
<!-- 定义普通的Bean实例 -->
<bean id="afterAdviceBean" class="com.abc.advice.AfterAdviceBean" />
<aop:config>
<!-- 将容器中的afterAdviceBean转换成切面Bean -->
<aop:aspect id="afterAdviceAspect" ref="afterAdviceBean">
...
</aop:aspect>
</aop:config>
上面的配置中,将一个AfterAdviceBean类型普通的Bean对象afterAdviceBean转换成了切面Bean对象afterAdviceAspect。
2、配置增强处理
与使用@AspectJ完全一样,使用XML一样可以配置Before、After、AfterReturning、AfterThrowing和Around 5种增强处理,而且完全支持和@Aspect完全一样的语义。使用XML配置增强处理分别依赖于如下几个元素:
<aop:before../>
:配置Before增强处理<aop:after../>
:配置After增强处理<aop:after-returning../>
:配置AfterReturning增强处理<aop:after-throwing../>
:配置AfterThrowing增强处理<aop:around../>
:配置Around增强处理
这些元素都不支持使用子元素,但通常可以指定如下属性:
pointcut:指定一个切入点表达式,Spring将在匹配该表达式的连接点织入增强处理
pointcut-ref:指定一个已经存在的切入点名称,通常pointcut和pointcut-ref只需使用其中之一
method:指定一个方法名,指定切面Bean的该方法作为增强处理
throwing:只对
<aop:after-throwing../>
元素有效,用于指定一个形参名,AfterThrowing增强处理方法,可通过该形参访问目标方法所抛出的异常returning:只对
<aop:after-returning../>
元素有效,用于指定一个形参名,AfterThrowing增强处理方法,可通过该形参访问目标方法的返回值
既然选择XML配置文件的方式来管理切面、切点和增强处理,那么切面类里定义切面,切点和增强处理的注解就可以全部删除了。
定义切点时,XML配置方式和@AspectJ注解方式支持完全相同的切点指示符,一样可以支持execution、within、args、this、target和bean等切点提示符。另外,XML配置文件方式也和@AspectJ方式一样支持组合切入点表达式,但XML配置方式不再使用简单的&&、|| 和 ! 作为组合运算符(因为直接在XML文件中需要使用实体引用来表示他们),而是使用如下三个组合运算符:and(相当于&&)、or(相当于||)和not(相当于!)。 下面是一个使用
<bean id="adviceTest" class="com.abc.advice.AdviceTest" />
<aop:config>
<!-- 注意这里可以使用order属性为Aspect指定优先级 -->
<aop:aspect id="firstAspect" ref="adviceTest" order="2">
<!-- @Before切点 -->
<aop:before pointcut="execution(* com.abc.service.*.*(..))"
method="permissionCheck"/>
<!-- @After切点 -->
<aop:after pointcut="execution(* com.abc.service.*.*(..))"
method="releaseResource"/>
<!-- @AfterReturning切点 -->
<aop:after-returning pointcut="execution(* com.abc.service.*.*(..))"
method="log"/>
<!-- @AfterThrowing切点 -->
<aop:after-throwing pointcut="execution(* com.abc.service.*.*(..))"
method="handleException"/>
<!-- @Around切点(多个切点提示符使用and、or或者not连接) -->
<aop:around pointcut="execution(* com.abc.service.*.*(..)) and args(name,time,..)"
method="process"/>
</aop:aspect>
</aop:config>
上面的定义中,特意为firstAspec指定了order=2,表明firstAspect的优先级为2,如果这个XML文件中还有order=1的Aspect,那么这个Aspect将被Spring AOP优先织入。其执行结果,和前面文章中介绍的相同,这里不再给出。
3、配置切点
在Spring中通过<aop:pointcut../>
元素来定义切点。当把<aop:pointcut../>
元素作为<aop:config../>
的子元素时,表明该切点可以被多个切面共享;当把<aop:pointcut../>
元素作为<aop:aspect../>
的子元素时,表明该切点只能在这个切面内使用。配置<aop:pointcut../>
时,通常需要配置如下两个属性:
id:指定该切点的标识名
expression:指定该切点关联的切点表达式
如下的配置定义了一个简单的切点:
<aop:pointcut id="point1" expression="execution(* com.abc.service.*.*(..))" />
另外,如果程序中已经使用注解的方式定义了切点,在
<aop:pointcut id="point2" expression="com.abc.service.AdviceTest.myPointcut()" />
下面的程序中定义了一个AfterThrowing增强处理,包含该增强处理的切面类如下:
package com.abc.advice;
public class AfterThrowingAdviceTest {
//定义一个普通方法作为增强处理方法,这个方法名将在XML配置文件中指定
public void doRecoveryAction(Throwable th) {
System.out.println("目标方法抛出异常:" + th);
System.out.println("模拟数据库事务恢复");
}
}
与前面的切面类完全类似,该Java类就是一个普通的Java类。下面的配置文件将负责配置该Bean实例,并将该Bean转换成切面Bean:
<bean id="afterThrowingAdviceTest"
class="com.abc.advice.AfterThrowingAdviceTest" />
<aop:config>
<!-- 这个切点将可以被多个<aop:aspect../>使用 -->
<aop:pointcut id="myPointcut"
expression="execution(* com.abc.service.*.*(..))" />
<!-- 这个aspect由上面的Bean afterThrowingAdviceTest转化而来 -->
<aop:aspect id="aspect1" ref="afterThrowingAdviceTest">
<!-- 定义一个AfterThrowing增强处理,指定切入点以切面Bean中
的doRecoverryAction作为增强处理方法 -->
<aop:after-throwing pointcut-ref="myPointcut"
method="doRecoveryAction" throwing="th" />
</aop:aspect>
</aop:config>
上面的<aop:pointcut../>
元素定义了一个全局的切点myPointcut,这样其他切面Bean就可以多次复用这个切点了。<aop:after-throwing../>
元素中,使用pointcut-ref属性指定了一个已经存在的切点。