您的位置:首页 > 编程语言 > Java开发

Spring AOP中定义切点(PointCut)和通知(Advice)

2017-09-21 21:24 525 查看
本文讨论一下Spring AOP编程中的两个关键问题,定义切点和定义通知,理解这两个问题能应付大部分AOP场景。
如果你还不熟悉AOP,请先看AOP基本原理,本文的例子也沿用了AOP基本原理中的例子。


切点表达式

切点的功能是指出切面的通知应该从哪里织入应用的执行流。切面只能织入公共方法。
在Spring AOP中,使用AspectJ的切点表达式语言定义切点其中
excecution()
是最重要的描述符,其它描述符用于辅助
excecution()

excecution()
的语法如下
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

这个语法看似复杂,但是我们逐个分解一下,其实就是描述了一个方法的特征:
问号表示可选项,即可以不指定。
excecution(* com.tianmaying.service.BlogService.updateBlog(..))

modifier-pattern:表示方法的修饰符
ret-type-pattern:表示方法的返回值
declaring-type-pattern?:表示方法所在的类的路径
name-pattern:表示方法名
param-pattern:表示方法的参数
throws-pattern:表示方法抛出的异常
注意事项
其中后面跟着“?”的是可选项。
在各个pattern中,可以使用"
*
"来表示匹配所有。
在param-pattern中,可以指定具体的参数类型,多个参数间用“,”隔开,各个也可以用“
*
”来表示匹配任意类型的参数,如
(String)
表示匹配一个
String
参数的方法;
(*,String)
表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是
String
类型。
可以用
(..)
表示零个或多个任意的方法参数。
使用
&&
符号表示与关系,使用
||
表示或关系、使用
!
表示非关系。在XML文件中使用
and
or
not
这三个符号。


在切点中引用Bean

Spring还提供了一个
bean()
描述符,用于在切点表达式中引用Spring
Beans。例如:
excecution(* com.tianmaying.service.BlogService.updateBlog(..))  and bean('tianmayingBlog')

这表示将切面应用于
BlogService
updateBlog
方法上,但是仅限于ID为tianmayingBlog的Bean。
也可以排除特定的Bean:
excecution(* com.tianmaying.service.BlogService.updateBlog(..))  and !bean('tianmayingBlog')


其它切点描述符

其它可用的描述符包括:

args()


@args()


execution()


this()


target()


@target()


within()


@within()


@annotation


当你有更加复杂的切点需要描述时,你可能可以用上这些描述符,通过这些你可以设置目标类实现的接口、方法和类拥有的标注等信息。具体可以参考Spring的官方文档
这里一共有9个描述符,
execution()
前面已经详细讨论过,其它几个可以做一个简单的分类:

this()
是用来限定方法所属的类,比如
this(com.tianmaying.service.BlogServiceInterface)
表示实现了
com.tianmaying.service.BlogServiceInterface
的所有类。如果this括号内是具体类而不是接口的话,则表示单个类。

@annotation
表示具有某个标注的方法,比如
@annotation(org.springframework.transaction.annotation.Transactional)
表示被
Transactional
标注的方法

args
 表示方法的参数属于一个特定的类

within
 表示方法属于一个特定的类

target
 表示方法所属的类

它们对应的加了
@
的版本则表示对应的类具有某个标注。


单独定义切点

详细了解了定义切点之后,在回顾上一节中的代码:
package com.tianmaying.aopdemo.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect //1
@Component
public class LogAspect {

@Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))") //2
private void logPointCut() {
}

@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3
public void logBookingStatus(boolean retVal) {  //4
if (retVal) {
System.out.println("booking flight succeeded!");
} else {
System.out.println("booking flight failed!");
}
}
}

可以看到通过标注方式定义切点只需要两个步骤:
定义一个空方法
使用
@Piontcut
标注,填入切点表达式
@AfterReturning(pointcut
= "execution(* com.tianmaying.aopdemo..*.bookFlight(..))", returning = "retVal")
中通过
pointcout
= "logPointCut"
引用了这个切点。当然也可以在
@AfterReturning()
直接定义切点表达式,如:
@AfterReturning(pointcut = "logPointCut()", returning = "retVal") //3

推荐使用前一种方法,因为这样可以在多个通知中复用切点的定义。


切点定义实例

这里我们给出一些切点的定义实例。
@Pointcut("execution(public * *(..))") // 1
private void anyPublicOperation() {}

