一. 什么是 Spring AOP
关于什么是 Spring AOP. 当我翻到官方文档的时候也是一惊
Let us begin by defining some central AOP concepts and terminology. These terms are not Spring-specific… unfortunately, AOP terminology is not particularly intuitive; however, it would be even more confusing if Spring used its own terminology.
什么意思呢 ? 大概意思就是 Spring 官方说这些概念不是 Spring 独有的, 而是已经存在了的. 并且这些术语都不是很直观, 如果使用 SPring 自己的术语, 将会让人更加困惑.
当说到这的时候, 相信你已经知道了. 不是 Spring 特有的概念这不是关键, 主要的是连官方都说了 Spring AOP 的概念非常抽象, 理解起来会有一定的难度. 大致来看看到底什么是 SPring AOP, 它又到底是能做些什么 ?
1. Spring AOP 作用
现在无论是什么系统或者应用, 在使用之前都需要进行用户登陆验证. 除了登陆以及注册一面不需要验证以外, 其余功能基本都需要登陆才能使用. 因此想要完成这件事, 在之前我们是在所有需要验证的页面中通过 Controller 来登陆验证.
当你的功能越来越多之时, 你需要写的登陆验证也越来越多, 但是这些方法又是相同的, 会无故增加你很多代码修改和维护的成本. 那么如何将它单独抽离出来做成一个大家都能使用的功能, 让其他页面直接调用就能判定登录就是目前需要解决的.
在上面的基础之上, Spring AOP 的诞生就让其很好地进行了解决. 那么现在能解释什么是 Spring AOP 了吧 ?
AOP 是一种思想, Spring AOP 它是一种框架, 它提供了一种对 AOP 的具体实现. 类似于我们之前学的 Ioc 和 DI 之间的关系. 简单来说, AOP 干的是将某一个统一的功能集中处理.
- 统一的用户登录
- 统一的日志记录
- 统一的方法执行时间统计
- 统一的返回格式
- 统一的异常处理
- 统一的事务开启和提交
除了这些, 还有很多. 也就是说使用 AOP 可以扩充多个对象的某种能力. ( 张三、李四都具有相同的登陆能力等等 ).
2. AOP 的组成
2.1 Aspect 切面
什么是切面 ?
指的是横切多个类的一种模块. 在 Spring 中切面用的就是普通的类 ( XML 或者 @Aspect 注解配置 ).这么一听好像很抽象, 比如我们刚刚说的登陆页面模块, 查看他人文章, 发布文章等等, 在执行自己对应的功能之前进行登录验证. 那么为了处理这个问题, 创建了一个普通类来集中处理. 而这个类就是切面.
这里的多个类就是执行对应的功能类, 而横切就是登陆. 把登陆创建成一个集中模块来处理就是切面.
2.2 Joint point
Joint Point : 连接点, 表示要横切的方法. 就是执行 AOP 功能的所有方法
例如上面登陆说的, 查看他人文章和发布文章的功能方法都是连接点.
2.3 Pointcut 切点
所谓的切点就是从哪里开始入手的意思. 也就是对于那些连接点起作用.
例如上面登陆说的, 可以从查看他人文章和发布文章这两个功能切入, 也就是定义的 AOP 只对这两个起作用, 而对于注册功能是不起作用的, 因为它不需要实现登录检验.
2.4 Advice 通知
通知, 是非常复杂的. 它里面定义了切面是什么, 什么时候使用. 描述了切面要完成的工作, 还解决什么时候执行这个工作的问题.
关于 Advice 通知它有很多类型, 可以在方法上使用一下注解, 会设置改方法为通知方法, 在满足条件以后会通知本方法进行调用
2.4.1 前置通知 : @Before
通知方法会在目标方法 ( 也就是连接点 ) 调用之前执行
2.4.2 后置通知 : @After
通知方法会在目标方法 ( 连接点 ) 调用之后执行.
PS : 无论连接点是正常结束还是异常结束都会执行
2.4.3 返回通知 : @AfterReturning
通知方法会在目标方法 ( 连接点 ) 返回之后调用
PS : 需要方法正常 return 并且没有抛出异常
2.4.4 异常通知 : @AfterThrowing
通知方法会在目标方法 ( 连接点 ) 异常之后调用
2.4.5 环绕通知 : @Around
通知包裹了被通知的方法, 在被通知的方法 ( 连接点 ) 通知之前和调用之后执行的自定义的行为
二. Spring AOP 实现
知道了什么是 Spring AOP 还有 AOP 的组成等等, 现在就可以来简单实现一个统一的登陆功能了.
1. 添加 Spring AOP 框架
因为我们的 Spring Boot 框架没有内置 Spring AOP 因此, 我们需要手动引入依赖. 而 Spring 框架中是有 Spring AOP 依赖的. 但我此处是 Spring Boot 项目. 需要引入 Spring Boot 框架支持的 Spring AOP 框架.
上 Maven 仓库 中进行引入依赖, 一定是引入的 Spring Boot 支持的 Spring AOP 依赖
选择一个相应的版本进行 pom.xml 中引入依赖即可
<!-- 添加的是 Spring Boot 项目的 AOP 依赖, 而原生的 AOP 依赖在 Spring 中使用 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.7.12</version> </dependency>
2. 定义切面
引入好了 Spring AOP 以后, 对登陆功能进行统一处理, 需要先建立切面, 也就是前所说的一个普通类来集中处理
@Aspect // 添加切面注解 @Component // 随 Spring 框架启动而注入 // 创建切面 - 关于某一操作的统一类 public class UserAOP { }
可以看到, 我添加了 @Aspect 和 @Component 注解.
- @Aspect 注解也就是切面注解, 表明我们当前这个普通类是一个切面
- @Component 注解不陌生, 将其添加到我们的 Spring 容器当中供外部使用. 为什么呢 ?
原因也很简单就是为了在 Spring 框架启动时就注入, 否则其他功能进行访问时, 还未成功加载, 就无法进行统一的处理了.
3. 定义切点
切点我们刚刚也说了, 它是对我们的连接点进行匹配的, 看那些连接点是我们需要处理的, 那些是我们不需要处理的, 从而提高效率. 并且切点是必须定义在切面里的
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void doPointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 }
@Pointcut 切点注解, 里面需要配置 execution 也就是需要处理的
而里面配置就是我们具体需要匹配的连接点, 相当于拦截的规则. 而这里面是如何设置的, 采用的是 aspectj 的语法
3.1 aspectj 语法
Aspectj 支持的三种通配符 :
1.* : 匹配任意字符, 只匹配一个元素 ( 包, 类, 方法, 或者方法参数 )**
2. … : 匹配任意字符, 可以匹配多个元素, 在表示类的时候必须和 * 联合使用
3. + : 表示按照类型匹配指定类的所有类, 必须跟在类名后面
如 com.example.demo.User + 这就表示继承 com.example.demo 包底下的 User 类的所有子类, 也包括 User 本身
execution 的组成包括 : ( 修饰符 + 返回类型 + 包.类.方法(参数) + 异常 )
- 修饰符
在 aspectj 中 修饰符一般可以省略. 如果不省略的话, 可以进行声明, 例如
( public + 返回类型 + 包.类.方法(参数) + 异常 )
当然, 除了指定特定返回值以外, 还可以使用通配符一次性匹配所有的修饰符
( * + 返回类型 + 包.类.方法(参数) + 异常 )
- 返回类型
返回值是不可以省略的. 和修饰符一样, 可以指定返回值或者使用通配符
- 包.
固定包 : com.example.demo
固定包底下的任意子包 : com.example.demo.*.service
即 com.example.demo 固定包下面的 service 包
固定包底下的所有包 : com.example.demo…
即 com.example.demo 固定包下的所有包, 包括本身
- 类.
指定类 : UserController
Controller : 以 Controller 结尾
User : 以 User 开头
- 方法
方法名不能省略
fun : 固定方法
fun* : 以 fun 开头的方法
*fun : 以 fun 结尾的方法
通配符 * : 任意方法
- 参数
( ) : 无参
( int ) : 指定 int 类型参数
( int, int ) : 指定两个 int 类型的参数
( … ) : 表示任意参数
- 异常
异常一般省略不写
3.2 实现切点
了解了 Aspectj 语法以后, 现在就可以在 @Pointcut 注解里配置 execution 拦截规则了.
例如, 我想拦截 com.example.demo.controller 包下的 UserController 类任意参数并且返回值任意的所有方法
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))") public void doPointcut() { // 切点只是为了配置规则, 并非具体实现. 因此为空方法 }
可以看到, 我在里面提供了一个没有方法体的空方法 doPointcut( ), 需要注意的是, **切点它只是为连接点是否执行 AOP 设置匹配规则的, 并非具体的连接点功能实现. **
4. 建立连接点
由于刚刚的切点配置的是 com.example.demo.controller.UserController , 因此起作用的连接点只能在这里面, 而在这外面的连接点都是不起作用的.
在指定的类里写我们的连接点和具体连接方法实现, 我们上面说的查看文章和发布文章
@RequestMapping("/user/article") public String login1() { System.out.println("执行了查看文章功能中的登陆检验"); return "Spring AOP"; }
@RequestMapping("user/write") public String login2() { System.out.println("执行了写文章功能中的登录检验"); return "Spring AOP"; }
5. 建立通知
为了更方便我们查看是否真的执行了 Spring AOP 的统一登录功能, 我们可以在切面中实现一个通知来帮助我们观察, 下面就举例一个前置通知
// 配置前置通知 @Before("pointcut()") // 里面填写针对那个切点的通知, 可以有多个切点 public void doBefore() { System.out.println("执行 before 前置通知 : 登陆检验" ); }
通知在我们上面的 AOP 组成中说到, 它里面定义了切面是什么, 什么时候使用. 这里的定义了切面指的就是是因为我们通知的一般主体是切面. **可以理解为这个通知就是通知这个统一的处理去干什么 ( 切点 - 拦截那些内容 ), 什么时候执行( 在连接点之前, 之后等, 具体看什么通知 ) **
6. 检验统一登录
当我们想使用写文章和查看文章功能的时候, 需要先经过 Spring AOP 的统一登录检验, 来看看我们的这两个路由方法是否能正确在 Spring AOP 中执行
- 查看文章功能检验统一登录
可以看到, 当我们使用查看文章时, 它便会进行指定的统一登录检验, 触发切面中的通知方法.
- 写文章功能
同样的, 当我们执行写文章功能时, 它也会先进行统一登录的检验, 触发切面中的通知方法
当然, 上面还是不够直观, 我们可以创建一个切点以外 ( 和切点路径不匹配的 ) 的连接点
我在 controller 包底下建立了一个 RegisterController 类, 来表示我的注册类
@RestController public class RegisterController { @RequestMapping("/user/reg") public String register() { System.out.println("执行注册功能 -> "); return "Spring AOP"; } }
显然, 我们的注册方法是不需要经过统一的登陆处理的. 因此预期它不会触发我们切面中的通知方法
可以看到, 我们虽然执行了注册功能, 但是并没有触发通知方法, 也就是说并没有进行统一的登陆检验.