面向切面的Spring
之前已有一篇《Spring中的面向切面》简单的介绍了一下,这次按照《Spring实战》的第四章来详细讲述一下面向切面。
在软件系统中有一些功能需要用到应用程序的多个地方,但是我们又不想在每个点都明确调用它们。日志、安全和事物管理的确都很重要,但它们是否是应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面的问题由其他应用对象来处理,这会不会更好呢?
在软件开发中,散布于应用中多处的功能被称为横切关注点(crosscutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
之前,我们介绍了如何使用依赖注入(DI)管理和配置我们的应用对象。DI 有助于应用对象之间的解耦,而 AOP 可以实现横切关注点与它们所影响的对象之间的解耦。
日志是应用切面的常见范例,但它并不是切面适用的唯一场景。通览本书,我们还会看到切面所适用的多个场景,包括声明式事务、安全和缓存。
什么是面向切面编程
如前所述,切面能帮助我们模块化横切关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。例如,安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则。图 4.1 直观呈现了横切关注点的概念。

图 4.1展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全和事务管理。
如果要重用通用功能的话,最常见的面向对象技术是继承(inheritance)或委托(delegation)。但是,如果在整个应用中都使用相同的基类,继承往往会导致一个脆弱的对象体系;而使用委托可能需要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
定义AOP术语
与大多数技术一样,AOP 已经形成了自己的术语。描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point)。图 4.2 展示了这些概念是如何关联在一起的。

