1. 什么是 AOP?
AOP 是 Aspect Oriented Programming 的缩写, 意思是: 面向切面编程, 它是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.
可以认为 AOP 是对 OOP(Object Oriented Programming 面向对象编程) 的补充, 主要使用在日志记录, 性能统计, 安全控制等场景, 使用 AOP 可以使得业务逻辑各部分之间的耦合度降低, 只专注于各自的业务逻辑实现, 从而提高程序的可读性及维护性.
比如, 我们需要记录项目中所有对外接口的入参和出参, 以便出现问题时定位原因, 在每一个对外接口的代码中添加代码记录入参和出参当然也可以达到目的, 但是这种硬编码的方式非常不友好, 也不够灵活, 而且记录日志本身和接口要实现的核心功能没有任何关系.
此时, 我们可以将记录日志的功能定义到 1 个切面中, 然后通过声明的方式定义要在何时何地使用这个切面, 而不用修改任何 1 个外部接口.
在讲解具体的实现方式之前, 我们先了解几个 AOP 中的术语.
1.1 通知 (Advice)
在 AOP 术语中, 切面要完成的工作被称为通知, 通知定义了切面是什么以及何时使用.
Spring 切面有 5 种类型的通知, 分别是:
前置通知 (Before): 在目标方法被调用之前调用通知功能
后置通知 (After): 在目标方法完成之后调用通知, 此时不关心方法的输出结果是什么
返回通知 (After-returning): 在目标方法成功执行之后调用通知
异常通知 (After-throwing): 在目标方法抛出异常后调用通知
环绕通知 (Around): 通知包裹了被通知的方法, 在被通知的方法调用之前和调用之后执行自定义的行为
1.2 连接点 (Join point)
连接点是在应用执行过程中能够插入切面的一个点, 这个点可以是调用方法时, 抛出异常时, 修改某个字段时.
1.3 切点 (Pointcut)
切点是为了缩小切面所通知的连接点的范围, 即切面在何处执行. 我们通常使用明确的类和方法名称, 或者利用正则表达式定义所匹配的类和方法名称来指定切点.
1.4 切面 (Aspect)
切面是通知和切点的结合. 通知和切点共同定义了切面的全部内容: 它是什么, 在何时和何处完成其功能.
1.5 引入 (Introduction)
引入允许我们在不修改现有类的基础上, 向现有类添加新方法或属性.
1.6 织入 (Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程.
切面在指定的连接点被织入到目标对象中, 在目标对象的生命周期里, 有以下几个点可以进行织入:
编译期: 切面在目标类编译时被织入. 这种方式需要特殊的编译器. AspectJ 的织入编译器就是以这种方式织入切面的.
类加载期: 切面在目标类加载到 JVM 时被织入. 这种方式需要特殊的类加载器 (ClassLoader), 它可以在目标类被引入应用之前增强该目标类的字节码.
运行期: 切面在应用运行的某个时刻被织入. 一般情况下, 在织入切面时, AOP 容器会为目标对象动态地创建一个代理对象. Spring AOP 就是以这种方式织入切面的.
2. Spring 对 AOP 的支持
2.1 动态代理
Spring AOP 构建在动态代理之上, 也就是说, Spring 运行时会为目标对象动态创建代理对象.
代理类封装了目标类, 并拦截被通知方法的调用, 再把调用转发给真正的目标 bean.
当代理类拦截到方法调用时, 在调用目标 bean 方法之前, 会执行切面逻辑.
2.2 织入切面时机
通过在代理类中包裹切面, Spring 在运行期把切面织入到 Spring 管理的 bean 中, 也就是说, 直到应用需要被代理的 bean 时, Spring 才会创建代理对象.
因为 Spring 运行时才创建代理对象, 所以我们不需要特殊的编译器来织入 Spring AOP 切面.
2.3 连接点限制
Spring 只支持方法级别的连接点, 如果需要字段级别或者构造器级别的连接点, 可以利用 AspectJ 来补充 Spring AOP 的功能.
3. Spring AOP 使用
假设我们有个现场表演的接口 Performance 和它的实现类 SleepNoMore:
- package chapter04.concert;
- /**
- * 现场表演, 如舞台剧, 电影, 音乐会
- */
- public interface Performance {
- void perform();
- }
- package chapter04.concert;
- import org.springframework.stereotype.Component;
- /**
- * 戏剧:《不眠之夜 Sleep No More》
- */
- @Component
- public class SleepNoMore implements Performance {
- @Override
- public void perform() {
- System.out.println("戏剧《不眠之夜 Sleep No More》");
- }
- }
既然是演出, 就需要观众, 假设我们的需求是: 在看演出之前, 观众先入座并将手机调整至静音, 在观看演出之后观众鼓掌, 如果演出失败观众退票, 我们当然可以把这些逻辑写在上面的 perform() 方法中, 但不推荐这么做, 因为这些逻辑理论上和演出的核心无关, 就算观众不将手机调整至静音或者看完演出不鼓掌, 都不影响演出的进行.
针对这个需求, 我们可以使用 AOP 来实现.
3.1 定义切面
首先, 定义一个观众的切面如下:
- package chapter04.concert;
- import org.aspectj.lang.annotation.Aspect;
- /**
- * 观众
- * 使用 @Aspect 注解定义为切面
- */
- @Aspect
- public class Audience {
- }
注意事项:@Aspect 注解表明 Audience 类是一个切面.
3.2 定义前置通知
在 Audience 切面中定义前置通知如下所示:
- /**
- * 表演之前, 观众就座
- */
- @Before("execution(* chapter04.concert.Performance.perform(..))")
- public void takeSeats() {
- System.out.println("Taking seats");
- }
- /**
- * 表演之前, 将手机调至静音
- */
- @Before("execution(* chapter04.concert.Performance.perform(..))")
- public void silenceCellPhones() {
- System.out.println("Silencing cell phones");
- }
这里的重点代码是 @Before("execution(* chapter04.concert.Performance.perform(..))"), 它定义了 1 个前置通知, 其中 execution(* chapter04.concert.Performance.perform(..)) 被称为 AspectJ 切点表达式, 每一部分的讲解如下:
@Before: 该注解用来定义前置通知, 通知方法会在目标方法调用之前执行
execution: 在方法执行时触发
*: 表明我们不关心方法返回值的类型, 即可以是任意类型
chapter04.concert.Performance.perform: 使用全限定类名和方法名指定要添加前置通知的方法
(..): 方法的参数列表使用 (..), 表明我们不关心方法的入参是什么, 即可以是任意类型
3.3 定义后置通知
在 Audience 切面中定义后置通知如下所示:
- /**
- * 表演结束, 不管表演成功或者失败
- */
- @After("execution(* chapter04.concert.Performance.perform(..))")
- public void finish() {
- System.out.println("perform finish");
- }
注意事项:@After 注解用来定义后置通知, 通知方法会在目标方法返回或者抛出异常后调用
3.4 定义返回通知
在 Audience 切面中定义返回通知如下所示:
- /**
- * 表演之后, 鼓掌
- */
- @AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
- public void applause() {
- System.out.println("CLAP CLAP CLAP!!!");
- }
注意事项:@AfterReturning 注解用来定义返回通知, 通知方法会在目标方法返回后调用
3.5 定义异常通知
在 Audience 切面中定义异常通知如下所示:
- /**
- * 表演失败之后, 观众要求退款
- */
- @AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
- public void demandRefund() {
- System.out.println("Demanding a refund");
- }
注意事项:@AfterThrowing 注解用来定义异常通知, 通知方法会在目标方法抛出异常后调用
3.6 定义可复用的切点表达式
细心的你可能会发现, 我们上面定义的 5 个切点中, 切点表达式都是一样的, 这显然是不好的, 好在我们可以使用 @Pointcut 注解来定义可重复使用的切点表达式:
- /**
- * 可复用的切点
- */
- @Pointcut("execution(* chapter04.concert.Performance.perform(..))")
- public void perform() {
- }
然后之前定义的 5 个切点都可以引用这个切点表达式:
- /**
- * 表演之前, 观众就座
- */
- @Before("perform()")
- public void takeSeats() {
- System.out.println("Taking seats");
- }
- /**
- * 表演之前, 将手机调至静音
- */
- @Before("perform()")
- public void silenceCellPhones() {
- System.out.println("Silencing cell phones");
- }
- /**
- * 表演结束, 不管表演成功或者失败
- */
- @After("perform()")
- public void finish() {
- System.out.println("perform finish");
- }
- /**
- * 表演之后, 鼓掌
- */
- @AfterReturning("perform()")
- public void applause() {
- System.out.println("CLAP CLAP CLAP!!!");
- }
- /**
- * 表演失败之后, 观众要求退款
- */
- @AfterThrowing("perform()")
- public void demandRefund() {
- System.out.println("Demanding a refund");
- }
3.7 单元测试
新建配置类 ConcertConfig 如下所示:
- package chapter04.concert;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.ComponentScan;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.EnableAspectJAutoProxy;
- @Configuration
- @EnableAspectJAutoProxy
- @ComponentScan
- public class ConcertConfig {
- @Bean
- public Audience audience() {
- return new Audience();
- }
- }
注意事项: 和以往不同的是, 我们使用了 @EnableAspectJAutoProxy 注解, 该注解用来启用自动代理功能.
新建 Main 类, 在其 main() 方法中添加如下测试代码:
- package chapter04.concert;
- import org.springframework.context.annotation.AnnotationConfigApplicationContext;
- public class Main {
- public static void main(String[] args) {
- AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);
- Performance performance = context.getBean(Performance.class);
- performance.perform();
- context.close();
- }
- }
运行代码, 输出结果如下所示:
- Silencing cell phones
- Taking seats
戏剧《不眠之夜 Sleep No More》
- perform finish
- CLAP CLAP CLAP!!!
稍微修改下 SleepNoMore 类的 perform() 方法, 让它抛出一个异常:
- @Override
- public void perform() {
- int number = 3 / 0;
- System.out.println("戏剧《不眠之夜 Sleep No More》");
- }
再次运行代码, 输出结果如下所示:
- Silencing cell phones
- Taking seats
- perform finish
- Demanding a refund
- Exception in thread "main" java.lang.ArithmeticException: / by zero
由此也可以说明, 不管目标方法是否执行成功,@After 注解都会执行, 但 @AfterReturning 注解只会在目标方法执行成功时执行.
值得注意的是, 使用 @Aspect 注解的切面类必须是一个 bean(不管以何种方式声明), 否则切面不会生效, 因为 AspectJ 自动代理只会为使用 @Aspect 注解的 bean 创建代理类.
也就是说, 如果我们将 ConcertConfig 配置类中的以下代码删除或者注释掉:
- @Bean
- public Audience audience() {
- return new Audience();
- }
运行结果将变为:
戏剧《不眠之夜 Sleep No More》
3.8 创建环绕通知
我们可以使用 @Around 注解创建环绕通知, 该注解能够让你在调用目标方法前后, 自定义自己的逻辑.
因此, 我们之前定义的 5 个切点, 现在可以定义在一个切点中, 为不影响之前的切面, 我们新建切面 AroundAudience, 如下所示:
- package chapter04.concert;
- 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 AroundAudience {
- /**
- * 可重用的切点
- */
- @Pointcut("execution(* chapter04.concert.Performance.perform(..))")
- public void perform() {
- }
- @Around("perform()")
- public void watchPerform(ProceedingJoinPoint joinPoint) {
- try {
- System.out.println("Taking seats");
- System.out.println("Silencing cell phones");
- joinPoint.proceed();
- System.out.println("CLAP CLAP CLAP!!!");
- } catch (Throwable throwable) {
- System.out.println("Demanding a refund");
- } finally {
- System.out.println("perform finish");
- }
- }
- }
这里要注意的是, 该方法有个 ProceedingJoinPoint 类型的参数, 在方法中可以通过调用它的 proceed() 方法来调用目标方法.
然后修改下 ConcertConfig 类的代码:
- package chapter04.concert;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.ComponentScan;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.EnableAspectJAutoProxy;
- @Configuration
- @EnableAspectJAutoProxy
- @ComponentScan
- public class ConcertConfig {
- /*@Bean
- public Audience audience() {
- return new Audience();
- }*/
- @Bean
- public AroundAudience aroundAudience() {
- return new AroundAudience();
- }
- }
运行结果如下所示:
- Taking seats
- Silencing cell phones
戏剧《不眠之夜 Sleep No More》
- CLAP CLAP CLAP!!!
- perform finish
4. 源码及参考
源码地址: https://github.com/zwwhnly/spring-action.git , 欢迎下载.
Craig Walls 《Spring 实战 (第 4 版)》
AOP(面向切面编程)_百度百科 https://baike.baidu.com/item/AOP/1332219
5. 最后
来源: https://www.cnblogs.com/zwwhnly/p/11397984.html