目录
1. 什么是Spring AOP?
2. 为什么要用AOP?
3. AOP该怎么学习?
3.1 AOP的组成
(1)切面(Aspect)
(2)连接点(join point)
(3)切点(Pointcut)
(4)通知(Advice)
4. Spring AOP实现
4.1 添加 AOP 框架支持
编辑
4.2 定义切面
4.3 定义切点
4.4 定义通知
4.5 切点表达式说明 AspectJ
5.使用 AOP 统计 UserController 每个方法的执行时间 StopWatch
6. Spring AOP 实现原理
6.1 生成代理的时机 :织入(Weaving)
6.2 JDK 动态代理实现
6.3 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 定义切面
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
}
4.3 定义切点
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
/**
* 切点(配置拦截规则)
*/
@Pointcut("execution(* com.example.demo.Controller.*.*(..))")
public void pointcut() {
}
}
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
4.4 定义通知
@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实体类:
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实体类:
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 对象:
// 添加环绕通知
@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 动态代理就是依靠反射来实现的
//动态代理:使⽤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 动态代理实现
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 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象,这种方式实现方式效率高