目录
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 实现的区别(面试常问)
AOP(Aspect Oriented Programming):面向切面编程,它和 OOP(面向对象编程)类似。
面向切面编程就是面对某一方面、某个问题做集中的处理
针对某一类事情进行集中处理,这一类事情就是切面
比如用户登录权限的效验,在学习 AOP 之前,在需要判断用户登录的页面,都要各自实现或调用用户验证的方法,学习 AOP 之后,我们只需要在某一处配置一下,那么所有需要判断用户登录的页面就全部可以实现用户登录验证了,不用在每个方法中都写用户登录验证了
AOP 是一种思想,而 Spring AOP 是实现(框架),这种关系和 IOC(思想)与 DI(实现)类似
除了统一的用户登录判断之外,AOP还可以实现:
也就是说使用AOP可以扩充多个对象的某个能力,所以AOP可以说是OOP(Object OrientedProgramming,面向对象编程)的补充和完善。
定义 AOP 是针对某个统一的功能的,这个功能就叫做一个切面,比如用户登录功能或方法的统计日志,他们就各是一个切面。切面是由切点和通知组成的。
通俗的理解就是,切面就是处理某一个具体问题的一个类,类中包含了很多方法,这些方法就是切点和通知
所有可能触发 AOP(拦截方法的点)就称为连接点
切点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知,总的来说就是,定义 AOP 拦截的规则的
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)
用来进行主动拦截的规则(配置)
拦截到这个行为后要做什么事就是通知
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后悔通知本方法进行调用:
就相当于在一个生产型公司中
通知相当于底层的执行者,切点是小领导制定规则,切面是大领导制定公司的发展方向,连接点是属于一个普通的消费者用户
以CSDN的登录为例子:
Spring AOP 实现步骤
接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的方法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。
创建Spring Boot项目时是没有Spring AOP框架可以选择的,这个没关系,咱们创建好项目之后,再在pom. xml中添加Spring AOP的依赖即可。
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
}
@Aspect // 告诉框架我是一个切面类
@Component // 随着框架的启动而启动
public class UserAspect {
/**
* 切点(配置拦截规则)
*/
@Pointcut("execution(* com.example.demo.Controller.*.*(..))")
public void pointcut() {
}
}
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
@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中的打印,没有前置、后置通知
切点表达式由切点函数组成,其中 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
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;
}
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截
Spring AOP 动态代理实现:
默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中
在目标对象的生命周期中有多个点可以进行织入
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();
}
}
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();
}
}
- JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHander 及 Proxy,在运行时动态的在内存中生成了代理对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成的
- CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象,这种方式实现方式效率高