1.AOP是什么
我们之前学过 OOP 面向对象编程, 那么 AOP 是否就比 OOP 更牛逼呢? 是否也是面向过程到面向对象这种质的跨越呢? 其实不是, 它和 OOP 思想是一种互补的关系, 是对 OOP 思想的一种补充.
AOP (Aspect Oriented Programming) : 面向切面编程, 它是一种思想, 它是对某一类事情的集中处理.
那什么叫面向切面编程呢 ? 这个切面是数学中的切面吗 ? 其实不是, 这都是英译汉的锅.
就拿常见博客中的用户登录功能来说, 我们在写博客的时候, 需要验证用户登录; 我们在发布博客的时候, 也需要验证用户登录; 我们在查看文章列表的时候,也需要验证用户登录. 很多地方都需要做这个事情, 那么我就可以将这个事提出来, 做成统一的处理. 所以验证用户登录这个功能就可以使用 AOP来处理.
听到这, 大概就明白了, AOP 其实是不能代替 OOP 的, 只有集中或者可以高度抽象的一类事情才可以使用 AOP 思想来处理, 而并不是所有事情都是集中, 可以高度抽象的, 就比如注册功能, 登录功能, 我只是在注册, 登录的时候才做这件事情.
1.1 AOP 思想的好处
我们分析 "面向过程 -> 封装 -> AOP" 这三种操作慢慢升级的过程, 就能从中体会这种看似不起眼的升级. 这也是日常开发和企业级开发的区别所在.
1. 开发初级阶段 (面向过程) : 所有的方法都自己实现一遍. 分别在修改, 删除, 发布博客三个业务中自己写验证用户登录的功能
2. 开发中级阶段 (封装) : 封装成公共的方法, 在需要的地方进行调用. 将验证用户登录功能提取出来, 写成一个公共的方法, 分别在修改, 删除, 发布博客中调用该方法. (降低代码耦合)
3. 开发的高级阶段 (AOP) : 使用 AOP (拦截器/过滤器) 对某个功能做统一的处理. 使用 AOP 思想对验证登录这种可以高度抽象的业务进行集中处理, 不再需要在每个方法中进行调用. (再次降低代码耦合)
【举个例子加以理解】
假如我们是深圳高铁站的高管,这里有四个站台,分别开往广州、上海、杭州、北京四个地方, 当乘客们进站前, 我们需要检查乘客是否携带易燃易爆物品.
初级阶段, 我们分别在四个站台安排自己的员工进行安全检查.
中级阶段, 我们将检查工作外包给保安公司, 让保安人员分别在四个站台进行安全检查.
高级阶段, 我们在进站口安排保安人员进行安全检查, 不需要在各个站台进行安全检查.
这就是 AOP 思想的好处. 当然不使用 AOP也能做事情, 但是不够优雅, 不够高级.如果你封装的代码, 你有一百个, 一千个地方需要调用, 某一天, 你接口的参数变了, 那么你这一百个, 一千个调用的地方都要改, 代码耦合度就相当高了. 这中看似不起眼的升级, 在代码没有发生变动的时候, 大家都能正常开发, 正常使用. 一旦代码发生变动, 两种方式的差别就体现出来了.
除了统一的用户登录效验之外, AOP 还可以实现如下功能 (后面会用代码实现)
统一的日志记录
统一方法执行时间统计
统一的返回格式设置
统一的异常处理
事务的开启和提交等
1.2 Spring AOP 是什么
既然我们知道 AOP 是什么, 又知道了 AOP 可以用来干嘛, 以及它的好处, 那么什么又是 Spring AOP 呢
AOP 是一种思想, 而 Spring AOP 是它的具体实现. 就类似 Spring IoC 和DI 的关系一样.
2.Spring AOP 应该如何学习
Spring AOP 就学三样东西:
1. AOP 的组成. 【概念】
2. Spring AOP 的实现.
3. Spring AOP 的实现原理.
2.1 AOP 的组成
1. 切面 (Aspect) : 定义 AOP 业务类型的. (当前业务类型是用户登录效验, 还是统一异常处理等等)
2. 连接点 (Join Point) : 有可能调用 AOP 的地方就叫做一个连接点. (前面例子在中级阶段的各个业务中调用公共方法的地方, 就是一个一个的连接点.)
3. 切点 (Pointcut) : 定义 AOP 拦截规则.
4. 通知 (Advice) : 定义什么时机, 做什么事. [增强方法]
通知分为五类:
前置通知 : 拦截的目标方法之前执行的通知 (事件).
后置通知 : 拦截的目标方法之后执行的通知 (事件).
返回之后通知 : 拦截的目标方法返回数据之后通知.
抛出异常之后的通知 : 拦截的目标方法抛出异常之后执行的通知.
环绕通知 : 在拦截方法执行前后都执行的通知.
2.2 Spring AOP 的实现
1. 添加 Spring AOP 框架支持.
2. 定义切面和切点.
3. 定义通知.
添加Spring AOP 框架支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面和切点
就拿用户登录效验功能来举例>>
@Component
@Aspect // 标识当前类为一个切面
public class LoginAop {
// 定义切点 (拦截的规则, 此处使用的是 AspectJ 表达式语法)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
// 该方法没啥作用, 只是后续需要使用这个方法名
public void pointcut() {
}
}
定义通知
// 前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知");
}
// return 之前通知
@AfterReturning("pointcut()")
public void doAfterReturning() {
System.out.println("执行了返回之后通知");
}
// 抛出异常之前通知
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("执行了返回之后通知");
}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) { // 拿到目标方法的执行对象
// 这个对象是框架能否继续执行后续流程的对象, 与目标方法是否返回值, 以及返回类型无关
Object res = null;
// 前置业务代码
System.out.println("执行了环绕通知的前置方法");
try {
// 执行目标方法
res = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 后置业务代码
System.out.println("执行了环绕通知的后置方法");
return res;
}
切点,通知都在 LoginAop 这个类中.
【分析】
1. 切点的拦截规则表示,拦截 UserController 类里面的所有方法. (后面细讲拦截规则)
2. 切点里的 pointcut() 方法是为了给后面的通知使用该方法名.
3. 前四种通知都很简单, 除了注解不一样, 其他都一样. 主要是环绕通知, 它带有参数, 参数 joinPoint 的意义就是拿到目标方法中的执行对象, 也就是 UserController 中的所有方法的执行对象. 用这个对象调用 proceed() 就是执行 UserController 中的所有方法. 环绕通知的返回值 res , 和目标方法的方法类型无关, 它只决定框架能否继续执行后续流程.
【测试】
现在我们测试一下我们的 AOP 是否可以拦截 UserController 中的方法.
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 查询所有用户
* @return
*/
@RequestMapping("/select")
public List<UserInfo> selectAll() {
log.debug("执行了 selectAll 方法");
return userService.selectAll();
}
}
当我们通过流浪器访问 selectAll() 方法时, 观察控制台的信息:
【结论】
1. 成功拦截了 UserController 中的方法.
2. 环绕通知的前置方法在最前面执行, 环绕通知的后置方法在最后执行.
3. 通过上面 AOP的简单实现, 我们大概也就知道了如何对 "用户登录效验功能进行统一处理了. 只需要在同一将所有调用用户登录校验的方法写在一个类中, 或者一个文件夹下, 然后设置对应的拦截规则即可.
如果想进一步验证 "拦截规则" 是否正确, 可以在 controller 包下再建一个 TestController 类, 然后写一个方法, 并通过浏览器访问, 观察是否还打印了这几个通知方法. (答案肯定是没有, 下来可以自己试一下)
🍁切点表达式的说明
AspectJ 支持三种通配符:
* : 匹配任意字符, 只匹配一个元素 (包,类,或⽅法,⽅法参数)
.. : 匹配任意字符, 可以匹配多个元素, 在表示类时, 必须和 * 联合使用.
+ : 表示按照类型匹配指定类的所有类, 必须跟在类名后面, 如 com.hl.Cat+, 表示继承该类的所有子类, 包括本身.
切点表达式由切点函数组成, 最常用的切点函数就是 execution() , 用来匹配方法, 语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
【具体规则】>>>
修饰符(一般省略)
public 公共方法
* 任意
返回类型(不能省略)
void 没有返回值
String 返回字符串类型
* 任意
包
com.example.demo 固定包
com.example.demo.*.controller demo 下面任意子包 (com.example.demo.aop.controller)
com.example.demo.. demo 包下面所有子包 (含自己)
com.example.demo.*.controller.. demo 包下面任意子包 / 固定目录 controller / controller 目录任意包
类
UserController 指定类
*service 以 service 结尾的类
User* 以 User 开头的类
* 任意
方法名
addUser 固定方法
add* 以 add 开头 方法
*Add 以 Add 结尾的方法
* 任意
参数
() 无参
(int) 一个整型
(int, int) 两个整型
(..) 任意参数
throws (可省略, 一般不写)
2.3 Spring AOP 的实现原理
由于Spring AOP 是构建在动态代理基础上的, 因此 Spring 对 AOP 的支持局限于方法级别的拦截.
什么是动态代理 >>>
程序运行期间产生的代理, 就叫做动态代理.
举个例子: 就拿放鞭炮这件事来说吧.
当政府还没有批准放鞭炮的时候, 你就是一个卖鞭炮的商贩了, 此时你就是一个静态代理.
如果说你是因为政府批准放鞭炮后, 你在一个广场上看见很多人都聚集在这里放鞭炮, 然后你觉得有利可图, 于是你就进购很多鞭炮, 你就将鞭炮拿到广场上去卖. 像这种本身不是烟花商贩, 只是看见有利可图才做起了流动商贩, 这就叫做动态代理.
动态代理也是一种思想, JDK Proxy 和 CGLIB 这两种方式就是动态代理的具体实现.
如果是一个实现接口的类, 使用 AOP 会基于 JDK Proxy 生成代理类.
如果是一个没有实现接口的类, 使用 AOP 会基于 CGLIB 生成代理类.
织入(Weaving) : 代理的生成时机
织入是把切⾯应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对
象中.
在目标对象的声明周期里, 有三个时期可以进行织入 (类的三个阶段):
编译期: 切面在目标类编译时被织入. 这种方式需要特殊的编译器.
类加载期: 切面在目标类加载到JVM时被织入.这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码.
运行期: 切面在应用运行的某一时刻被织入.
我们学习 Spring AOP , 主要就是基于 JDK Proxy 和 CGLIB 这两种方式. 这两种方式的代理目标都是在被代理类中的方法, 在运行期间, 动态的织入字节码生成代理类. 这种实现在设计模式上称为动态代理模式.
【常见面试题】
JDK Proxy 和 CGLIB 的区别 >>
1. JDK Proxy 的实现,要求 被代理类必须实现接口,然后通过 InvocationHandler 及 Proxy,在运行期间动态的在内存中生成了代理类对象.
2. CGLIB 实现, 被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类
对象.
对于 JDK Proxy 和 CGLIB 的实现原理和区别建议可以去简单看看源码, 这对后续的 Spring AOP 的统一功能处理的理解以及实现都是有很大的帮助.
本篇文章就到这里了, 谢谢观看!!