【Spring(十一)】万字带你深入学习面向切面编程AOP

奋斗吧
奋斗吧
擅长邻域:未填写

标签: 【Spring(十一)】万字带你深入学习面向切面编程AOP

2023-03-11 03:00:07 147浏览

今天我们来学习AOP,在最初我们学习Spring时说过Spring的两大特征,一个是IOC,一个是AOP,我们现在要学习的就是这个AOP。面向切面编程,一种编程范式,指导开发者如何组织程序结构。作用:在不惊动原始设计的基础上为其进行功能增强。首先我们先来看看代码环境,在主方法中获取BookDao对象,并调用它的save()方法,在中save()方法是测试它的万次执行效率,而此类中的别的方法没有这个功能??。类。

前言

  今天我们来学习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的全部内容,如果有什么错误的话,大家可以私信我??,希望大家多多关注+点赞+收藏 ^_ ^??,你们的鼓励是我不断前进的动力??!!!

好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695