遗憾的是,大多数用于描述 AOP 功能的术语并不直观,尽管如此,它们现在已经是 AOP 行话的组成部分了,为了理解 AOP,我们必须了解这些术语。在我们进入某个领域之前,必须学会在这个领域该如何说话。
Aspect(切面)
Aspect由Pointcut和Advice组成,它既包含了横切逻辑的定义,也包括了连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
AOP的工作重心在于如何将Advice织入目标对象的连接点上,这里包含两个工作:
- 如何通过 pointcut 和 advice 定位到特定的 join point 上
- 如何在 advice 中编写切面代码
可以简单地认为,使用 @Aspect 注解的类就是切面。
Advice(通知)
由 aspect 添加到特定的 join point (即满足 pointcut 规则的 join point ) 的一段代码。许多 AOP 框架,包括 Spring AOP,会将 advice 模拟为一个拦截器(interceptor),并且在 join point 上维护多个 advice,进行层层拦截。
例如 HTTP 鉴权的实现,我们可以为每个使用 RequestMapping 标注的方法织入 advice,当 HTTP 请求到来时,首先进入到 advice 代码中,在这里我们可以分析这个 HTTP 请求是否有相应的权限,如果有,则执行 Controller,如果没有,则抛出异常。这里的 advice 就扮演着鉴权拦截器的角色了。
Spring 切面可以应用 5 种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
Join point(连接点)
a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程序运行中的一些时间点,例如一个方法的执行,或者是一个异常的处理。在 Spring AOP 中,join point 总是方法的执行点,即只有方法连接点。
Pointcut(切点)
匹配 join point 的谓词(a predicate that matches join points)。advice 是和特定的 pointcut 关联的,并且在 pointcut 相匹配的 join point 中执行。
在 Spring 中,所有的方法都可以认为是 joinpoint,但是我们并不希望在所有的方法上都添加 advice,而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 join point,给满足规则的 join point 添加 advice。
关于join point 和 pointcut 的区别
在 Spring AOP 中,所有的方法执行都是 join point。而 pointcut 是一个描述信息,它修饰的是 join point,通过 pointcut,我们就可以确定哪些 join point 可以被织入 advice。因此 join point 和 pointcut 本质上就是两个不同纬度上的东西。
Introduction(引入)
为一个类型添加额外的方法或字段。Spring AOP 允许我们为 目标对象 引入新的接口(和对应的实现).。例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口,并以此来简化 caching 的实现。
Target(目标对象)
织入 advice 的目标对象. 目标对象也被称为 advised object。
因为 Spring AOP 使用运行时代理的方式来实现 aspect,因此 adviced object 总是一个代理对象(proxied object)。注意,adviced object 指的不是原来的类,而是织入 advice 后所产生的代理类。
AOP Proxy
一个类被 AOP 织入 advice,就会产生一个结果类,它是融合了原类和增强逻辑的代理类。在 Spring AOP 中,一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象。
Weaving(织入)
将 aspect 和其他对象连接起来,并创建 adviced object 的过程。根据不同的实现技术,AOP织入有三种方式:
- 编译器织入,这要求有特殊的 Java 编译器
- 类装载期织入,这需要有特殊的类装载器
- 动态代理织入,在运行期为目标类添加 Advice 生成子类的方式
Spring 采用动态代理织入,而 Aspectj 则采用编译器织入和类装载期织入。
关于 AOP Proxy
Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理,通过它,我们可以为任意的接口实现代理。
如果需要为一个类实现代理,那么可以使用 CGLIB 代理。当一个业务逻辑对象没有实现接口时,那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了。即如果我们需要为一个方法织入 advice,但是这个方法不是一个接口所提供的方法,则此时 Spring AOP 会使用 CGLIB 来实现动态代理。鉴于此,Spring AOP 建议基于接口编程,对接口进行 AOP 而不是类。
要掌握的新术语可真不少啊。再看一下图 4.1,现在我们已经了解了如下的知识,通知包含了需要用于多个应用对象的横切行为;连接点是程序执行过程中能够应用通知的所有点;切点定义了通知被应用的具体位置(在哪些连接点)。其中关键的概念是切点定义了哪些连接点会得到通知。
Spring 对 AOP 的支持
并不是所有的 AOP 框架都是相同的,它们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是 AOP 框架的基本功能。
Spring 提供了 4 种类型的 AOP 支持:
- 基于代理的经典 Spring AOP;
- 纯 POJO 切面;
- @AspectJ 注解驱动的切面;
- 注入式 AspectJ 切面(适用于 Spring 各版本)
前三种都是 Spring AOP 实现的变体,Spring AOP 构建在动态代理基础之上,因此,Spring 对 AOP 的支持局限于方法拦截。
借助 Spring 的 aop 命名空间,我们可以将纯 POJO 转换为切面。实际上,这些 POJO 只是提供了满足切点条件时所要调用的方法。遗憾的是,这种技术需要 XML 配置,但这的确是声明式地将对象转换为切面的简便方式。
Spring 借鉴了 AspectJ 的切面,以提供注解驱动的 AOP。本质上,它依然是 Spring 基于代理的 AOP,但是编程模型几乎与编写成熟的 AspectJ 注解切面完全一致。这种 AOP 风格的好处在于能够不使用 XML 来完成功能。 如果你的 AOP 需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用 AspectJ 来实现切面。在这种情况下,上文所示的第四种类型能够帮助你将值注入到 AspectJ 驱动的切面中。
在开始之前,我们必须要了解 Spring AOP 框架的一些关键知识。
Spring 通知是 Java 编写的
Spring 所创建的通知都是用标准的 Java 类编写的。这样的话,我们就可以使用与普通 Java 开发一样的集成开发环境(IDE)来开发切面。而且,定义通知所应用的切点通常会使用注解或在 Spring 配置文件里采用 XML 来编写,这两种语法对于 Java 开发者来说都是相当熟悉的。
AspectJ 与之相反。虽然 AspectJ 现在支持基于注解的切面,但 AspectJ 最初是以 Java 语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的 AOP 语言,我们可以获得更强大和细粒度的控制,以及更丰富的 AOP 工具集,但是我们需要额外学习新的工具和语法。
Spring在运行时通知对象
通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。如图 4.3 所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时, 在调用目标 bean 方法之前,会执行切面逻辑。

