Spring AOP 详解

一、概述

AOP:
⾯向切⾯编程,让程序员关注于类中方法的某个切点上
在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复
默认情况下,Spring 会根据被代理对象是否实现接⼝来选择使⽤ JDK 还是 CGLIB。当被代理对象没有实现 任何接⼝时,Spring 会选择 CGLIB。当被代理对象实现了接⼝,Spring 会选择 JDK 官⽅的代理技术,不过 我们可以通过配置的⽅式,让 Spring 强制使⽤ CGLIB。

  • 通知(Advice):通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题
    • 前置通知(@Before):在目标方法被调用之前调用通知功能
    • 后置通知(@After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
    • 返回通知(@AfterReturning):在目标方法成功执行之后调用通知
    • 异常通知(@AfterThrowing):在目标方法抛出异常后调用通知
    • 环绕通知(@Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
  • 连接点(Joinpoint):要增强代码的地方(方法开始、结束、异常时等时机点)。只是候选点,最终是否增强代码得看后续逻辑
  • 切点(Pointcut):已经把增强代码加入到业务主线之后的连接点
  • 引入:引入允许我们向现有的类添加新方法或属性
  • 织入(Weaving):把增强应用到目标对象老创建新的代理对象的过程。Spring 采用动态代理织入,而 Aspect 采用编译期织入和类装载期织入
  • 切面(Aspect):切面 = 通知 + 切点。通知和切点共同定义了切面的全部内容
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>

二、切点

Spring 借助 AspectJ 的切点表达式语言来定义 Spring 切面

AspectJ 指示器 描述
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数由指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配 AOP 代理的 bean 引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within() 限制连接点匹配指定注解锁标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里)
@annotation 限定匹配带有指定注解的连接点


image.png

1. 限定 Bean 的 ID

1
execution(* concert.Performance.perform()) and bean('woodstock')

2. 通过注解定义

1
2
3
4
5
6
7
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}

@Before("performance()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

3. 使用参数化通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Player.play(int):被通知方法的参数类型 
// args(trackNumber):被通知方法参数传递给通知方法中的 trackNumber 形参,即 pointcut 方法中的 trackNumber
@Pointcut("execution(* com.san.spring.aop.Player.play(int)) && args(trackNumber)")
public void pointcut(int trackNumber) {}

@Around("pointcut(trackNumber)")
public void countTrack(ProceedingJoinPoint pjp, int trackNumber) {
try {
pjp.proceed();
int currentCount = getTrackCurrentCount(trackNumber);
map.put(trackNumber, ++currentCount);
} catch (Throwable e) {
...
}
}

三、创建切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Aspect
@Component
public class Audience {
@Before("execution(** cocert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

@Before("execution(** concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}

@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}

@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}

@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
// 执行目标方法
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}

1. 使用自动代理

JavaConfig:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}

XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy />
<bean class="concert.Audience" />
</beans>

2. 在 XML 中声名切面

AOP 配置元素 用途
aop:advisor 定义 AOP 通知器
aop:after 定义 AOP 后置通知
aop:after-returning 定义 AOP 返回通知
aop:after-throwing 定义 AOP 异常通知
aop:around 定义 AOP 环绕通知
aop:aspect 定义一个切面
aop:aspectj-autoproxy 启用 @AspectJ
注解驱动的切面
aop:before 定义一个 AOP 前置通知
aop:config 顶级的 AOP 配置元素。大多数的 aop:* 元素必须包含在 aop:config 元素内
aop:declare-parents 以透明的方式为被通知的对象引入额外的接口
aop:pointcut 定义一个切点

具体方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Audience {    
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}

public void takeSeats() {
System.out.println("Taking seats");
}

public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}

public void demandRefund() {
System.out.println("Demanding a refund");
}

public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
// 执行目标方法
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable e) {
System.out.println("Demanding a refund");
}
}
}

XML 配置:

1
2
3
4
5
6
7
8
9
10
11
<aop:config>
<aop:aspect ref="audience">
<!-- 切点 -->
<aop:pointcut id="performance" expression="execution(** concert.Performance.perform(..))"/>
<aop:before pointcut-ref="performance" method="silenceCellPhones"/>
<aop:before pointcut-ref="performance" method="takeSeats"/>
<aop:after-returning pointcut="execution(** concert.Performance.perform(..))" method="applause"/>
<aop:after-throwing pointcut="execution(** concert.Performance.perform(..))" method="demandRefund"/>
<aop:around pointcut-ref="performance" method="watchPerformance"/>
</aop:aspect>
</aop:config>

2.1. 为通知传递参数 —— XML

具体方法:

1
2
3
4
5
6
7
8
9
10
public class TrackCounter {    
private Map<Integer, Integer> trackCounts = new HashMap<>();
public void countTrack(int trackNumber) {
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount + 1);
}
public int getPlayCount(int trackNumber) {
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}

XML 配置:

1
2
3
4
5
6
7
8
<aop:config>
<aop:aspect ref="trackCounter">
<!-- 切点 -->
<aop:pointcut id="trackPlayed" expression="execution(* soundsystem.CompactDisc.playTrack(int))
and args(trackNumber)"/>
<aop:before pointcut-ref="trackPlayed" method="countTrack"/>
</aop:aspect>
</aop:config>

2.2. 通过切面引入新的功能 —— XML

  • type-matching: 指定目标接口
  • implement-interface: 指定要增加的方法所在的接口
  • default-impl: 指定要增加的方法的实现,也可以由 delegate-ref=”beanID” 来代替从而导入 Spring 容器中的 Bean
1
2
3
4
5
6
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"/>
</aop:aspect>

四、通过注解引入新功能

通过建立一个代理类同时代理 A 和 B,调用者调用该代理时,就可以同时 A 和 B 中的方法了。

目标类:

1
2
3
4
5
6
7
@Component
public class Women implements Person {
@Override
public void likePerson() {
System.out.println("我是女生,我喜欢帅哥");
}
}

被引入的类:

1
2
3
4
5
6
7
@Component
public class FemaleAnimal implements Animal {
@Override
public void eat() {
System.out.println("我是雌性,我比雄性更喜欢吃零食");
}
}

切面:

1
2
3
4
5
6
7
@Aspect
@Component
public class AspectConfig {
// "+"表示 person 的所有子类;defaultImpl 表示默认需要添加的新的类
@DeclareParents(value = "com.ccomma.annotation.Person+", defaultImpl = FemaleAnimal.class)
public Animal animal;
}

五、源码

  • 在 bean 的后初始化方法(后置处理器)applyBeanPostProcessorsAfterInitialization 中执行了 AbstractAutoProxyCreator#postProcessAfterInitialization 方法
    • 内部执行了 AbstractAutoProxyCreator#wrapIfNecessary 方法
      • getAdvicesAndAdvisorsForBean:查找出和当前 bean 匹配的 advisor
      • AbstractAutoProxyCreator#createProxy:把委托对象的 aop 增强和通用拦截进行合并,最终给代理对象,内部通过代理工厂 ProxyFactory#getProxy 创建代理
        • 代理工厂内部最终调用 DefaultAopProxyFactory#createAopProxy 来依据情况创建 jdk 代理或 cglib 代理