目录
Spring AOP
Spring AOP 常见使用场景
AOP 组成
切面(类)
切点(方法)
通知
编辑
前置通知(@Before)
后置通知(@After)
返回通知(@AfterReturning)
异常通知(@AfterThrowing)
环绕通知(@Around)
连接点
Spring AOP 实现
切点表达式说明
Spring AOP 实现原理
织入
动态代理
JDK 动态代理
CGLIB
Spring AOP
- AOP (Aspect Oriented Programming)译为 面向切面编程
- AOP 为一种思想,是对某一类事情的集中处理
- Spring AOP 是一个框架,提供了对 AOP 思想的实现
- Spring AOP 是 Spring 的三大核心思想之一,另外两个是 IOC(控制反转)和 DI(依赖注入)
实例理解
- 在我们程序中,经常存在一些系统性的需求
- 这些需求代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,具有极高的耦合性
- AOP 的目的就是将这些非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中
Spring AOP 常见使用场景
- 统一登录检测机制
- 统一方法的执行时间统计
- 统一的返回格式设置
- 统一异常处理
- 事务的开始和提交
实例理解
- 假设我们有一个 Web 应用,其中有很多需要用户登录后才能访问的页面
- 为了避免在每个需要登录的方法中都检查用户是否已经登录
- 我们便可以使用 Spring AOP 来解决该问题
AOP 组成
切面(类)
- 在程序中就是对某一功能进行统一处理的类
- 这个类包含了很多方法,这些方法就是由 切点 和 通知 组成
切点(方法)
- 定义一个进行主动拦截的规则
- 所谓的拦截就是 对用户向服务器发送的请求进行拦截,检测用户的操作是否符合预期,发现问题并统一处理的过程
通知
- 通知就是 AOP 的具体执行动作
- 在程序中被拦截后会触发一个具体的动作,即通知中具体实现的业务代码
- 在 Spring 中可以在方法上使用以下注解,设置方法为通知方法,在满足条件后会通知本方法进行调用
前置通知(@Before)
- 执行目标方法(实际要执行的方法)之前执行的方法
后置通知(@After)
- 执行了目标方法之后执行的方法
返回通知(@AfterReturning)
- 目标方法返回(return)的时候,执行的方法
异常通知(@AfterThrowing)
- 在执行目标方法抛出异常时,执行的方法
- 通常结合事务一起使用,如果未抛出异常则提交成功,如果抛出异常则回滚事务
环绕通知(@Around)
- 在目标方法调用前、后都执行的通知
- 如果已经有了前置和后置通知,再使用环绕通知,那么周期范围就在前置通知之前,后置通知之后
- 通常可用于计算目标方法执行的时间
连接点
- 所有可能触发切点的点就叫做连接点
Spring AOP 实现
- 此处我们想实现拦截 UserController 类中的所有方法
添加 Spring Boot AOP 依赖
- 创建 Spring Boot 项目时,无法选择添加 Spring boot AOP 依赖
- 所以我们可以选择进入 中央仓库 来选择与当前 Spring Boot 版本相对应的 AOP
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定义切面(类)
- 使用 @Aspect 注解修饰该类,告诉 Spring Boot 该类为一个切面类
package com.example.demo.component; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect // 切面 @Component // 不能省略 public class UserAspect { }
定义切点(方法)
- 定义拦截规则,实现拦截 UserController 类中的所有方法
- 使用 @Pointcut 修饰一个方法,该方法无需有方法体
- 方法名就是起到一个标识的作用,标识通知方法具体指的是哪一个切点,因为切点可能有很多个
package com.example.demo.component; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect // 切面 @Component // 不能省略 public class UserAspect { // 切点(配置拦截规则) @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut() { } }
创建通知
- 此处我们创建三个通知
- 前置通知、后置通知、环绕通知
package com.example.demo.component; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Aspect // 切面 @Component // 不能省略 public class UserAspect { // 切点(配置拦截规则) @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut() { } /* * 前置通知 * */ @Before("pointcut()") public void beforeAdvice() { System.out.println("执行了前置方法"); } /* * 后置通知 * */ @After("pointcut()") public void afterAdvice() { System.out.println("执行了后置方法"); } /* * 环绕通知 * 此处的 joinPoint 就是连接点,即方法本身 * */ @Around("pointcut()") public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable { Object obj = null; System.out.println("进入了环绕通知"); obj = joinPoint.proceed(); System.out.println("退出了环绕通知"); // 最后将执行的结果交给框架 return obj; } }
创建连接点
- 此处我们对照 切点的拦截规则
- 创建一个 UserController 类
- 并在该类中创建两个连接点
- 当我们在浏览器中访问这两个连接点时,会触发切点的拦截规则,对其进行拦截,然后执行各种通知
package com.example.demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @RequestMapping("/user/sayhi") public String sayHi() { System.out.println("执行了 sayHi 方法"); return "hi spring boot aop"; } @RequestMapping("/user/login") public String login() { System.out.println("执行了 login 方法"); return "do user login"; } }
执行结果
- 运行 Spring Boot 项目
- 此处我们在浏览器中输入对应的 URL 地址,访问 UserController 中的 sayHi 方法
图示理解
切点表达式说明
- 该切点表达式属于 AspectJ 的语法
- execution( <修饰符> <返回类型> <包.类.方法(参数)> <异常> )
实例理解
Spring AOP 实现原理
- Spring 的切面是代理类实现的
- 代理类包裹了 目标对象
- 即 用户只能先通过代理类,进行校验,如果没有问题才会进一步访问到目标对象
织入
- 指代理生成的时机
- 一般情况下,Spring AOP 动态代理的植入时机是程序的运行期
动态代理
- Spring AOP 是构建在动态代理基础上的,所以 Spring 对 AOP 的支持局限于方法级别的拦截
- Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理
JDK 动态代理
- 基于接口实现
- 底层通过反射实现
- 要求代理类一定要实现接口
CGLIB
- 基于类的子类实现
- 底层通过字节码增强技术生成子类实现
- 不能代理被 final 修饰的类,因为其无法生成子类