本篇学习Spring AOP的相关概念和实现原理
文章目录
AOP相关概念
什么是 Spring AOP?
AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理。比如用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用登录验证了,不再需要每个方法中都写相同的用户登录验证了。
而 AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和IoC 与 DI 类似。
对于这种功能统⼀,且使用的地方较多的功能,就可以考虑 AOP来统⼀处理了
除了统一的用户登录判断之外,AOP 还可以实现:
- 统⼀日志记录
- 统⼀方法执行时间统计
- 统⼀的返回格式设置
- 统⼀的异常处理
- 事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
AOP的组成
切面(类)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合
指的就是某一方面的具体内容就是一个切面
切点(方法)
切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,而连接点就是表中⼀条⼀条的数据)
切点就是类里面的方法,定义一个拦截规则,满足切面使用
连接点
应用执行过程中能够插入切面的⼀个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插⼊到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法
所以可触发切点的点叫做连接点
通知(方法的具体实现代码)
执行AOP逻辑业务
切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
Spring 切⾯类中,可以在方法上使用以下注解,设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知:使用 @Before:通知方法会在目标方法调用之前执行。(在目标方法和实际要执行的方法调用之前执行的通知)
后置通知:使用 @After:通知方法会在目标方法返回或者抛出异常后调用。(在目标方法调用之后执行的通知)
返回之后通知:使用 @AfterReturning:通知方法会在目标方法返回后调用。
异常通知:使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
环绕通知:使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调⽤之后执行自定义的行为。
织入
将切面应用到目标对象的过程,可以在编译时,加载时或者运行时。
实现Spring AOP
添加依赖
首先添加框架依赖,在pom.xml中添加配置:
选择合适的版本进行添加
maven仓库地址
创建切面
添加两个注解
@Aspect//切面的意思
@Component//不能省略,随着项目启动而启动
创建切点(定义拦截规则)
@Aspect//切面的意思
@Component
public class UserAOP {
//定义切点,这里使用AspectJ语法(配置拦截规则)
@Pointcut("execution( * com.example.demo.controller.UserController.*(..))")
public void pointcut(){
}
}
AspectJ 支持三种通配符:
’ * ’ :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,方法参数)
‘. .’ :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
’ + ’ :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的所有子类包括本身
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
修饰符和异常可以省略,具体含义如下:
修饰符,一般省略:public : 公共方法;’ * ':任意
返回值,不能省略:void : 返回没有值;String : 返回值字符串;* : 任意
包,com.a.b : 固定包 ;com.a. * .b : a下面子包任意 ;com.a.b. . :b包下面所有子包(包括自己)、
类,a:指定类;* a:以a结尾的类;a * :以a开头的类; * :所有类
方法名,不能省略,a:指定方法; * a:以a结尾的方法;a * :以a开头的方法;* :所有方法
参数,():无参;(int):一个参数;(int,int):两个参数;(…):任意参数
异常:通常省略
表达示例:
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
创建通知
//前置通知
//前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执行了前置通知"+ LocalDateTime.now());
}
//后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行了后置通知"+ LocalDateTime.now());
}
//环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知");
Object obj = joinPoint.proceed();
System.out.println("结束环绕通知");
return obj;
}
创建连接点
@RequestMapping("/sayhi")
public String sayHi(){
System.out.println("执行了sayHi方法");
return "hi,spring boot aop.";
}
@RequestMapping("/login")
public String login(){
System.out.println("执行了login方法");
return "hi,login.";
}
Spring AOP实现原理
Spring AOP的实现原理基于动态代理。
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截
Spring的切面由包裹了目标对象的代理类实现,代理类的处理方法的调用,执行了额外的切面逻辑,并且调用目标方法。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。
默认情况下,实现了接口的类,使用 AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类
织入(代理生成的时机)
在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某⼀时刻被织入。⼀般情况下,在织入切面时,AOP容器会为目标对象动态创建⼀个代理对象。SpringAOP就是以这种方式织入切面的。
两个底层实现:
1.JDK动态代理:通过反射实现,要求:被代理的类一定要实现接口
2.CGLIB:字节码增强技术(生成子类),通过继承被代理类,在运行时动态的生成代理类对象
JDK 和 CGLIB 实现的区别
- JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHandler 及 Proxy,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统⼀的业务逻辑字节码来完成。
- CGLIB 实现,是基于类的,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的⽣成代理类对象
1.JDK基于接口,CGLIB基于类
2.JDK生成的代理对象是目标对象的接口,CGLIB生成的是目标对象的子类
3.JDK性能相对较高,生成代理速度较快,CGLIB性能相对较低,生成代理速度较慢(1.7以前)
4.CGLIB无法处理fianl的类和方法,JDK则可以处理任意类的方法
如果既使用了JDK又使用了CGLIB,默认情况下使用JDK,如果目标对象没有实现接口,则使用CGLIB