1.AOP概述
什么是AOP?
Spring 俩大核心: Spring IoC 和 Spring AOP
IoC 控制反转(把Bean的控制权交给Spring来进行管理)
AOP(Aspect Oriented Programming)面向切面编程.它和面向对象编程不是互斥关系,而是面向对象编程的补充.
什么是⾯向切⾯编程呢?
切⾯就是指某⼀类特定问题, 所以AOP也可以理解为面向特定方法编程.(也就是对于统一问题的解决方法)
什么是⾯向特定⽅法编程呢? 比如"登录校验", 就是⼀类特定问题. 登录校验拦截器, 就是对"登录校验"这类问题的统⼀处理. 所以, 拦截器也是AOP的⼀种应⽤. AOP是⼀种思想, 拦截器是AOP思想的⼀种实现. Spring框架实现了这种思想, 提供了拦截器技术的相关接⼝. 同样的, 统⼀数据返回格式和统⼀异常处理, 也是AOP思想的⼀种实现.Spring 对AOP的实现就是Spring AOP.
简单来说: AOP是⼀种思想, 是对某⼀类事情的集中处理.(用户的登录校验问题,结果的统一返回,异常的统一处理都是AOP思想的实现)
什么是Spring AOP?
AOP是⼀种思想, 它的实现⽅法有很多, 有Spring AOP,也有AspectJ、CGLIB等.
Spring AOP是其中的⼀种实现⽅式.
学会了统⼀功能之后, 是不是就学会了Spring AOP呢, 当然不是.
拦截器作⽤的范围是所有的URL(⼀次请求和响应), @ControllerAdvice 应⽤场景主要是全局异常处理(配合⾃定义异常效果更佳), 数据绑定, 数据预处理. AOP作⽤的维度更加细致(可以根据包、类、⽅法名、参数等进⾏拦截), 能够实现更加复杂的业务逻辑.(每个方法,接口执行的时间,我们需要计算,因此我们需要对每个方法进行监控)
比如我们在之前的图书管理系统的查询接口上测试耗时
现在有⼀些业务的执⾏效率⽐较低, 耗时较⻓, 我们需要对接⼝进⾏优化.
第⼀步就需要定位出执⾏耗时⽐较⻓的业务⽅法, 再针对该业务⽅法来进⾏优化
如何定位呢? 我们就需要统计当前项⽬中每⼀个业务⽅法的执⾏耗时.
如何统计呢? 可以在业务⽅法运⾏前和运⾏后, 记录下⽅法的开始时间和结束时间, 两者之差就是这个⽅法的耗时. 这种⽅法是可以解决问题的, 但⼀个项⽬中会包含很多业务模块, 每个业务模块⼜有很多接⼝, ⼀个接⼝⼜包含很多⽅法, 如果我们要在每个业务⽅法中都记录⽅法的耗时, 对于程序员而言, 会增加很多的⼯作量.
AOP就可以做到在不改动这些原始方法的基础上, 针对特定的方法进行功能的增强.
AOP的作用:在程序期间运行在不修改源代码的基础上对已有方法进行增强(⽆侵⼊性: 解耦)
举个例⼦:
我们现在有⼀个项⽬, 项⽬中开发了很多的业务功能
打印每个方法的时间戳就是一类问题,我们就提出一个问题: 是否可以使用AOP的方式把方法的耗时打印出来?
现在我们要使用AOP来打印所有的接口耗时.我们来看具体实现
2. Spring AOP快速入门
2.1 引入依赖,在pom文件引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 写一个切面(一类特定的问题)
@Aspect和AspectJ的关系
我们自己写一个切面并且进行分析: 我们只是写了一个类,但是却影响了整个程序,只要调用接口就会把耗时给打印出来,对其他类的侵入性小.
注意 @Around("execution(* org.xiaobai.library.controller.*.*(..))"),如果我们只对BookController生效就要写成@Around("execution(* org.xiaobai.library.controller.BookController.*(..))"),如果要对不止controller里面的代码生效的话: @Around("execution(* org.xiaobai.library.*.*.*(..))")
然后我们对刚刚的代码进行分析
1. @Aspect: 标识这是⼀个切⾯类
2. @Around: 环绕通知, 在⽬标⽅法的前后都会被执⾏. 后⾯的表达式表⽰对哪些⽅法进⾏增强.
3. ProceedingJoinPoint.proceed() 让原始⽅法执⾏
AOP的优势(面试)
• 代码⽆侵⼊: 不修改原始的业务⽅法, 就可以对原始的业务⽅法进⾏了功能的增强或者是功能的改变
• 减少了重复代码
• 提⾼开发效率
• 维护方便
3. Spring AOP 详细解释
下⾯我们再来详细学习AOP, 主要是以下⼏部分
• Spring AOP中涉及的核⼼概念
• Spring AOP通知类型
• 多个AOP程序的执⾏顺序
3.1 Spring AOP核心概念
切点
切点(Pointcut), 也称之为"切⼊点"
Pointcut 的作⽤就是提供⼀组规则 (使⽤ AspectJ pointcut expression language (切点表达式)来描述), 告诉程序对哪些方法来进行功能增强(对哪些方法生效).比如我们刚刚的"execution(* org.xiaobai.library.controller.*.*(..))"
连接点
满⾜切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法.(具体作用的方法,也就是我们的切点对哪些方法进行了功能增强,那么这些方法就是连接点
比如我们BookController里面所有的方法,都满足上述的表达式
切点和连接点的关系
连接点是满⾜切点表达式的元素. 切点可以看做是保存了众多连接点的⼀个集合. 比如:
切点表达式: 全体教师
连接点就是: 张三,李四等各个⽼师
通知
通知就是具体要做的⼯作, 指哪些重复的逻辑,也就是共性功能(最终体现为⼀个⽅法) 比如上述程序中记录业务⽅法的耗时时间, 就是通知.(连接点具体要做的事情).
在AOP⾯向切⾯编程当中, 我们把这部分重复的代码逻辑抽取出来单独定义, 这部分代码就是通知的内容.
切面
切⾯(Aspect) = 切点(Pointcut) + 通知(Advice)
通过切⾯就能够描述当前AOP程序需要针对于哪些方法, 在什么时候执⾏什么样的操作.
切⾯既包含了通知逻辑的定义, 也包括了连接点的定义.
切⾯所在的类, 我们⼀般称为切⾯类(被@Aspect注解标识的类),切面类里面可以有很多个切面
切点,切面,通知的关系
切点是一组规则,主要是规定参与的范围是什么,连接点是参与范围的每一个个体,通知是具体做什么,切面是整个事情.
3.2 通知类型
上⾯我们讲了什么是通知(具体要做的事情,但是我们要做发事情可以分为在目标方法执行前做的,也可以是在目标方法执行后做的), 接下来学习通知的类型(哪一个时机执行的). @Around 就是其中⼀种通知类型, 表示环绕通知. Spring中AOP的通知类型有以下⼏种:
• @Around: 环绕通知, 此注解标注的通知⽅法(也就是我们写的通知)在⽬标⽅法前, 后都被执⾏
• @Before: 前置通知, 此注解标注的通知⽅法在⽬标⽅法前被执⾏
• @After: 后置通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, ⽆论是否有异常都会执⾏
• @AfterReturning: 返回后通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, 有异常不会执⾏
• @AfterThrowing: 异常后通知, 此注解标注的通知⽅法发⽣异常后执⾏
注意: @Around的返回值必须要有返回结果不然controller不会生效,因为我们执行的是目标函数,需要把结果返回
总体的代码例子
程序正常运⾏的情况下, @AfterThrowing 标识的通知⽅法不会执⾏
从上图也可以看出来, @Around 标识的通知⽅法包含两部分, ⼀个"前置逻辑", ⼀个"后置逻辑".其中"前置逻辑" 会先于 @Before 标识的通知⽅法执⾏, "后置逻辑" 会晚于 @After 标识的通知⽅法执⾏.
程序发⽣异常的情况下:
• @AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了
• @Around 环绕通知中原始⽅法调⽤时有异常,通知中的环绕后的代码逻辑也不会在执⾏了(因为
原始⽅法调⽤出异常了)
3.3 @PointCut
上⾯代码存在⼀个问题, 就是存在⼤量重复的切点表达式 execution(* com.example.demo.controller.*.*(..)) ,如果我们要修改表达式,那么就会要修改很多地方, Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要⽤到时引⽤该切⼊点表达式即可.
@PointCut表示声明一个切点,它所修饰的方法名称就是切点的名称
注意: 当切点定义使⽤private修饰时, 仅能在当前切⾯类中使⽤, 当其他切⾯类也要使⽤当前切点定义时, 就需要把private改为public. 引用为: 全限定类名.方法名(). (切点跨类使用)
如果有多个切面的话,程序的执行顺序,默认按照名称进行
AspectDemo2,AspectDemo3,AspectDemo4,数字越大优先级越低
3.4 切面优先级 @Order
当我们在⼀个项⽬中, 定义了多个切⾯类时, 并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候, 这些切⾯类中的通知⽅法都会执⾏, 那么这⼏个通知⽅法的执⾏顺序是什么样的呢?
存在多个切⾯类时, 默认按照切⾯类的类名字⺟排序:
• @Before 通知:字⺟排名靠前的先执⾏
• @After 通知:字⺟排名靠前的后执⾏
但这种⽅式不⽅便管理, 我们的类名更多还是具备⼀定含义的.
Spring 给我们提供了⼀个新的注解, 来控制这些切⾯通知的执⾏顺序,: @Order 使⽤⽅式如下:
Order里面是优先级的大小,数字越大,优先级越低
3.5 切点表达式
上⾯的代码中, 我们⼀直在使⽤切点表达式来描述切点. 下⾯我们来介绍⼀下切点表达式的语法. 切点表达式常⻅有两种表达⽅式
1. execution(RR):根据⽅法的签名来匹配
2. @annotation(RR) :根据注解匹配
3.5.1 execution表达式
execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
其中,访问修饰符和异常可以省略
切点表达式⽀持通配符表达:
*表示任意单词,可以提花返回类型,包名,类名方法等
.. 表示任意多个单词
更加详细的介绍
切点表达式示例
TestController 下的 public修饰, 返回类型为String ⽅法名为t1, 无参方法((完整写法)
execution(public String com.example.demo.controller.TestController.t1())
省略访问修饰符: 表示适应所有的访问修饰限定符,返回类型String,在controller包里面的TestController类里面的t1无参方法
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回类型: 匹配在controller包里面的TestController类里面的t1无参方法,任意返回类型,适合所有访问修饰限定符的方法
execution(* com.example.demo.controller.TestController.t1())
匹配TestController 下的所有⽆参⽅法
execution(* com.example.demo.controller.TestController.*())
匹配TestController 下的所有⽅法
execution(* com.example.demo.controller.TestController.*(..))
匹配controller包下所有的类的所有⽅法
execution(* com.example.demo.controller.*.*(..))
如果controller下面还有很多包
execution(* com.example.demo.controller..*(..))
匹配所有包下⾯的TestController
execution(* com..TestController.*(..))
匹配com.example.demo包下, ⼦孙包下的所有类的所有⽅法
execution(* com.example.demo..*(..))
匹配特定方法名且有特定参数的方法
execution(* myMethod(String, int))
匹配特定方法名且有特定参数,并且抛出异常的方法
exectution(* myMethod(String,int) throws IOException)
上面的方法是适合于有规则可循的
3.5.2 @annotation
execution表达式更适⽤有规则的(统一的), 如果我们要匹配多个⽆规则的⽅法呢, ⽐如:TestController中的t1()和UserController中的u1()这两个⽅法.这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了.
我们可以借助⾃定义注解的⽅式以及另⼀种切点表达式 @annotation(零散无规则) 来描述这⼀类的切点.
execution表达式适合于自己写项目,组件和框架的开发适合annotation
实现步骤:
1. 编写⾃定义注解
2. 使⽤ @annotation 表达式来描述切点
3. 在连接点的⽅法上添加⾃定义注解
自定义注解里面的元注解解释
这种写法不止适用于自定义注解,我也可以规定是作用于哪个包里面的哪个类的哪个被spring的注解修饰的方法,假设我是想作用于所有使用RequestMapping注解的方法.我们发下
Spring AOP的实现方式(面试题)
1. 基于注解 @Aspect (参考上述内容)基于execution
2. 基于⾃自定义注解 (参考⾃定义注解 @annotation 部分的内容)基于annotation
3. 基于Spring API (通过xml配置的⽅式, 自从SpringBoot ⼴泛使⽤之后, 这种⽅法⼏乎看不到了)1> 配置切面类 2> 通过<aop: config>来配置方法
4. 基于代理来实现(更加久远的⼀种实现⽅式, 写法笨重, 不建议使⽤)后续会聊代理模式
参考: https://cloud.tencent.com/developer/article/2032268
总体框架
4. Spring AOP 原理
上⾯我们主要学习了Spring AOP的应⽤, 接下来我们来学习Spring AOP的原理, 也就是Spring 是如何实现AOP的.
Spring AOP 是基于动态代理来实现AOP的, 咱们学习内容主要分以下两部分
1. 代理模式
2. Spring AOP源码剖析
一般我们设计模式要回答: 思想,实现方式,应用场景(几个方面)
4.1 代理模式
代理模式, 也叫委托模式.(不同模式的实现方法是不同的,每个模式的实现方法也不止一种)
定义:为其他对象(房东)提供⼀种代理(中介)以控制对这个对象的访问. 它的作⽤就是通过提供⼀个代理类, 让我们在调用目标方法的时候, 不再是直接对⽬标⽅法进⾏调⽤, 而是通过代理类间接调⽤.(有时候房东不能提供一些服务,比如我哪哪哪坏了,需要修理,此时代理的作用就出来了)
在某些情况下, ⼀个对象不适合或者不能直接引⽤另⼀个对象, ⽽代理对象可以在客户端和⽬标对象之间起到中介的作⽤.
使用场景
1> 无法直接访问目标对象
2> 目标对象给我们提供的功能不够,希望对目标对象进行功能上的增强
使⽤代理前:
使⽤代理后:
代理模式的主要角色
1. Subject: 业务接⼝类. 可以是抽象类或者接⼝(要求房东做的事情)
2. RealSubject: 业务实现类. 具体的业务执⾏, 也就是被代理对象. (房东)
3. Proxy: 代理类. RealSubject的代理.(中介)
⽐如房屋租赁
Subject 就是提前定义了房东做的事情, 交给中介代理, 也是中介要做的事情
RealSubject: 房东
Proxy: 中介
代理模式可以在不修改被代理对象的基础上, 通过扩展代理类, 进⾏⼀些功能的附加与增强. 根据代理的创建时期, 代理模式分为静态代理和动态代理.
• 静态代理: 由程序员创建代理类或特定⼯具⾃动⽣成源代码再对其编译, 在程序运⾏前代理类的(在程序运行前代理就存在了)
.class ⽂件就已经存在了.
• 动态代理: 在程序运⾏时, 运⽤反射机制动态创建而成.
我们任然用房东和中介的关系举例: 我们在豆瓣上看房,我们看上房子后,我们要约中介实地带看.
中介存在俩种:
1> 每个房子提前分配好中介了,房源A -> 中介A,房源B ->中介B
2> 选择想看的房子之后,提交信息,平台给我分配中介
从上面可知,中介产生的时机分为俩种:
1> 提前分配(静态代理)
2> 运行时分配(动态代理)
4.1.1 静态代理
静态代理: 在程序运⾏前, 代理类的 .class⽂件就已经存在了. (在出租房⼦之前, 中介已经做好了相关的⼯作, 就等租⼾来租房⼦了)
我们通过代码来加深理解. 以房租租赁为例
1. 定义接⼝(定义房东要做的事情, 也是中介需要做的事情)
2. 实现接⼝(房东出租房⼦)
3. 代理(中介, 帮房东出租房⼦)
4. 使用
代码演示
上⾯这个代理实现⽅式就是静态代理(仿佛啥也没⼲).
从上述程序可以看出, 虽然静态代理也完成了对⽬标对象的代理, 但是由于代码都写死了, 对⽬标对象的每个⽅法的增强都是⼿动完成的,⾮常不灵活. 所以⽇常开发⼏乎看不到静态代理的场景.
接下来新增需求: 中介⼜新增了其他业务: 代理房屋出售 我们需要对上述代码进⾏修改
从上述代码可以看出, 我们修改接⼝(Subject)和业务实现类(RealSubject)时, 还需要修改代理类(Proxy).
同样的, 如果有新增接⼝(Subject)和业务实现类(RealSubject), 也需要对每⼀个业务实现类新增代理类(Proxy).
既然代理的流程是⼀样的(可以进一波封装了), 有没有⼀种办法, 让他们通过⼀个代理类来实现呢? 这就需要⽤到动态代理技术了.
4.1.2 动态代理
相⽐于静态代理来说,动态代理更加灵活.
我们不需要针对每个⽬标对象都单独创建⼀个代理对象, 而是把这个创建代理对象的⼯作推迟到程序运⾏时由JVM来实现. 也就是说动态代理在程序运⾏时, 根据需要动态创建⽣成.
⽐如房屋中介, 我不需要提前预测都有哪些业务, ⽽是业务来了我再根据情况创建.
我们还是先看代码再来理解.
Java也对动态代理进行了实现, 并给我们提供了⼀些API, 常见的实现⽅式有两种:
1. JDK动态代理
2. CGLIB(也是SpringAOP的一种实现方式)动态代理
动态代理在我们⽇常开发中使⽤的相对较少,但是在框架中几乎是必用的一门技术. 学会了动态代理之后, 对于我们理解和学习各种框架的原理也⾮常有帮助
JDK动态代理
JDK 动态代理类实现步骤
1. 定义⼀个接⼝及其实现类(静态代理中的 HouseSubject 和 RealHouseSubject )
2. ⾃定义 InvocationHandler 并重写 invoke ⽅法,在 invoke ⽅法中我们会调⽤目标方法(被代理类的⽅法)并⾃定义⼀些处理逻辑(Spring基于动态代理实现的,动态代理底层是反射实现的)
3. 通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h)来创建代理对象
实现 InvocationHandler 接⼝
创建⼀个代理对象并使⽤
代码讲解
1. InvocationHandler
InvocationHandler 接⼝是Java动态代理的关键接⼝之⼀, 它定义了⼀个单⼀⽅法 invoke() , ⽤于处理被代理对象的⽅法调⽤.
通过实现 InvocationHandler 接⼝, 可以对被代理对象的⽅法进⾏功能增强.
2. Proxy
Proxy 类中使⽤频率最⾼的⽅法是: newProxyInstance() , 这个⽅法主要⽤来⽣成⼀个代理对象
这个⽅法⼀共有 3 个参数:
Loader: 类加载器, ⽤于加载代理对象.
interfaces : 被代理类实现的⼀些接⼝(这个参数的定义, 也决定了JDK动态代理只能代理实现了接⼝的⼀些类)
h : 实现了 InvocationHandler 接⼝的对象
总的调用流程
但是有个缺陷: JDK只能代理接口,不能代理类
CGLIB动态代理
JDK 动态代理有⼀个最致命的问题是其只能代理实现了接⼝的类.
有些场景下, 我们的业务代码是直接实现的, 并没有接⼝定义. 为了解决这个问题, 我们可以⽤ CGLIB 动态代理机制来解决.
CGLIB(Code Generation Library)是⼀个基于ASM的字节码⽣成库,它允许我们在运⾏时对字节码进⾏修改和动态⽣成. CGLIB 通过继承⽅式实现代理, 很多知名的开源框架都使⽤到了CGLIB. 例如 Spring 中的 AOP 模块中: 如果目标对象实现了接口,则默认采⽤ JDK 动态代理, 否则采⽤ CGLIB 动态代理.(它既可以代理类,也可以代理接口)
添加依赖
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version>
</dependency>
CGLIB 动态代理类实现步骤
1. 定义⼀个类(被代理类)
2. 自定义 MethodInterceptor 并重写 intercept ⽅法, intercept ⽤于增强⽬标⽅法,和 JDK 动态代理中的 invoke 方法类似
3. 通过 Enhancer 类的 create()创建代理类
⾃定义 MethodInterceptor(⽅法拦截器) 实现MethodInterceptor接⼝
创建代理类, 并使⽤
代码简单讲解
1. MethodInterceptor
MethodInterceptor 和 JDK动态代理中的 InvocationHandler 类似, 它只定义了⼀个⽅法 intercept() , ⽤于增强⽬标⽅法.
2. Enhancer.create()
Enhancer.create() ⽤来⽣成⼀个代理对象
参数说明:
type: 被代理类的类型(类或接⼝)
callback: ⾃定义⽅法拦截器 MethodInterceptor
JDK和CGLib的区别(面试题)
Spring Boot是基Spring进行开发的,因此Spring又基于proxyTargetClass 进行开发的
因此俩者在不同设置上代理是不同的
总的来说: JDK可以代理接口(实现了接口的类),不可以代理类(没有实现接口的普通类). CGLib可以代理接口,又可以代理类
Sping
默认proxyTargetClass 是false
实现了接口 使用 JDK代理
普通类 使用CGLib代理
Spring Boot 从2.x之后
默认proxyTargetClass 是true
默认使用的是CGLib代理
可以参考这个图
源码解释
proxyTargetClass设置
我们下面会对这个图进行具体的解释
可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 来设置
注意:
Spring Boot 2.X开始, 默认使⽤CGLIB代理
可以通过配置项 spring.aop.proxy-target-class=false 来进⾏修改,设置默认为jdk代理 SpringBoot设置 @EnableAspectJAutoProxy ⽆效, 因为Spring Boot 默认使⽤ AopAutoConfiguration进⾏装配
设置CGLib代理(默认未true)
未实现接口
普通类
debug结果
实现了接口
实现了接口的类
debug结果
配置改为false
如果是代理普通类
任然还是CGLIib
如果是代理实现了接口的类
那么就强制转换为jdk代理
刚刚演示的是这一块代码的逻辑
总结八股文
1. 什么是AoP: 对于一类事情的解决..实现思想...实现方式(CGLib,JDK,Spring AOP..)
2. Spring AOP 的实现方式
1>通过注解
@Aspect
exevution,自定义注解
2>基于XML的方式(俩种)
3>基于代理
3. Spring AOP的实现原理
基于动态代理实现
1> JDK
2> CGLib
4. Spring 使用的是那种代理方式?
都使用了(看源码晓得的),在这里Spring和SpringBoot的实现有区别
主要和参数proxyTargetClass有关系
先解释这个图
再解释Spring和SpringBoot
Sping
默认proxyTargetClass 是false
实现了接口 使用 JDK代理
普通类 使用CGLib代理
Spring Boot 从2.x之后
默认proxyTargetClass 是true
默认使用的是CGLib代理
5. JDK 和 CGLib的区别
其他总结
1. AOP是⼀种思想, 是对某⼀类事情的集中处理. Spring框架实现了AOP, 称之为SpringAOP
2. Spring AOP常⻅实现⽅式有两种: 1. 基于注解@Aspect来实现 2. 基于⾃定义注解来实现, 还有⼀些更原始的⽅式,⽐如基于代理, 基于xml配置的⽅式, 但⽬标⽐较少⻅
3. Spring AOP 是基于动态代理实现的, 有两种⽅式: 1. 基本JDK动态代理实现 2. 基于CGLIB动态代理实现. 运⾏时使⽤哪种⽅式与项⽬配置(proxyTargetClass)和代理的对象(是否实现接口)有关.