一、面向切面的基本原理
在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来说,这些个横切关注点从概念上是与应用的业务逻辑相分离的,但往往会直接嵌入到应用的业务逻辑之中。因此,面向切面编程(AOP)核心在于把这些横切关注点与业务逻辑相分离。日志是应用切面的常见范例,但是切面所适用的场景很多,包括声明式事务、安全和缓存。
1. AOP术语
- 通知(Advice)
在AOP中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:
- 前置通知
Before
—— 在方法被调用之前调用通知。 - 后置通知
After
—— 在方法完成之后调用通知,无论方法是否执行成功。 - 返回通知
After-returning
—— 在方法成功执行之后调用通知。 - 异常通知
After-throwing
—— 在方法抛出异常以后调用通知。 - 环绕通知
Around
—— 通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
- 连接点(Joinpoint)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至是修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 - 切点(Pointcut)
切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。 - 切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了关于切面的全部内容 —— 它是什么,在何时和何处完成其功能。 - 引入(Introduction)
引入允许我们向现有的类添加新的方法和属性。 - 织入(Weaving)
织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译器 —— 切面在目标类编译时被织入。
- 类加载期 —— 切面在目标类加载到JVM时被织入。
- 运行期 —— 切面在应用运行的某个时候被织入。Spring AOP就是以这种方式织入切面的。
二、在Spring XML中声明切面
1. AspectJ切点指示器
AspectJ指示器 | 描述 |
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用的指定类型的类 |
target() | 限制连接点匹配目标对象为执行类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation() | 限制匹配带有指定注解连接点 |
2. 步骤
1)基本数据
public interface Performance {
public void perform();
}
@Component
public class Audience {
// 表演之前
public void takeSeats(){
System.out.println("观众入座");
}
// 表演之前
public void turnOffCellPhones(){
System.out.println("关闭手机");
}
// 表演之后
public void applaud(){
System.out.println("鼓掌: 啪啪啪啪啪");
}
// 表演失败后
public void failure(){
System.out.println("坑爹,退钱!");
}
}
2)编写切点
execution(* com.shiftycat.concert.Performance.perform(..))
// 或者使用bean,在执行Instrument的`play()`方法时应用通知,并且Bean的ID为eddie。
execution(* com.shiftycat.concert.Performance.perform()) and bean(rocketStar)
3)XML中配置AOP
在Spring XML中配置AOP使用元素,下表概述了AOP配置元素。
AOP配置元素 | 描述 |
aop:advisor | 定义AOP通知器 |
aop:after | 定义AOP后置通知(不管被通知的方法是否执行成功) |
aop:after-returning | 定义AOP after-returning通知 |
aop:after-throwing | 定义AOP after-throwing通知 |
aop:around | 定义AOP环绕通知 |
aop:aspect | 定义切面 |
aop:aspectj-autoproxy | 启用@AspectJ注解驱动切面 |
aop:before | 定义AOP前置通知 |
aop:config | 顶层的AOP配置元素 |
aop:declare-parents | 为被通知的对象引入额外的接口,并透明的实现 |
aop:pointcut | 定义切点 |
为了演示Spring AOP,现在定义一个观众类 Audience:
public class Audience {
// 表演之前
public void takeSeats(){
System.out.println("观众入座");
}
// 表演之前
public void turnOffCellPhones(){
System.out.println("关闭手机");
}
// 表演之后
public void applaud(){
System.out.println("鼓掌: 啪啪啪啪啪");
}
// 表演失败后
public void failure(){
System.out.println("坑爹,退钱!");
}
}
在Spring XML中配置该Bean:
<bean id="rocketStar" class="com.shiftycat.concert.RocketStar" />
<bean id="audience" class="com.shiftycat.concert.Audience" />
4)声明相关通知
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(* com.shiftycat.concert.Performance.perform(..)) and bean(rocketStar)"/>
<aop:before
pointcut-ref="performance"
method="takeSeats"/>
<aop:before
pointcut-ref="performance"
method="turnOffCellPhones"/>
<aop:after-returning
pointcut-ref="performance"
method="applaud"/>
<aop:after-throwing
pointcut-ref="performance"
method="failure"/>
</aop:aspect>
</aop:config>
5)进行测试
@Test
public void performXMLTest() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
Performance performance = context.getBean(Performance.class);
performance.perform();
}
6)测试结果
观众入座
关闭手机
梁博演唱《出现又离开》
鼓掌: 啪啪啪啪啪
3. 声明环绕通知
如果不使用成员变量,那么在前置通知和后置通知之间共享信息是非常麻烦的。可以使用环绕通知代替前置通知和后置通知,现在在Audience类里添加一个新的方法:
public void watchPerformance(ProceedingJoinPoint joinPoint) {
try {
System.out.println("观众入座了");
System.out.println("关闭手机了");
joinPoint.proceed();
System.out.println("鼓掌了: 啪啪啪啪啪");
joinPoint.proceed();
System.out.println("鼓掌了: 啪啪啪啪啪");
} catch (Throwable e) {
System.out.println("坑爹,退钱!");
}
}
对于新的方法,我们使用了ProceedingJoinPoint作为参数,这个对象可以在通知里调用被通知的方法!!我们要把控制转给被通知的方法时,必须调用ProceedingJoinPoint的proceed()
方法。
修改<aop:config>
元素:
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(* com.shiftycat.concert.Performance.perform(..)) and bean(rocketStar)"/>
<aop:around pointcut-ref="performance"
method="watchPerformance"/>
</aop:aspect>
</aop:config>
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
4. 为通知传递参数
1)基本数据
定义一个新的参赛者,他是一个读心者,由MindReader接口所定义:
public interface MindReader {
void interceptThoughts(String thoughts);
String getThoughts();
}
魔术师Magician实现该接口:
public class Magician implements MindReader{
private String thoughts;
public void interceptThoughts(String thoughts) {
System.out.println("侦听志愿者的心声");
this.thoughts=thoughts;
}
public String getThoughts() {
return thoughts;
}
}
再定义一个Magician所要侦听的志愿者,首先定义一个思考者接口:
public interface Thinker {
void thinkOfSomething(String thoughts);
}
志愿者Volunteer实现该接口:
public class Volunteer implements Thinker{
private String thoughts;
public void thinkOfSomething(String thoughts) {
this.thoughts=thoughts;
}
public String getThoughts(){
return thoughts;
}
}
2)XML文件配置AOP
接下来使用Spring AOP传递Volunteer的thoughts参数,以此实现Magician的侦听:
<bean id="magician" class="com.shiftycat.concert.Magician"/>
<bean id="volunteer" class="com.shiftycat.concert.Volunteer"/>
<aop:config proxy-target-class="true">
<aop:aspect ref="magician">
<aop:pointcut id="thinking" expression="
execution(* com.shiftycat.concert.Volunteer.thinkOfSomething(String))
and args(thoughts)"/>
<!-- `arg-names`属性传递了参数给`interceptThoughts()`方法。 -->
<aop:before pointcut-ref="thinking"
method="interceptThoughts"
arg-names="thoughts"/>
</aop:aspect>
</aop:config>
输出:
侦听志愿者的心声
志愿者心里想的是:演出真精彩!
5. 通过切面引入新的方法
现在假设要给Performer派生类添加一个新的方法,传统做法是找到所有派生类,让后逐个增加新的方法或者实现。这不但很累而且假设第三方实现没有源码的话,这个过程会变得很困难。幸好,通过Spring AOP可以不必入侵性地改变原有地实现。比如,现在要给所有演出者添加一个receiveAward()
方法:
新增一个接口Contestant:
public interface Contestant {
void receiveAward();
}
由OutstandingContestant实现:
public class OutstandingContestant implements Contestant{
public void receiveAward() {
System.out.println("参加颁奖典礼");
}
}
XML:
<aop:config proxy-target-class="true">
<aop:aspect>
<aop:declare-parents
types-matching="com.shiftycat.concert.Performance+"
implement-interface="com.shiftycat.concert.Contestant"
default-impl="com.shiftycat.concert.OutstandingContestant"/>
</aop:aspect>
</aop:config>
或者:
<bean id="contestantDelegate" class="com.shiftycat.concert.OutstandingContestant"/>
<aop:config proxy-target-class="true">
<aop:aspect>
<aop:declare-parents
types-matching="com.shiftycat.concert.Performance+"
implement-interface="com.shiftycat.concert.Contestant"
delegate-ref="contestantDelegate"/>
</aop:aspect>
</aop:config>
-
types-matching
指定所要添加新方法的派生类实现的接口 -
implement-interface
指定要实现新的接口 -
default-impl
指定这个接口的实现类。
测试:
@Test
public void performXMLTest() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
Performance performance = (Performance) context.getBean("rocketStar");
performance.perform();
Contestant contestant = (Contestant) context.getBean("rocketStar");
contestant.receiveAward();
}
输出:
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
参加颁奖典礼
三、使用注解配置AOP切面
1. 注解切面
除了使用XML配置AOP切面,我们还可以使用更简洁的注解配置。现使用注解修改Audience类:
@Aspect
public class Audience {
// 声明切点
@Pointcut("execution(* com.shiftycat.concert.Performance.perform(..))")
public void performance(){
}
// 表演之前
@Before("performance()")
public void takeSeats(){
System.out.println("观众入座");
}
// 表演之前
@Before("performance()")
public void turnOffCellPhones(){
System.out.println("关闭手机");
}
// 表演之后
@After("performance()")
public void applaud(){
System.out.println("鼓掌: 啪啪啪啪啪");
}
// 表演失败后
@AfterThrowing("performance()")
public void failure(){
System.out.println("坑爹,退钱!");
}
}
@Aspect
使得Audience成为了切面。
为了让Spring识别改注解,我们还需在XML中添加<aop:aspectj-autoproxy/>
。<aop:aspectj-autoproxy/>
将在Spring应用上下文中创建一个AnnotationAwareAspectJAutoProxyCreator
类,它会自动代理@Aspect
标注的Bean:
<aop:aspectj-autoproxy proxy-target-class="true" />
测试输出:
观众入座
关闭手机
梁博演唱《出现又离开》
鼓掌: 啪啪啪啪啪
2. 注解环绕通知
使用@Around注解环绕通知:
@Aspect
public class Audience {
// 声明切点
@Pointcut("execution(* com.shiftycat.concert.Performance.perform(..))")
public void performance(){
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint) {
try {
System.out.println("观众入座了");
System.out.println("关闭手机了");
joinPoint.proceed();
System.out.println("鼓掌了: 啪啪啪啪啪");
joinPoint.proceed();
System.out.println("鼓掌了: 啪啪啪啪啪");
} catch (Throwable e) {
System.out.println("坑爹,退钱!");
}
}
}
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
3. 注解传递参数
@Aspect
public class Magician implements MindReader{
private String thoughts;
// 声明参数化切点
@Pointcut("execution(* com.shiftycat.concert."
+ "Thinker.thinkOfSomething(String)) && args(thoughts)")
public void thinking(String thoughts) {
}
// 把参数传递给通知
@Before("thinking(thoughts)")
public void interceptThoughts(String thoughts) {
System.out.println("侦听志愿者的心声");
this.thoughts=thoughts;
}
public String getThoughts() {
return thoughts;
}
}
<aop:pointcut>
变为@Pointcut
注解,<aop:before>
变为@Before
注解。
测试:
@Test
public void TestIntercept() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
Volunteer volunteer = context.getBean(Volunteer.class);
volunteer.thinkOfSomething("演出真精彩!");
Magician magician = (Magician) context.getBean("magician");
System.out.println("志愿者心里想的是:"+magician.getThoughts());
}
输出:
侦听志愿者的心声
志愿者心里想的是:演出真精彩!
4. 通过注解引入新的方法
之前通过在XML中配置AOP切面的方法为Bean引入新的方法,现在改用注解的方式来实现:
新建一个ContestantIntroducer类:
@Aspect
public class ContestantIntroducer {
@DeclareParents(
value = "com.shiftycat.concert.Performance+",
defaultImpl = OutstandingContestant.class)
public static Contestant contestant;
}
@DeclareParents
注解代替了之前的<aop:declare-parents>
标签。 @DeclareParents
注解由三个部分组成:
value
属性等同于<aop:declare-parents>
的types-matching
属性。它标识应该被引入指定接口的Bean。defaultImpl
属性等同于<aop:declare-parents>
的default-impl
属性。它标识该类所引入接口的实现。- 由
@DeclareParents
注解所标注的static属性制订了将被引入的接口。
<bean class="com.shiftycat.concert.ContestantIntroducer" />
测试:
@Test
public void performXMLTest() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("concert-config.xml");
Performance performance = (Performance) context.getBean("rocketStar");
performance.perform();
Contestant contestant = (Contestant) context.getBean("rocketStar");
contestant.receiveAward();
}
输出:
观众入座了
关闭手机了
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
梁博演唱《出现又离开》
鼓掌了: 啪啪啪啪啪
参加颁奖典礼
《Spring实战(第四版)》