文章目录
- 前言
- AOP简介
- AOP入门案例
- AOP工作流程
- AOP切入点表达式
- AOP通知类型
- AOP通知获取数据
- 总结
前言
今天我们来学习
AOP
,在最初我们学习Spring时说过Spring的两大特征,一个是IOC
,一个是AOP
,我们现在要学习的就是这个AOP。
AOP简介
AOP:
面向切面编程
,一种编程范式,指导开发者如何组织程序结构。
作用:在不惊动原始设计的基础上为其进行功能增强
。
首先我们先来看看代码环境,在主方法中获取
BookDao对象
,并调用它的save()方法
,在BookDaoImpl
中save()方法是测试它的万次执行效率
,而此类中的别的方法没有这个功能👇👇。
BookDaoImpl
类
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i<10000;i++) {
System.out.println("book dao save ...");
}
//记录程序当前执行时间(结束时间)
Long endTime = System.currentTimeMillis();
//计算时间差
Long totalTime = endTime-startTime;
//输出信息
System.out.println("执行万次消耗时间:" + totalTime + "ms");
}
public void update(){
System.out.println("book dao update ...");
}
public void delete(){
System.out.println("book dao delete ...");
}
public void select(){
System.out.println("book dao select ...");
}
}
主方法
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.save();
}
}
运行结果
现在我们要说的不是这个,如上,
update()、delete()、select()方法
中并没有测试万次执行效率
这个功能,但是我在运行update()、delete()方法时,它也打印了执行万次消耗的时间,而select()方法没有👇👇。
这是因为我用了一种技术在
不惊动原始设计的基础上
想为谁追加功能就为谁追加功能,这就是AOP
,而不惊动原始设计也是我们Spring的一种理念:无入侵式/无侵入式
。
那它是怎么做的呢,我们来分析一下👇👇。
这是我们刚才的程序,在这里边观察红色框出来的四行,这是我们要执行的业务,除了这四行,我们蓝色框出来的是围绕着save()方法的业务上下两块东西,这两块东西实际上我也想让别的方法拥有,在这里为了让别的方法也有这个功能,我们就把这个东西抽取出来了👇👇。
将它抽取为一个单独的方法,叫什么不重要,注意看,我这里边拥有的内容和右边蓝色的内容完全对应,这是将我要每一个方法所具有的功能抽取出来得到的一个方法,这个时候就要区分一下了,在AOP中,我们称右边的save()、update()、select()、方法,也就是原始方法,叫它为
连接点
,而我们刚才是给每一个方法都追加功能了吗?没有,我们刚才只给update()、delete()方法追加了功能,select()没有追加,那么AOP对于要追加功能的这些方法,叫它切入点
,这个切入点就说明了哪些方法要追加功能,那按照刚才的现象来看,我们的select()属于不属于切入点,不属于,为什么,因为我刚才在追加功能的时候select()没有,接着说,我现在左边抽取出来的这个方法,这个方法叫什么呢,它说你要大家都有的功能,这是一组共性功能,它管这种共性功能叫通知
,接下来问题又来了,你说这个功能你这次做的要测它的性能,下次我们能不能做个别的功能,当然可以, 这种通知是不是可以开发好多个,可以根据我的需求开发第一种、第二种、第三种通知,那么问题就来了,你怎么知道在这俩方法上执行这个通知呢,我们是不知道的,因此看来在通知和切入点之间,还得有个东西,把它们俩绑定在一块,这样的话,一个通知就对应一个切入点,那么 这个东西叫什么呢,叫切面
,也就是说,切面描述的是你的这个通知的共性功能与对应的切入点的关系,有了这个关系它就知道了这俩方法对应这个通知,回头select()还有可能加入别的功能呢,到这里我们已经了解了这几个概念,最后我们再说一个, 因为通知是一个方法,在Java中我们不能直接写方法,我们把它放在一个类中,这个类给它个名称叫通知类
,到这里我们就得到了如下概念👇👇。
AOP入门案例
在前边我们已经介绍了AOP,现在我们来分析一下怎么做,并且去实现它,我们来实现的
入门案例
是:在接口执行前输出当前系统时间,在这里我们使用注解
的开发方式。
思路分析:
1.导入坐标(pom.xml)
2.制作连接点方法(原始操作,Dao接口与实现类)
3.制作共性功能(通知类,与通知)
4.定义切入点
5.绑定切入点与通知关系(切面)
分析完以后我们就要开始做了,首先来看一下代码环境👇👇。
-
BookDao接口
-
BookDao实现类
-
SpringConfig
-
主方法
-
执行save()方法
-
执行update()方法
我们可以通过执行结果看出 update()方法是没有获取当前系统时间这个功能的,现在我们要做的就是在不动原来代码的基础上,给update()方法增加获取当前系统时间的功能💪💪。
首先第一步,导入坐标,做AOP开发要导的坐标是spring-aop,但是这里需要说一点,看一下这个依赖关系👇👇。
在它的依赖关系中,
context
一旦导入,你会发现aop的包自动就导进来了,所以aop开发是默认导入的,除此之外还要导入一个aspectj
的包👇👇。
第二步就是制作连接点方法,也就是做好我们的实现类👇👇。
第三步就是把 获取当前系统时间的这个
共性功能
给它抽出来单独做,右键创建一个新的类:aop包下的MyAdvice类,在类中随便起一个方法,然后将功能添加进去👇👇。
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
第四步我们要去定义它的
切入点
,先写一个私有的方法,方法名任意,但是不要冲突,方法里边有方法体,但是啥也没写,然后在它上边写一句话:@Pointcut()
,括号里边写什么呢,给它一个参数("execution()")
,execution自带一个括号,括号里边是描述我们刚才写的那个method方法的,怎么描述呢,它是这样一个方法,返回值是void类型
的,com.itheima包下的dao包下的BookDao接口里边的update()方法,没有参数:@Pointcut("execution(void com.itheima.dao.BookDao.update())")
,这句话就是告诉我们当执行到了这个方法的时候,这就是一个切入点。
//设置切入点,要求配置在方法上方
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
第五步是绑定这个共性功能和这个切入点之间的关系,怎么绑,在这里我们假定需要让这个共性功能的方法在切入点前边执行,需要在共性功能的这个method方法上写上一个注解:
@Before("pt()")
。
//设置在切入点pt()的前面运行当前操作(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
现在还是不能运行的,因为现在这段程序还得受
Spring控制
,但是它现在不受控,第一件事在类的上方加上@Component
让它变成Spring控制的bean
,写完以后虽然Spring能控制它,把它造成bean了,但是Spring并不知道你这里边是做AOP的,所以我们得告诉Spring,当它扫描到我以后我这个当AOP处理,所以在MyAdvice类上方再写一个注解:@Aspect
,这样就解决了这个问题了,加完这两个注解还不行,最后一个地方,配置类这,现在这里边不知道你整个程序里边是拿注解
开发的AOP
,所以我们还要在配置类中加一个注解来告诉它:@EnableAspectJAutoProxy
,现在我们整体看一下👇👇。
-
MyAdvice类
-
SpringConfig配置类
-
执行save()方法
-
执行update()方法
AOP工作流程
前边我们讲了AOP的入门案例,现在我们要来说说它的
工作流程
,对于AOP的整个工作流程,第一步应该做什么事呢,肯定是从Spring开始干活说起,所以第一步是Spring容器启动
,启动完以后是不是要进行初始化bean
的这些操作了,不着急,对于aop的工作流程来说,它要去读取所有切面配置中的切入点
,这里可没有说读取所有配置
的切入点。
为什么这么说呢,我们来看一下,这是我们刚才写的那段代码, 多了一句话,也就是在里边多配置了一个切入点,那这句话说的什么意思呢,也就是说如果你定义了一个切入点,并且在这使用了,那么这个切入点我读取,上边那个我不读取,也就是说它只读取你配置的,为什么这么说呢,我们后边要做的工作要与这个切入点有关,假定你配了100个切入点,但是你一个也没用,那不相当于没有配吗,所以在这个地方我们要看它配了的切入点。
把这配完以后,接下来就要
初始化bean,判定bean对应类中的方法是否匹配到任意切入点
,如果匹配失败
,创建对象
,然后获取bean,调用方法并执行,完成操作,我们主要说的是匹配成功
的情况,如果匹配成功,创建原始对象(目标对象
)的代理
对象,在这里边我们看到了一个熟悉的东西代理
, 前边我们学习过jdk的代理模式
,代理可以干什么事:增强
,也就是说我用代理对象
去调用对应的方法然后走我们增强的那些操作,那么aop内部是怎么做的呢,Spring的aop内部就是用代理模式
来实现的,看到这明白了,闹了半天它也是代理
。
接着说,除了这个概念以外,还有一个概念,叫
目标对象
,也就是你对那个对象做的代理,原始的那个对象我们叫目标对象
,如果匹配成功,创建原始对象(目标对象
)的代理
对象,那我们能不能想到执行操作的时候是一个什么样的流程,它在获取bean
的时候还是拿的哪个原始对象吗,当然不是,在它的Spring容器中保存的就是那个代理对象
,剩下的事就简单了,获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
,也就是说aop的整个工作实际上用了一套什么样的形式来做的,代理模式
,这就是aop的一个核心本质
,用的代理来做的。
接下来我们去代码中看👇👇。
现在我们运行不是为了看结果,在这执行不执行都不重要,我们要去打印一下这个
bookdao
对象,来看看这个对象是不是一个代理对象
👇👇。
现在我们在MyAdvice中修改配置的切入点,让它匹配不到,并再一次运行程序👇👇。
我们会发现,两次运行的结果几乎一模一样,难道出问题了吗,我们修改一下,在主方法中修改一下,再打印一下
这个对象的所属的字节码文件对象
👇👇。
也是一样,运行两次👇👇。
- 没配置切入点的
- 配置切入点的
通过观察字节码文件对象,我们会发现当配置了切入点后,会变成代理类型的对象,也就是说它最终用的是代理的对象,类型是这个没问题,但是你要打印对象时,它会按
BookDao
对象显示,为什么会这样,简单说一下,就是在aop的这套东西中间,它对它最终的那个对象的toString()方法
进行了重写,所以我们才会看到现在这种显示效果。
AOP切入点表达式
AOP的基础入门案例我们已经做完了,并且也知道内部它是用代理的形式进行工作的,下面我们就要针对AOP的各个环节的细节进行学习了,前边我们不是写了一个很长的式子,我们说那是它的切入点,现在我们就要研究那个东西了,那个东西不叫切入点,叫
切入点表达式
。
首先我们要知道这个式子写的时候到底有没有什么要求呢,也就是说要去学习它的
语法格式
,先说两个东西,切入点是我们要进行增强的方法,切入点表达式是要进行增强的方法的描述方式,注意一点:对于任意一个方法,它的描述方式是多种多样的,看一下我们现在做的方法👇👇。
对于这一个式子,它的大的描述方向就分
两种
,第一种方向是按接口
描述,第二种方向是按实现类
描述,现在这两种形式都可以👇👇。
接下来我们来说说这么复杂的一个式子,到底有什么规则没有,它是有严格的规则的👇👇。
这么写其实很容易,但是如果你的程序中有几百个几千个切入点,还要一一去写吗,当然不是,不然就累死了,所以我们接下来学习一种能简化这种一个一个去写的形式:
使用通配符描述切入点
。
在AOP中,可以使用通配符描述切入点,通配符干嘛的,加速配置,加速描述的,接下来我们看一下👇👇。
接下来再看一下它的
书写技巧
👇👇。
AOP通知类型
前边我们研究的是切入点表达式,也就是想控制给谁加aop就给谁加aop,现在我们再研究的细节点,
往哪加
,也就是AOP的通知类型
。
我们要介绍几种不同的通知类型,先来说第一个问题,我们现在aop实际上是从原来的功能中抽一部分出去做成共性功能,然后用的时候再加回来,问题来了,原来从前边挖走的你现在运行不能给它执行到后边吧,同样你原来从后边挖走的你现在运行不能给它执行到前边吧,所以说放的位置很讲究,一种有几种位置呢,一共有
五种位置
👇👇。
我们现在进入到程序中👇👇。
-
BookDao类
-
BookDao实现类
-
MyAdvice类
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
//@Before:前置通知,在原始方法运行之前执行
public void before() {
System.out.println("before advice ...");
}
//@After:后置通知,在原始方法运行之后执行
public void after() {
System.out.println("after advice ...");
}
//@Around:环绕通知,在原始方法运行的前后执行
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
//@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
//@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
- 主方法
我们提前在MyAdvice类中写了一些方法用来演示这五种通知类型,接下来我们去演示👇👇。
-
前置通知
-
后置通知
-
环绕通知
环绕通知是比较重要的,我们来分析一下,首先里边有两条打印语句,我们想要做的效果是在原始操作的前后,分别打印这两条语句,我们先按之前的运行一下👇👇。
运行完我们会发现两条打印语句出来了,但是我的原始操作怎么不见了,这是为什么呢,这是因为我们在这做的是
环绕通知
,环绕的话肯定是在原始操作的前和后,这里边有一个核心叫做那一句话代表的是对原始操作的调用呢?如果没有的话,你说这两个打印语句是一前一后,我还说这俩都是前呢,另一个人还说这俩都是后呢,所以这里边必须有一句话表示对原始操作的调用
,怎么做呢,格式非常固定,在你的around修饰
的通知上参数
写上一个叫ProceedingJoinPoint pjp
,定义完这个参数使用pjp.proceed()
,这一句话就代表对原始操作的调用👇👇。
但是他为什么会画红线呢,我们来解释一下,它告诉你要
抛异常
,为什么要抛异常呢,因为原始操作的调用你无法预料它有没有异常,所以在这需要先抛一个异常,让你强制的对这个东西进行处理👇👇。
现在我们来运行一下👇👇。
结果符合预期,这就叫做环绕通知,也是功能最强大的,说一个小细节,现在我的接口中除了有这个update()方法还有select()方法,我现在在主方法中调用这个select()方法👇👇。
因为现在程序中没有配置它的切入点,所以执行结果只有他自己的功能,现在我们去MyAdvice中定义一个全新的切入点 👇👇。
然后改一下刚才的环绕通知👇👇。
我们再来运行一下👇👇。
注意看,原始操作运行了嘛,运行了,环绕通知的前和后加上没有,加上了,但是最后
抛了一个异常
,这个异常是啥意思呢,解释一下,空的返回值从advice中出来了,它不匹配你的原始操作调用的返回值类型,原始操作的返回值是什么,是 int,那好解释一下这件事情,我们的环绕around对原始操作增强的时候,你记得原来如果没有返回值还好,但是如果原来有返回值的话,在环绕通知的最后边要将它的返回值扔出去,所以这个环绕通知的返回值也得改,改为什么呢,改为Object
,也就是返回一个对象,我们先返回一个200,运行一下,看程序有没有报错👇👇。
我们会发现首先
不会报错
,并且原始方法的返回值我已经篡改了,这里边的return将代表你原始操作的运行,简单一点就是原来你调你的select方法实际上走的是他对应的实现类,现在变成你先运行第一句打印,然后你运行原始操作,再运行第二句打印,再运行return,这个方法才算运行完,换句话说你原来的代理模式走完了,return的是这个200,那有人说那我的100哪里去了,在你运行原始操作那呢,这个方法有一个返回值,把这个返回值给它接出来,最后返回就行了💪💪。
-
返回后通知
-
抛出异常后通知
AOP通知获取数据
现在我们要来说说AOP里边的数据了,什么意思呢,就是现在我能够对你的功能进行干预了,加东西了,但是有一点,有些时候不是所有的情况都是统一处理的,比如说你过来一个参数,参数不一样我处理的方式不一样,这是我们经常见到的一些需求,那面对这种情况我们如果在通知里边拿不到我们原始通知的数据,你就玩不下去了,因此我们来说说
怎么样从AOP里边拿取通知的数据
。
那方式有多少种呢,有三种,第一种,获取参数,第二种,获取返回值,第三种,获取异常,对于这三个东西,我们需要分析一个问题,是不是所有的通知都有这些东西,当然不是,对于参数来说,所有的通知都能拿到,比如你现在调用一个方法,我不管你最后是正常结束还是异常结束了,你最起码调用的时候参数都有的呀,所以说参数是每个里边都有的,但是返回值就不是了,它必须得有保障原始操作正常执行,你才能在AOP中拿到原始操作的返回值,所以说,这里边只有两个东西能拿到返回值,哪两个呢,一个是返回后通知,一个是环绕通知,那接下来我们到程序中将这些信息拿一遍💪💪。
-
BookDao接口
-
BookDao实现类
-
主方法
接下来我们来
实现AOP获取数据
👇👇。
给方法里边设置一个
JoinPoint
参数,并通过getArgs()方法
获取参数,返回值是一个对象数组,通过Arrays.toString()去输出👇👇。
我们来改一改,给它设置两个参数,注意一点,我们的aop里边没动,注意看能拿到什么👇👇。
是不是都拿到了,后置通知也一模一样🎉🎉。
现在我们来说
环绕通知
,首先想一件事,JoinPoint是ProceedJoinPoint的父接口
,父接口都能调到的方法,子接口肯定能调到,在这就不打印它了,因为不是我们主体要说的,下面要说我们是不是在这调用原始方法了,注意调用原始方法的时候对于proceed这个操作,除了空参以外,还可以传递一个Object数组
👇👇。
也就是说我们可以把通过getArgs()获取的参数给proceed传进去用,写与不写代表的含义都一样,都代表使用getArgs()得到的参数来传递,我们来运行一下👇👇。
结果是没问题的,我们这里想说的是我要是给它使点坏,比如说在你获取参数以后,发送调用之前,我要把这里边的东西给改了,那会是为什么样呢?
我们会发现已经变成666了,现在我们已经做到了一个非常好的效果了,是如果传过来的参数有问题,我们就可以处理一下了💪💪。
接下来说
返回值
的获取,我们前边已经说过环绕通知了,现在只需要说一下返回后通知
🎉🎉。
在这里你如果想拿它的返回值,你可以先去定义一个用于接收返回值的形参,如下👇👇。
但是你要用这个东西,你必须告诉afterReturning你用ret这个变量准备接它的返回值,属性returning是专门干这事的👇👇。
这句话是什么意思,是如果你的原始方法有返回值,那就把返回值装到形参中叫ret的这个变量里,我们来运行一下👇👇。
在这里有一个问题,如果JoinPoint跟它同时存在,顺序必须是JoinPoint在前,如果不是第一个必报错👇👇。
接下来我们来说一下异常的,对于异常的怎么拿,在这虽然能拿到👇👇。
但是在这里边拿不到👇👇。
那怎么办呢,回归到最原始的 try catch中,这个 throwable就是你最终的异常对象了👇👇。
抛出异常后通知接收异常对象👇👇。
在这写完以后我们在原操作中抛异常👇👇。
注意看afterThrowing后边有一个异常信息,这个就是带过来的。
总结
以上就是我们学习AOP的全部内容,如果有什么错误的话,大家可以私信我📬📬,
希望大家多多关注+点赞+收藏 ^_ ^🙏🙏,你们的鼓励是我不断前进的动力💪💪!!!