依赖注入 DI 有助于应用对象之间的解耦,而 AOP 可以实现横切关注点与它们所影响的对象之间的解耦。
1 AOP 术语
1.1 通知
在 AOP 的术语中,切面的工作被称为 通知,通知定义了切面是什么以及何时使用。Spring 切面可以应用五种类型的通知:
- 前置通知 Before:在目标方法被调用之前调用通知功能
- 后置通知 After:在目标方法完成之后调用通知,注意:此时不会关心方法的输出是什么
- 返回通知 After-returnng:在目标方法完成之后调用通知功能
- 异常通知 After-throwing:在目标方法抛出异常后调用通知功能
- 环绕通知 Arround:通知包裹了被通知的方法,即在被通知的方法调用之前和调用之后执行通知功能
1.2 连接点
连接点是程序执行过程中能够应用通知的所有点,注意:连接点不是切点。
1.3 切点
如果说通知定义了切面的 “什么” 和 “何时” 的话,切点就是定义了 “何处”,切点定义了通知被应用的具体位置(在哪些连接点)
1.4 切面
切面就是通知和切点的结合,通知和切点共同定义了切面的全部内容
1.5 织入
织入是白切面引用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中,在目标对象的生命周期里有多个点可以进行织入操作:
- 编译期:切面在密保类编译的时候被织入,这种方式需要特殊的编译器,AspectJ 的织入编译器就是以这种方式织入切面的
- 类加载器:切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器
- 运行期:切面在引用运行的某个时刻被织入,一般情况下,在织入切面时,AOP 容器会为目标动态地创建一个代理对象,Spring 的 AOP 就是一这种方式织入切面的,因此,Spring AOP 构建在动态代理基础之上,所以 Spring 对 AOP 的支持局限于方法的拦截
2 Spring AOP
2.1 编写切点
Spring AOP 仅支持 AspectJ 其诶点指示器的一个子集,以下是 Spring AOP 所支持的 AspectJ 切点指示器:
-
arg()
:限制连接点匹配参数为指定类型的执行方法 -
@args()
:限制连接点匹配参数由 指定注解标注 的执行方法 -
execution()
:用于匹配是连接点的执行方法 -
this()
:限制连接点匹配 AOP 代理的 bean 引用为指定类型的类 -
target
:限制连接点匹配目标对象为指定类型的类 -
@target()
:限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 -
within()
:限制连接点匹配指定的类型 -
@within()
:限制连接点匹配指定注解所标注的类型 -
@annotation
:限定匹配带有指定注解的连接点
上图展现一个切点表达式,这个表达式能设置当perform()
方法执行处罚通知的调用,步骤是: -
首先我们使用的是
execution()
指示器,方法表达式以*
号开始,表明了我们并不关心方法返回值的类型。 -
其次,指定了全限定类名和方法名给
execution()
提供了切点,对于方法参数列表使用了..
表明切点要选择任意的perform()
方法,无论该方法的入参是什么。
如果需要配置的切点仅匹配 concert
包,此时可以使用 within()
指示器来限制匹配,同时使用多个指示器可以使用 &&
、||
、!
、and
、or
、not
这些逻辑运算符,如:
execution(* concert.Performance.perform(..)) && within(concert.*)
同时,Spring 还引入了一个新的 bean()
指示器,它允许我们在切点表达式中使用 bean 的 ID 来标识 bean,如:
execution(* concert.Performance.perform()) and bean('woodstock')
这样在执行 Performance.perform()
方法时应用通知,但限定 bean 的 ID 为 woodstock
。
2.2 创建切面
使用 @Aspect
注解来创建切面是 AspectJ5 所引入的关键特性,该注解表明被注解的类不仅是一个 POJO,还是一个切面,该类中的所有方法都使用注解来定义切面的具体行为,Spring 使用以下 AspectJ 注解来声明通知方法:
@After
:上文所说的后置通知@AfterReturning
:返回通知@AfterThrowing
:异常通知@Around
:环绕通知@Before
:前置通知
eg:
package top.seiei.aspects;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import top.seiei.bean.Minstrel;
@Aspect
public class minstrelByAspect {
@Autowired
private Minstrel minstrel;
@Before("execution(* top.seiei.bean.BraveKnight.embarkOnQuest(..))")
public void singBeforeQuest() {
minstrel.singBeforeQuest();
}
@AfterReturning("execution(* top.seiei.bean.BraveKnight.embarkOnQuest(..))")
public void singAfterQuest() {
minstrel.singAfterQuest();
}
}
上述方法声明了 BraveKnight
类中 embarkOnQuest()
方法为切点,并在 embarkOnQuest()
方法的调用之前及调用之后的时候分别调用了 Minstrel
类的 singBeforeQuest
方法以及 singAfterQuest
方法,与此同时,还需要将该切面类装配到 Spring 中的 bean,跟普通的装配方式一样,但止步于此的话,它并不会被视为切面,这些注解也不会被解析,更不会创建将其转换为切面的代理。
此时,如果使用的是 JavaConfig 的话,可以在配置类的 类级别 上通过使用 @EnableAspectJAutoProxy
注解启用自动代理功能,eg:
package top.seiei.springConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import top.seiei.aspects.MinstrelByAspect;
import top.seiei.bean.BraveKnight;
import top.seiei.bean.Minstrel;
import top.seiei.bean.SlayDragonQuest;
/**
* @EnableAspectJAutoProxy 注解用于启用 AspectJ 自动代理
*/
@Configuration
@EnableAspectJAutoProxy
public class BraveKnightConfigBak {
@Bean
public Minstrel minstrel(){
return new Minstrel();
}
@Bean
public MinstrelByAspect minstrelByAspect() {
return new MinstrelByAspect();
}
@Bean
public SlayDragonQuest slayDragonQuest() {
return new SlayDragonQuest();
}
@Bean
public BraveKnight braveKnight(SlayDragonQuest slayDragonQuest) {
return new BraveKnight(slayDragonQuest);
}
}
而 XML 配置就可以添加 <aop:aspectj-autoproxy>
元素达到相同的效果
2.3 @PoinCut
在上述的 MinstrelByAspect
例子中,可以为一个切点声明添加多种类型的通知的时候,会频繁地使用相同的切点表达式,此时使用 @PoinCut
注解就可以了,eg:
@Aspect
public class MinstrelByAspect {
@Autowired
private Minstrel minstrel;
/**
* @Poincut 注释用于定义命名的切点,注意该注释声明在方法之前
*/
@Pointcut("execution(* top.seiei.bean.BraveKnight.embarkOnQuest(..))")
public void embarkOnQuest() {}
@Before("embarkOnQuest()")
public void singBeforeQuest() {
minstrel.singBeforeQuest();
}
@AfterReturning("embarkOnQuest()")
public void singAfterQuest() {
minstrel.singAfterQuest();
}
}
2.4 环绕通知
环绕通知实际上就像在一个通知方法中同时编写前置通知和后置通知。创建环绕通知的方式与其它通知的创建有些许不同,环绕通知需要接受一个 ProceedingJoinPoint
对象作为参数,它实质存储了被通知的方法,在通知方法中做完所需要的事情后,一定要将控制权交给被通知的方法,此时调用 ProceedingJoinPoint
的 proceed
方法,当然,不交还控制权也是可以的。
@Around("embarkOnQuest()")
public void singAroundQuest(ProceedingJoinPoint joinPoint) {
try {
minstrel.singBeforeQuest();
joinPoint.proceed();
minstrel.singAfterQuest();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
2.5 通知访问传递给被通知方法的参数
切面可以访问和使用传递给被通知方法的参数,此时需要用到的是 @args
指示器,它表明传递到被通知的方法的类型参数也会传递到通知中去,eg:
execution(* soundsystem,CompactDisc.palyTrack(int)) && args(trackNumber)
详细的例子:
@Aspect
public class RegisterCDCounter {
public Map<Integer, Integer> cdCounts = new HashMap<>();
@Pointcut("execution(* top.seiei.bean.CDPlayer.registerCD(Integer)) && args(cdId)")
public void trackRegisterCD(Integer cdId) {};
@AfterReturning("trackRegisterCD(cdId)")
public void countTrack(Integer cdId) {
Integer currentCount = cdCounts.containsKey(cdId) ? cdCounts.get(cdId) : 0;
cdCounts.put(cdId, currentCount + 1);
}
}
需要注意的是:args
的指示器参数名称必须与通知中的参数相匹配。