目录
[1. 什么是Spring AOP?](#1. 什么是Spring AOP?)
[2. 为什么要用AOP?](#2. 为什么要用AOP?)
[3. AOP该怎么学习?](#3. AOP该怎么学习?)
[3.1 AOP的组成](#3.1 AOP的组成)
[(2)连接点(join point)](#(2)连接点(join point))
[4. Spring AOP实现](#4. Spring AOP实现)
[4.1 添加 AOP 框架支持](#4.1 添加 AOP 框架支持)
[4.2 定义切面](#4.2 定义切面)
[4.3 定义切点](#4.3 定义切点)
[4.4 定义通知](#4.4 定义通知)
[4.5 切点表达式说明 AspectJ](#4.5 切点表达式说明 AspectJ)
[5.使用 AOP 统计 UserController 每个方法的执行时间 StopWatch](#5.使用 AOP 统计 UserController 每个方法的执行时间 StopWatch)
[6. Spring AOP 实现原理](#6. Spring AOP 实现原理)
[6.1 生成代理的时机 :织入(Weaving)](#6.1 生成代理的时机 :织入(Weaving))
[6.2 JDK 动态代理实现](#6.2 JDK 动态代理实现)
[6.3 CGLIB 动态代理实现](#6.3 CGLIB 动态代理实现)
[6.4 JDK 和 CGLIB 实现的区别(面试常问)](#6.4 JDK 和 CGLIB 实现的区别(面试常问))
1. 什么是Spring AOP?
AOP(Aspect Oriented Programming):面向切面编程,它和 OOP(面向对象编程)类似。
面向切面编程就是面对某一方面、某个问题做集中的处理
针对某一类事情进行集中处理,这一类事情就是切面
比如用户登录权限的效验,在学习 AOP 之前,在需要判断用户登录的页面,都要各自实现或调用用户验证的方法,学习 AOP 之后,我们只需要在某一处配置一下,那么所有需要判断用户登录的页面就全部可以实现用户登录验证了,不用在每个方法中都写用户登录验证了
AOP 是一种思想,而 Spring AOP 是实现(框架),这种关系和 IOC(思想)与 DI(实现)类似
2. 为什么要用AOP?
- 采用AOP,我们可以不修改源代码,添加新的功能。我们单独编写独立的权限判断模块,并通过配置,将其配置到登录流程中(比如用户登录验证等)
- 更加便捷,想象我们做一个类似CSDN这类博客系统,在之前我们除了登录、注册不需要验证用户是否登录,其余所有页面几乎都要验证,且在每个功能的代码都需要再写一遍,这就显得十分麻烦,对于这类功能统一的,且地方使用较多的功能,AOP会表现的更加出色
除了统一的用户登录判断之外,AOP还可以实现:
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
也就是说使用AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP(Object OrientedProgramming,面向对象编程)的补充和完善。
3. AOP该怎么学习?
- 学习 AOP 是如何组成
- 学习 Spring AOP 的使用
- 学习 Spring AOP 实现原理
3.1 AOP的组成
(1)切面(Aspect)
定义 AOP 是针对某个统一的功能的 ,这个功能就叫做一个切面,比如用户登录功能或方法的统计日志,他们就各是一个切面。切面是由切点和通知组成的。
通俗的理解就是,切面就是处理某一个具体问题的一个类,类中包含了很多方法,这些方法就是切点和通知
(2)连接点(join point)
所有可能触发 AOP(拦截方法的点)就称为连接点
(3)切点(Pointcut)
切点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知,总的来说就是,定义 AOP 拦截的规则的
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)
用来进行主动拦截的规则(配置)
(4)通知(Advice)
拦截到这个行为后要做什么事就是通知
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后悔通知本方法进行调用:
- 前置通知使用@Before:通知方法会在目标方法调用之前执行
- 后置通知使用@After:通知方法会在目标方法调用之后执行
- 返回之后通知使用@AfterReturning通知方法会在目标方法返回后调用
- 抛异常后通知@AfterThrowing:通知方法会在目标方法爬出异常之后调用
- 环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为
就相当于在一个生产型公司中
通知相当于底层的执行者,切点是小领导制定规则,切面是大领导制定公司的发展方向,连接点是属于一个普通的消费者用户
以CSDN的登录为例子:
4. Spring AOP实现
Spring AOP 实现步骤
- 添加 Spring AOP 框架支持
- 定义切面和切点
- 实现通知
接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的方法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。
4.1 添加 AOP 框架支持
创建Spring Boot项目时是没有Spring AOP框架可以选择的,这个没关系,咱们创建好项目之后,再在pom. xml中添加Spring AOP的依赖即可。
4.2 定义切面
java
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
}
4.3 定义切点
java
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
/**
* 切点(配置拦截规则)
*/
@Pointcut("execution(* com.example.demo.Controller.*.*(..))")
public void pointcut() {
}
}
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
4.4 定义通知
java
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
/**
* 切点(配置拦截规则)
*/
@Pointcut("execution(* com.example.demo.Controller.*.*(..))")
public void pointcut() {
}
/**
* 前置通知
*/
@Before("pointcut()")
public void beforeAdvice() {
System.out.println("执行了前置通知~");
}
/**
* 后置通知
*/
@After("pointcut()")
public void afterAdvice() {
System.out.println("执行了后置通知~");
}
/**
* 环绕通知
*/
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
Object obj = null;
System.out.println("进入环绕通知之前");
// 执行目标方法
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("退出环绕通知了");
return obj;
}
}
UserController实体类:
java
package com.example.demo.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created with IntelliJ IEDA.
* Description:
* User:86186
* Date:2023-08-10
* Time:16:43
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/hi")
public String sayHi(String name) {
System.out.println("执行了 sayHi 方法");
return "Hi," + name;
}
@RequestMapping("/hello")
public String sayHello() {
System.out.println("执行了 sayHello 方法");
return "Hello, world.";
}
}
ArticleController实体类:
java
package com.example.demo.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created with IntelliJ IEDA.
* Description:
* User:86186
* Date:2023-08-10
* Time:16:45
*/
@RestController
@RequestMapping("/art")
public class ArticleController {
@RequestMapping("/hi")
public String sayHi() {
System.out.println("文章的 sayHI~");
return "Hi, world.";
}
}
当浏览art/hi时:
此时控制台只有articleControlle中的打印,没有前置、后置通知
但是当我们去访问
再次刷新
4.5 切点表达式说明 AspectJ
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
AspectJ 语法(Spring AOP 切点的匹配语法):
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
AspectJ ⽀持三种通配符
* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
... :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
- :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的所有⼦类包括本身
修饰符,一般省略public 公共方法
*任意
返回值,不能省略void 返回没有值
String 返回值字符串
*任意
包,通常不省略,但可以省略com.gyf.crm 固定包
com.gyf.crm.*.service crm 包下面子包任意(例如:com.gyf.crm.staff.service)
com.gyf.crm... crm 包下面的所有子包(含自己)
com.gyf.crm.*service... crm 包下面任意子包,固定目录 service,service 目录任意包
类,通常不省略,但可以省略UserServiceImpl 指定类
*Impl 以 Impl 结尾
User* 以 User 开头
*任意
方法名,不能省略addUser 固定方法
add* 以 add 开头
*DO 以 DO 结尾
*任意
参数() 无参
(int) 一个整形
(int,int)两个整型
(...) 参数任意
throws可省略,一般不写
表达式示例execution(* com.cad.demo.User.*(...)) :匹配 User 类⾥的所有⽅法
execution(* com.cad.demo.User+.*(...)) :匹配该类的⼦类包括该类的所有⽅法
execution(* com.cad..(...)) :匹配 com.cad 包下的所有类的所有⽅法
execution(* com.cad....(...)) :匹配 com.cad 包下、⼦孙包下所有类的所有⽅法
execution(* addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个参数类型是 int
5.使用 AOP 统计 UserController 每个方法的执行时间 StopWatch
Spring AOP 中统计时间用 StopWatch 对象:
java
// 添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
// spring 中的时间统计对象
StopWatch stopWatch = new StopWatch();
Object result = null;
try {
stopWatch.start(); // 统计方法的执行时间,开始计时
// 执行目标方法,以及目标方法所对应的相应通知
result = joinPoint.proceed();
stopWatch.stop(); // 统计方法的执行时间,停止计时
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "." +
joinPoint.getSignature().getName() +
"执行花费的时间:" + stopWatch.getTotalTimeMillis() + "ms");
return result;
}
6. Spring AOP 实现原理
Spring AOP 是构建在动态代理 基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截
Spring AOP 动态代理实现:
默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类
- JDK Proxy(JDK 动态代理)
- CGLIB Proxy:默认情况下 Spring AOP 都会采用 CGLIB 来实现动态代理,因为效率高
- CGLIB 实现原理:通过继承代理对象来实现动态代理的(子类拥有父类的所有功能)
- CGLIB 缺点:不能代理最终类(也就是被 final 修饰的类)
6.1 生成代理的时机 :织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中
在目标对象的生命周期中有多个点可以进行织入
- 编译期:切面在目标类编译时被织入,这种方法需要特殊的编译器,AspectJ 的织入编译器就是以这种方式织入切面的
- 类加载期:切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码,AspectJ5 的加载时织入 (load-time weaving. LTW)就支持以这种方式织入切面
- 运行期:切面在应用运行的某一时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 就是以这种方式织入切面的
6.2 JDK 动态代理实现
JDK 动态代理就是依靠反射来实现的
java
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被
代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler {
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler( Object target) {
this.target = target;
}
//proxy代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler = new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),new Class[]{PayService.class},handler);
proxy.pay();
}
}
6.3 CGLIB 动态代理实现
java
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxymethodProxy)throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
PayService proxy= (PayService) Enhancer.create(target.getClass(),
new PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
6.4 JDK 和 CGLIB 实现的区别(面试常问)
- JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHander 及 Proxy,在运行时动态的在内存中生成了代理对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成的
- CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象,这种方式实现方式效率高