直到应用需要被代理的 bean 时,Spring 才创建代理对象。如果使用的是 ApplicationContext 的话,在 ApplicationContext 从 BeanFactory 中加载所有 bean 的时候,Spring 才会创建被代理的对象。因为 Spring 运行时才创建代理对象,所以我们不需要特殊的编译器来织入 Spring AOP 的切面。
Spring 只支持方法级别的连接点
正如前面所探讨过的,通过使用各种 AOP 方案可以支持多种连接点模型。因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。这与一些其他的 AOP 框架是不同的,例如 AspectJ 和 JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring 缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在 bean 创建时应用通知。
但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用 AspectJ 来补充 Spring AOP 的功能。
对于什么是 AOP 以及 Spring 如何支持 AOP 的,我们现在已经有了一个大致的了解。现在是时候学习如何在 Spring 中创建切面了,让我们先从 Spring 的声明式 AOP 模型开始。
通过切点来选择连接点
正如之前所提过的,切点用于准确定位应该在什么地方应用切面的通知。通知和切点是切面的最基本元素。因此,了解如何编写切点非常重要。
在 Spring AOP 中,要使用 AspectJ 的切点表达式语言来定义切点。关于 Spring AOP 的 AspectJ 切点,最重要的一点就是 Spring 仅支持 AspectJ 切点指示器(pointcut designator)的一个子集。让我们回顾下,Spring 是基于代理的,而某些切点表达式是与基于代理的 AOP 无关的。表 4.1 列出了 Spring AOP 所支持的 AspectJ 切点指示器。
| AspectJ 指示器 | 描述 |
|---|---|
arg() |
限制连接点匹配参数为指定类型的执行方法 |
@args() |
限制连接点匹配参数由指定注解标注的执行方法 |
execution() |
用于匹配是连接点的执行方法 |
this() |
限制连接点匹配 AOP 代理的 bean 引用为指定类型的类 |
target |
限制连接点匹配目标对象为指定类型的类 |
@target() |
限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() |
限制连接点匹配指定的类型 |
@within() |
限制连接点匹配指定注解所标注的类型(当使用 Spring AOP 时,方 法定义在由指定的注解所标注的类里) |
@annotation |
限定匹配带有指定注解的连接点 |
在 Spring 中尝试使用 AspectJ 其他指示器时,将会抛出 IllegalArgumentException异常。
当我们查看如上所展示的这些 Spring 支持的指示器时,注意只有 execution 指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。这说明 execution 指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
编写切点
为了阐述 Spring 中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个 Performance 接口:
/**
* 这个是一个演出接口
* 可以代表任何类型的现场表演
* 如舞台剧、演唱会等等。
*/
public interface Performance {
/**
* 表演
*/
public void perform();
}
Performance 可以代表任何类型的现场表演,如舞台剧、电影或音乐会。假设我们想编写 Performance 的 perform() 方法触发的通知。图 4.4 展现了一个切点表达式,这个表达式能够设置当 perform() 方法执行时触发通知的调用。

我们使用 execution() 指示器选择 Performance 的 perform() 方法。方法表达式以 "*" 号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(..)表明切点要选择任意的 perform() 方法,无论该方法的入参是什么。
现在假设我们需要配置的切点仅匹配 concert 包。在此场景下,可以使用 within() 指示器来限制匹配,如图4.5 所示。

