AOP (Aspect Oriented Programming) 面向切面编程
AOP 是一种思想, 是对某一类事情的集中处理
AOP 的作用
在程序运行期间, 在不改变源代码的基础上, 对已有方法进行增强 (无侵入性: 解耦)
AOP 的常见用途
- 统一日志思想
- 统一执行方法计时
- 事务的开启和提交
- 统一数据返回格式
- 统一异常处理
Spring AOP
Spring AOP 是 AOP 思想的一种实现
AOP 思想有很多实现 : Spring Boot 统一功能处理 , Spring AOP , AspectJ , CGLIB …
Spring AOP 的实现方式
- 基于注解 @Aspect 实现 (常用)
- 基于 Spring API (xml 文件) 方式实现
- 基于代理实现
Spring Boot 统一功能处理 和 Spring AOP
Spring Boot 统一功能处理 和 Spring AOP 是 相辅相成 的技术
像拦截器作用维度是 url(一次请求和响应), @ControllerAdvice 的应用场景主要是全局异常处理, 数据绑定, 数据预处理.
Spring AOP 的作用维度则可以更加细致 (包, 类, 方法, 参数 …), 能够实现更复杂的业务逻辑
Spring AOP:
Spring AOP 是 Spring 框架的一个模块,用于支持面向切面编程。它通过在方法执行前、执行后或抛出异常时动态地添加横切逻辑,例如日志记录、性能监控、事务管理等。AOP 的主要目的是在不修改原始代码的情况下,通过将横切逻辑与业务逻辑分离,提高代码的模块性和可维护性。
Spring Boot:
Spring Boot 是 Spring 框架的一个扩展,旨在简化基于 Spring 的应用程序的开发和部署。它提供了自动配置、快速构建、嵌入式服务器等特性,使得开发者可以更加便捷地创建独立的、生产级别的 Spring 应用程序。
功能统一处理:
在实际应用中,Spring AOP 可以用于统一处理某些横切关注点,如日志记录、权限控制等,而 Spring Boot 可以用于统一处理应用程序的配置、异常处理、安全性等方面的功能。因此,虽然 Spring AOP 和 Spring Boot 在实现功能统一处理上有一定的重叠,但它们更多地是在不同层面上为应用程序提供支持,而不是直接相关的概念或功能模块。
综上所述,Spring AOP 和 Spring Boot 都可以用于实现功能的统一处理,但它们是不同的模块,各自在不同的层面提供支持,没有直接的关联关系。
Spring AOP 的原理
Spring AOP 基于 动态代理 实现
代理模式(委托模式)
想了解动态代理, 就得先了解代理模式
代理模式 : 为其他对象提供一种代理, 以控制对这个对象的访问. 它的作用是通过一个代理类, 让我们在调用目标方法的时候, 不再是直接调用目标方法 (有可能参数不适配, 返回值不准确 …) , 而是通过调用代理类, 代理类里面再调用目标方法的方式, 间接调用
代理模式主要角色
- Subject : 业务接口类. (不一定有)
- RealSubject : 业务实现类. 具体的业务执行, 即被代理对象
- Proxy : 代理类
代理模式分类
代理模式分为静态代理和动态代理
- 静态代理 : 在程序运行期代理类的 .class 文件已经存在, 是被写死不能修改的代理
- 动态代理 : 在程序运行时, 运用 反射机制 动态创建而成的代理
Java 中动态代理有两种实现: JDK 动态代理 & CGBIG 动态代理
JDK 动态代理, 只能代理接口, 不能代理普通类
CGLIB 动态代理, 能够代理接口和普通类
什么时候使用什么代理?
Spring AOP 中, 两种动态代理模式都使用了, 具体使用哪种方式, 还是依据 版本和配置 实现:
- 在配置文件中, 通过属性 proxyTargetClass 设置使用哪种代理方式
- Spring Boot 2.X 版本 之前, 该属性默认为 false, 即目标对象实现接口, 则使用 jdk 静态代理, 目标方法未实现接口 (只有实现类), 则使用 cglib 动态代理
- Spring Boot 2.X 版本 之后, 该属性默认为 true, 无论目标对象是否实现接口, 都使用 cglib 动态代理
基于 @Aspect 注解实现 Spring AOP
首先要引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
使用 Spring AOP 编写, 对调用的方法计时操作
@Slf4j
@Component
@Aspect
public class SpringAOP {
@Pointcut("execution(* com.zrj.mybatisreview.*.*(..))")
public void pt(){}
//@Around("execution(* com.zrj.mybatisreview.*.*(..))")
@Around("pt()")
public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 使用该方法记录 每个方法被调用的时长
// 记录方法执行开始时间
long begin = System.currentTimeMillis();
// 执行原始方法
Object result = proceedingJoinPoint.proceed();
// 记录方法执行结束时间
long end = System.currentTimeMillis();
// 日志打印方法执行时长
log.info(proceedingJoinPoint.getSignature() + "执行耗时: {}ms", end - begin);
// 返回原始方法运行结果 (不返回运行结果, 就相当于代码没执行 ...)
return result;
}
@Before("pt()")
public void beforeAOP() {
log.info("hello world!");
}
}
Spring AOP 中的一些概念
切点(Pointcut)
也叫做 “切入点”
切点就是 一组规则 (好抽象), 通过切点表达式, 告诉程序应该对哪些方法进行功能增强
@Pointcut
提取出公共的切点表达式, 可供重复使用
连接点(Join Point)
满足切面表达式规则的所有方法, 都成为连接点
可以理解为: 切点是保存了众多连接点的一个集合
通知(Advice)
通知就是具体要做的内容, 指哪些重复的逻辑, 也就是共性功能
上述代码为例就是对每个方法进行计时操作, 并打印日志
切面(Aspect)
切面 = 切点 + 通知
通过切点能够描述出 AOP 的作用范围, 通知则告诉 对切点具体要执行的操作
切面即包含了通知逻辑的定义, 也包含了连接点的定义
切面所在的类, 一般称为 切面类 (被@Aspect
注解标注的类)
一个切面类可以包含多个切点
切面优先级 @Order
- 使用场景: 多切面, 且具有 相同类型 通知情况下, 指定不同通知的执行顺序
- 数字越小, 优先级越高, 同种类型通知, 先执行优先级高的, 在执行优先级低的
Spring AOP 的通知类型
@Around :
环绕通知
@Before :
前置通知
@After :
后置通知
@AfterReturning :
返回后通知
@AfterThrowing :
异常后通知
通知执行顺序
通知注意事项
@Around
环绕通知需要主动调用proceedingJoinPoint.proceed()
方法来让原始方法执行, 其他通知则不需要考虑目标方法的执行@Around
环绕通知方法的返回值, 必须指定为 Object 类型, 来接受原始方法的返回值, 否则原始方法执行完毕, 是获取不到返回值的
切点表达式
execution() :
根据方法签名来 定义切点规则@annotation() :
根据注解来匹配 定义切点规则
execution()
execution()
是最常见的切点表达式, 用来匹配方法, 语法为:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
切点表达式支持通配符匹配
*
匹配任意一个元素..
匹配任意个元素
@annotation
@annotation 常用来匹配多个无规则方法
(一个类中一部分方法需要匹配, 一部分方法不需要匹配, 此时再使用 execution() 就不太方便了)
@annotation 使用流程
- 编写自定义注解 (如果使用本来就有的注解 [eg: @Controller], 可以省略这一步)
- 使用 @annotation 表达式来描述切点
- 再连接点的方法上添加自定义注解
// 注解类代码
package com.zrj.mybatisreview;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeRecord {
// 使用本注解对需要进行计时的方法进行标注
}
// 切面类代码
@Slf4j
@Aspect
@Component
public class TimeRecord {
// @annotation() 中的参数是注解的位置(包名.注解名)
@Before("@annotation(com.zrj.mybatisreview.TimeRecord)")
public void before() {
log.info("TimeRecord -> begin");
}
@After("@annotation(com.zrj.mybatisreview.TimeRecord)")
public void after() {
log.info("TimeRecord -> after");
}
}
对想要计时的方法添加 自定义注解, 该方法就会在调用的时候自动执行 AOP 逻辑