@Pointcut("within(com.xyz.someapp.web..*))") // 2
private void inTrading() {}

@Pointcut("anyPublicOperation() && inTrading()") // 3
private void tradingOperation() {}

@within(org.springframework.transaction.annotation.Transactional) // 4
private void transactionalClass() {}

@annotation(org.springframework.transaction.annotation.Transactional) //5
private void transactionalMethod() {}

上面的代码定义了三个切点:
任意公共方法(实际应用中一般不会定义这样的切点)
within(com.xyz.someapp.web
包或者其子包下任意类的方法
同时满足切点1和切点2条件的切点,这里使用了
&&
符号
标注了
Transactional
的类的方法
标注了
Transactional
的方法


定义通知

依然回到
TimeRecordingAspect
的代码:
@Aspect
@Component
public class TimeRecordingAspect {

@Pointcut("execution(* com.tianmaying.aopdemo..*.bookFlight(..))")
private void timeRecordingPointCut() {
}

@Around("timeRecordingPointCut()") //1
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {  //2

long start = System.currentTimeMillis();
Object retVal = pjp.proceed(); // 3

long duration = System.currentTimeMillis() - start;
System.out.println(String.format(
"time for booking flight is %d seconds", duration));

return retVal;
}
}

定义了切点之后,我们需要定义何时调用
recordTime
方法记录时间,即需要定义通知。
AspectJ提供了五种定义通知的标注:
@Before
:前置通知,在调用目标方法之前执行通知定义的任务
@After
:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
@After-returning
:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
@After-throwing
:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
@Around
:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务
通过标注定义通知只需要两个步骤:
将以上五种标注之一添加到切面的方法中
在标注中设置切点的定义


创建环绕通知

环绕通知相比其它四种通知有其特殊之处。环绕通知本质上是将前置通知、后置通知和异常通知整合成一个单独的通知。
@Around
标注的方法,该方法必须有一个
ProceedingJoinPoint
类型的参数,比如上面代码中的
recordTime
的签名:
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable

在方法体中,需要通过这个参数,以
joinPoint.proceed();
的形式调用目标方法。注意在环绕通知中必须进行该调用,否则目标方法本身的执行就会被跳过。
比如在
recoredTime
的实现中:
long start = System.currentTimeMillis();
Object retVal = pjp.proceed();
long duration = System.currentTimeMillis() - start;
System.out.println(String.format("time for booking flight is %d seconds", duration));

在目标方法调用前首先记录系统时间,然后通过
pjp.proceed()
调用目标方法,调用完之后再次记录系统时间,即可计算出目标方法的耗时。


处理通知中参数

有时我们需要给通知中的方法传递目标对象的一些信息,比如传入目标业务方法的参数。
在前面的代码中我们曾经通过
@AfterReturning(pointcut
= "logPointCut()", returning = "retVal")
在通知中获取目标业务方法的返回值。获取参数的方式则需要使用关键词是
args

假设需要对系统中的
accountOperator
方法,做
Account
的验证,验证逻辑以切面的方式显示,示例如下:
@Before("com.tianmaying.UserService.accountOperator() && args(account,..)")
public void validateAccount(Account account) {
// ...
// 这可以获取传入accountOperator中的Account信息
}

args()
中参数的名称必须跟切点方法的签名中(
public
void validateAccount(Account account)
)的参数名称相同。如果使用切点函数定义,其中的参数名称也必须与通知方法签名中的参数完全相同,例如:
@Pointcut("com.tianmaying.UserService.accountOperator() && args(account,..)")
private void accountOperation(Account account) {}

@Before("accountOperation(account)")
public void validateAccount(Account account) {
// ...
}


小节

AOP的知识就介绍到这里,更复杂的场景还需要了解AOP更深入的一些知识,比如:
AOP的生成代理的方式
多个切面的顺序
更复杂的参数类型(如泛型)
使用AspectJ的切面
...
感兴趣的同学可以继续深入学习,最好的学习材料就是Spring的官方文档
天码营外围的网站开发的也基本只使用了我们介绍的这些知识点,可见这些关键知识点足以解决大部分复杂场景,确实需要用到更高级的特性时,再去参考文档即可。

版权声明本文由David创作,转载需署名作者且注明文章出处参考代码
要获取本文的参考代码,请访问: https://www.tianmaying.com/tutorial/spring-aop-point-advice/repo

from: https://www.tianmaying.com/tutorial/spring-aop-point-advice
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息