请注意我们使用了 “&&” 操作符把 execution() 和 within() 指示器连接在一起形成与(and)关系(切点必须匹配所有的指示器)。类似地,我们可以使用 “||” 操作符来标识或(or)关系,而使用 “!” 操作符来标识非(not)操作。
因为 "&" 在 XML 中有特殊含义,所以在 Spring 的 XML 配置里面描述切点时,我们可以使用 and 来代替 "&&"。同样,or 和 not 可以分别用来代替 "||" 和 "!"。
在切点中选择 bean
除了表 4.1 所列的指示器外,Spring 还引入了一个新的 bean() 指示器,它允许我们在切点表达式中使用 bean 的 ID 来标识 bean。bean() 使用 bean ID 或 bean 名称作为参数来限制切点只匹配特定的 bean。
例如,考虑如下的切点:
execution(* concert.Performance.perform()) and bean('woodstock')
在这里,我们希望在执行 Performance 的 perform() 方法时应用通知,但限定 bean 的 ID 为 woodstock。
在某些场景下,限定切点为指定的 bean 或许很有意义,但我们还可以使用非操作为除了特定 ID 以外的其他 bean 应用通知:
execution(* concert.Performance.perform()) and !bean('woodstock')
在此场景下,切面的通知会被编织到所有 ID 不为 woodstock 的 bean 中。
现在,我们已经讲解了编写切点的基础知识,让我们再了解一下如何编写通知和使用这些切点声明切面。
使用注解创建切面
使用注解来创建切面是 AspectJ 5 所引入的关键特性。AspectJ 5 之前,编写 AspectJ 切面需要学习一种 Java 语言的扩展,但是 AspectJ 面向注解的模型可以非常简便地通过少量注解把任意类转变为切面。
我们已经定义了 Performance 接口,它是切面中切点的目标对象。现在,让我们使用 AspecJ 注解来定义切面。但在这个之前先开一场演唱会吧!
import java.util.Random;
/**
* 一个演唱会类
*/
public class SingPerformance implements Performance {
@Override
public void perform() {
System.out.println("表演中...");
// 测试Enforceable时先注释,避免抛出异常影响测试
Random random = new Random(System.nanoTime());
if(random.nextInt(10) % 2 == 0) { //[0,10)能被2整除就演砸
throw new RuntimeException("演砸了");
}
}
}
SingPerformance实现了Performance接口,重写了perform方法,并且还增加了演出可能不太行的可能性 q(≧▽≦q)
定义切面
如果一场演出没有观众的话,那不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来讲,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。
创建一个Audience 观众类,它定义了我们所需的一个切面。
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* 这是一个观众类
*/
@Aspect
public class Audience {
@Before("execution(* com.syuez.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("设置手机为静音模式");
}
@Before("execution(* com.syuez.Performance.perform(..))")
public void takeSeats() {
System.out.println("入座");
}
@AfterReturning("execution(* com.syuez.Performance.perform(..))")
public void applause() {
System.out.println("鼓掌!!!");
}
@AfterThrowing("execution(* com.syuez.Performance.perform(..))")
public void demandRefund() {
System.out.println("RNM,退钱!!!");
}
}
Audience 类使用 @AspectJ 注解进行了标注。该注解表明 Audience 不仅仅是一个 POJO,还是一个切面。Audience 类中的方法都使用注解来定义切面的具体行为。
Audience 有四个方法,定义了一个观众在观看演出时可能会做的事情。在演出之前,观众要就坐 takeSeats() 并将手机调至静音状态 silenceCellPhones()。如果演出很精彩的话,观众应该会鼓掌喝彩 applause()。不过,如果演出没有达到观众预期的话,观众会要求退款 demandRefund()。
| 注解 | 通知 |
|---|---|
@After |
通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning |
通知方法会在目标方法返回后调用 |
@AfterThrowing |
通知方法会在目标方法抛出异常后调用 |
@Around |
通知方法会将目标方法封装起来 |
@Before |
通知方法会在目标方法调用之前执行 |
Audience 使用到了前面五个注解中的三个。takeSeats() 和 silenceCellPhones() 方法都用到了 @Before 注解,表明它们应该在演出开始之前调用。applause() 方法使用了 @AfterReturning 注解,它会在演出成功返回后调用。demandRefund() 方法上添加了 @AfterThrowing 注解,这表明它会在抛出异常以后执行。
你可能已经注意到了,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。其实,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。让我们近距离看一下这个设置给通知注解的切点表达式,我们发现它会在 Performance 的 perform() 方法执行时触发。
相同的切点表达式我们重复了四遍,这可真不是什么光彩的事情。这样的重复让人感觉有些不对劲。如果我们只定义这个切点一次,然后每次需要的时候引用它,那么这会是一个很好的方案。
幸好,我们完全可以这样做:@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。接下来的以下程序展现了新的 Audience,现在它使用了@Pointcut。
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* 这是一个观众类
*/
@Aspect
public class Audience {
@Pointcut("execution(* com.syuez.Performance.perform(..))")
public void performance() { }
@Before("performance()")
public void silenceCellPhones() {
System.out.println("设置手机为静音模式");
}
@Before("performance()")
public void takeSeats() {
System.out.println("入座");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("鼓掌!!!");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("RNM,退钱!!!");
}
}
在 Audience 中,performance() 方法使用了 @Pointcut 注解。为 @Pointcut 注解设置的值是一个切点表达式,就像之前在通知注解上所设置的那样。通过在 performance() 方法上添加 @Pointcut 注解,我们实际上扩展了切点表达式语言,这样就可以在任何的切点表达式中使用 performance() 了,如果不这样做的话,你需要在这些地方使用那个更长的切点表达式。我们现在把所有通知注解中的长表达式都替换成了 performance()。
performance() 方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供 @Pointcut 注解依附。
需要注意的是,除了注解和没有实际操作的 performance() 方法,Audience 类依然是一个 POJO。我们能够像使用其他的 Java 类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的 Java 类并没有什么区别。Audience 只是一个 Java 类,只不过它通过注解表明会作为切面使用而已。
像其他的 Java 类一样,它可以装配为 Spring 中的 bean:
@Bean
public Audience audience() {
return new Audience();
}
如果你就此止步的话,Audience 只会是 Spring 容器中的一个 bean。即便使用了 AspectJ 注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。
如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 EnableAspectJAutoProxy 注解启用自动代理功能。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class ConcertConfig {
@Bean
public Performance performance() {
return new SingPerformance();
}
@Bean
public Audience audience() {
return new Audience();
}
}
一切似乎都就绪了,创建一个单元测试,然后开始一场演出吧。
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class) // 让测试运行于Spring测试环 境,以便在测试开始的时候自动创建Spring的应用上下文
@ContextConfiguration(classes = ConcertConfig.class) // 加载配置类文件
public class SingPerformanceTest {
@Autowired
private Performance performance;
@Test
public void singPerformanceTest() {
performance.perform();
}
}
结果:
演砸了

演出成功

我们需要记住的是,Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是 Spring 基于代理的切面。这一点非常重要,因为这意味着尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖 Spring 来创建基于代理的切面。
到现在为止,我们的切面在定义时,使用了不同的通知方法来实现前置通知和后置通知。但是表 4.2 还提到了另外的一种通知:环绕通知 (around advice)。环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。为了阐述环绕通知,我们重写 Audience 切面。这次,我们使用一个 环绕通知来代替之前多个不同的前置通知和后置通知。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* 这是一个观众类
*/
@Aspect
public class Audience {
@Pointcut("execution(* com.syuez.Performance.perform(..))")
public void performance() { }
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("设置手机为静音模式");
System.out.println("入座");
jp.proceed();
System.out.println("鼓掌!!!");
} catch (Throwable e) {
System.out.println("RNM,退钱!!!");
}
}
}
在这里,@Around 注解表明 watchPerformance() 方法会作为 performance() 切点的环绕通知。在这个通知中,观众在演出之前会将手机调至静音并就坐,演出结束后会鼓掌喝彩。像前面一样,如果演出失败的话,观众会要求退款。
可以看到,这个通知所达到的效果与之前的前置通知和后置通知是一样的。但是,现在它们位于同一个方法中,不像之前那样分散在四个不同的通知方法里面。
关于这个新的通知方法,你首先注意到的可能是它接受 ProceedingJoinPoint 作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用 ProceedingJoinPoint 的 proceed() 方法。
需要注意的是,别忘记调用 proceed() 方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。
有意思的是,你可以不调用 proceed() 方法,从而阻塞对被通知方法的访问,与之类似,你也可以在通知中对它进行多次调用。要这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试。