Spring个人学习笔记3(AOP)

Spring个人学习笔记3(AOP),第1张

1.AOP的介绍 AOP 是在不改原有代码的前提下对其进行增强。 什么是AOP? AOP(Aspect Oriented Programming) 面向切面编程,一种编程范式,指导开发者如何组织程序结构。 OOP(Object Oriented Programming)面向对象编程 。我们都知道OOP 是一种编程思想,那么 AOP 也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们 两个是不同的编程范式。 AOP的作用: 作用 : 在不惊动原始设计的基础上为其进行功能增强,前面咱们有技术就可以实现这样的功能即代 理模式。 AOP的核心概念: 引入案例
@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.update();
    }
}

update执行的方法输出为:

 save执行的方法输出为:

select的方法输出为:

、 

 引出:这个功能是怎么实现的呢?AOP(无侵入式编程)

 

(1) Spring AOP 是对一个类的方法在不进行任何修改的前提下实现增强。对于上面的案例中BookServiceImpl 中有 save , update , delete select 方法 , 这些方法我们给起了一 个名字叫 连接点 (2) BookServiceImpl 的四个方法中, update delete 只有打印没有计算万次执行消耗时间, 但是在运行的时候已经有该功能,那也就是说 update delete 方法都已经被增强,所以 对于需要增 强的方法 我们给起了一个名字叫 切入点 (3) 执行 BookServiceImpl update delete 方法的时候都被添加了一个计算万次执行消耗时间 的功能,将这个功能抽取到一个方法中,换句话说就是 存放共性功能的方法 ,我们给起了个名字叫 知。 (4) 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述,我们给起了个名字叫 切面 (5) 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类我们也给起了个名字叫 通知类 至此 AOP 中的核心概念就已经介绍完了,总结下 : 连接点(JoinPoint) :程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等 在SpringAOP中,理解为方法的执行 切入点 (Pointcut): 匹配连接点的式子 SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法 ,一个具体的方法: com.itheima.dao 包下的 BookDao 接口中的无形参无返回值的 save 方法 匹配多个方法: 所有的 save 方法,所有的 get 开头的方法,所有以 Dao 结尾的接口中的任意 方法,所有带有一个参数的方法 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一 定要被增强,所以可能不是切入点。 通知 (Advice): 在切入点处执行的 *** 作,也就是共性功能 SpringAOP 中,功能最终以方法的形式呈现 通知类:定义通知的类 切面(Aspect):描述通知与切入点的对应关系。 AOP的入门案例 案例设定:测算接口执行效率,但是这个案例稍微复杂了点,我们对其进行简化。 简化设定:在方法执行前输出当前系统时间。 开发方式:xml或 注解 解决思路: 1. 导入坐标 (pom.xml) 2. 制作连接点 ( 原始 *** 作, Dao 接口与实现类 ) 3.制作共性功能(通知类与通知) 4.定义切入点 5.绑定切入点与通知关系(切面) 1.导入坐标
 
      org.aspectj
      aspectjweaver
      1.9.4
    

2.定义接口与实现类

package com.itheima.dao;

public interface BookDao {
    public void save();
    public void update();
}
package com.itheima.dao.impl;

import com.itheima.dao.BookDao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@Repository
public class BookDaoImpl implements BookDao {

    public void save() {
        System.out.println(System.currentTimeMillis());
        System.out.println("book dao save ...");
    }

    public void update(){
        System.out.println("book dao update ...");
    }
}

3.定义通知类和通知

通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。

 4.:定义切入点

BookDaoImpl 中有两个方法,分别是 save update ,我们要增强的是 update 方法,该如何定义呢 ? 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。 5. :制作切面 切面是用来描述通知和切入点之间的关系,如何进行关系的绑定? 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行 位置

 

 6:将通知类配给容器并标识其为切面类

 步骤7:开启注解格式AOP功能

 

8 运行程序

 通知类的关键:

package com.itheima.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : Runqiang Jiang
 * @create 2022/5/14 16:10
 */
//通知类必须配置成Spring管理的bean
@Component
//设置当前类为切面类类
@Aspect
public class MyAdvice {
    //设置切入点,要求配置在方法上方
    @Pointcut("execution( void com.itheima.dao.BookDao.update())")
    public void pt(){}
    //设置在切入点pt()的前面运行当前 *** 作(前置通知)
    @Before("pt()") //把切入点与通知联系起来
    public void method(){
        //定义好通知
        System.out.println(System.currentTimeMillis());
    }
}

AOP相关注解总结:

 

 AOP工作流程 3.1 AOP 工作流程 由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring加载bean说起: 流程 1:Spring 容器启动 容器启动就需要去加载 bean, 哪些类需要被加载呢 ? 需要被增强的类,如 :BookServiceImpl 通知类,如 :MyAdvice 注意此时 bean 对象还没有创建成功 流程 2: 读取所有切面配置中的切入点

流程 3: 初始化 bean         判定bean 对应的类中的方法是否匹配到任意切入点 ,注意第1 步在容器启动的时候, bean 对象还没有被创建成功。 要被实例化bean对象的类中的方法和切入点进行匹配

流程 4: 获取 bean 执行方法         获取的bean 是原始对象时,调用方法并执行,完成 *** 作         获取的bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成 *** 作 验证容器中是否为代理对象 为了验证 IOC 容器中创建的对象和我们刚才所说的结论是否一致,首先先把结论理出来 :         如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象         如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。 验证思路         1.要执行的方法,不被定义的切入点包含,即不要增强,打印当前类的 getClass() 方法         2.要执行的方法,被定义的切入点包含,即要增强,打印出当前类的 getClass() 方法         3.观察两次打印的结果  

 

 3.2 AOP核心概念

目标对象 (Target) :原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终 工作的 代理 (Proxy) :目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实 目标对象就是要增强的类 [ :BookServiceImpl ] 对应的对象,也叫原始对象,不能说它不能运 行,只能说它在运行的过程中对于要增强的内容是缺失的。 SpringAOP 是在不改变原有设计 ( 代码 ) 的前提下对其进行增强的,它的底层采用的是代理模式实现 的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [ :MyAdvice 中的 method 方法 ] 内容加进去,就实现了增强 , 这就是我们所说的代理 (Proxy)

 4AOP配置管理 切入点 : 要进行增强的方法 切入点表达式:要进行增强的方法的描述方式

 对于切入点表达式的语法为:

切入点表达式标准格式:动作关键字 (访问修饰符 返回值 包名./接口名.方法名(参数) 异常 名)
execution(public User com.itheima.service.UserService.findById(int))
execution   动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点   public: 访问修饰符, 还可以是 public private等,可以省略 User 返回值,写返回值类型 com.itheima.service 包名,多级包使用点连接 UserService:   类/接口名称   findById 方法名 int: 参数,直接写参数的类型,多个类型用逗号隔开 异常名: 方法定义中抛出指定异常,可以省略 通配符简化的 *** 作:

 

execution(void com.itheima.dao.BookDao.update())
 匹配接口,能匹配到 
execution(void com.itheima.dao.impl.BookDaoImpl.update())
 匹配实现类,能匹配到 
execution(* com.itheima.dao.impl.BookDaoImpl.update()) 
返回值任意,能匹配到
 execution(* com.itheima.dao.impl.BookDaoImpl.update(*)) 
返回值任意,但是update方法必须要有一个参数,无法匹配,要想匹配需要在update接口和实现类添加参数execution(void com.*.*.*.*.update())
 返回值为void,com包下的任意包三层包下的任意类的update方法,匹配到的是实现类,能匹配 execution(void com.*.*.*.update()) 
返回值为void,com包下的任意两层包下的任意类的update方法,匹配到的是接口,能匹配 
execution(void *..update()) 
返回值为void,方法名是update的任意包下的任意类,能匹配 
execution(* *..*(..))
 匹配项目中任意类的任意方法,能匹配,但是不建议使用这种方式,影响范围广
 execution(* *..u*(..)) 
匹配项目中任意包任意类下只要以u开头的方法,update方法能满足,能匹配 
execution(* *..*e(..))
 匹配项目中任意包任意类下只要以e结尾的方法,update和save方法能满足,能匹配 
execution(void com..*()) 
返回值为void,com包下的任意包任意类任意方法,能匹配,*代表的是方法 execution(* com.itheima.*.*Service.find*(..)) 
将项目中所有业务层方法的以find开头的方法匹配
 execution(* com.itheima.*.*Service.save*(..)) 
将项目中所有业务层方法的以save开头的方法匹配
4.2 AOP 通知类型 AOP 通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合 理的位置,通知具体要添加到切入点的哪里? 共提供了5 种通知类型 : 前置通知 后置通知 环绕通知 ( 重点 ) 返回后通知 ( 了解 ) 抛出异常后通知 ( 了解

 

(1) 前置通知,追加功能到方法执行前 , 类似于在代码 1 或者代码 2 添加内容 (2) 后置通知 , 追加功能到方法执行后 , 不管方法执行的过程中有没有抛出异常都会执行,类似于在代 5 添加内容 (3) 返回后通知 , 追加功能到方法执行后,只有方法正常执行结束后才进行 , 类似于在代码 3 添加内容, 如果方法执行抛出异常,返回后通知将不会被添加 (4) 抛出异常后通知 , 追加功能到方法抛出异常后,只有方法执行出异常才进行 , 类似于在代码 4 添加内 容,只有方法抛出异常后才会被添加 (5) 环绕通知 , 环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式, 它可以实现其他四种通知类型的功能
package com.itheima.aop;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : Runqiang Jiang
 * @create 2022/5/15 10:38
 */

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}
    @Pointcut("execution(int com.itheima.dao.BookDao.select())")
    private void pt2(){}

    //@Before:前置通知,在原始方法运行之前执行
  //  @Before("pt()")
    public void before() {
        System.out.println("before advice ...");
    }

    //@After:后置通知,在原始方法运行之后执行
    //@After("pt()")
    public void after() {
        System.out.println("after advice ...");
    }

    //@Around:环绕通知,在原始方法运行的前后执行
//    @Around("pt()")
    public void around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始 *** 作的调用
        pjp.proceed();
        System.out.println("around after advice ...");
    }
   @Around("pt2()")
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("around before advice ...");
        //表示对原始 *** 作的调用
        Integer ret = (Integer) pjp.proceed();
        System.out.println("around after advice ...");
        return ret;
    }

    //@AfterReturning:返回后通知,在原始方法执行完毕后运行,且原始方法执行过程中未出现异常现象
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }

    //@AfterThrowing:抛出异常后通知,在原始方法执行过程中出现异常后运行
   @AfterThrowing("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}

注解总结:

环绕通知注意事项 1. 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法 调用前后同时添加通知 2.  通知中如果未使用ProceedingJoinPoint对原始方法   进行调用将跳过原始方法的执行 3. 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为 Object类型 4. 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void, 也可以设置成 Object 5. 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常

 

 4.3 业务层接口执行效率

需求:任意业务层接口执行均可显示其执行效率(执行时长)

这个案例的目的是查看每个业务层执行的时间,这样就可以监控出哪个业务比较耗时,将其查找出来 方便优化。 具体实现的思路 : (1) 开始执行方法之前记录一个时间 (2) 执行方法 (3) 执行完方法之后记录一个时间 (4) 用后一个时间减去前一个时间的差值,就是我们需要的结果。 所以要在方法执行的前后添加业务,经过分析我们将采用 环绕通知 说明 : 原始方法如果只执行一次,时间太快,两个时间差可能为 0 ,所以我们要执行万次来计算时间 差。
package com.itheima.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : Runqiang Jiang
 * @create 2022/5/15 11:08
 */
@Component
@Aspect
public class ProjectAdvice1 {
    //匹配业务层的所有的方法(切入点)
    @Pointcut("execution(* com.itheima.service.impl.AccountServiceImpl.*(..))")
    public  void servicePt(){}

    // 通知方法
    @Around("servicePt()")
    public void renSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //代表了一次执行的签名信息
        Signature signature = pjp.getSignature();
        //通过签名获取执行 *** 作名称(接口名)
        String className=signature.getDeclaringTypeName();
        //通过签名获取执行 *** 作名称(方法名)
        String methodName=signature.getName();
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
           pjp.proceed();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("业务层接口千次执行时间:"+className+"."+methodName+"--->"+(endTime-startTime)+"ms");


    }
}
4.4 AOP通知获取数据 目前我们写 AOP 仅仅是在原始方法前后追加一些 *** 作,接下来要说说 AOP 中数据相关的内容,将从获取 参数获取返回值获取异常 三个方面来研究切入点的相关信息。 前面介绍通知类型的时候总共讲了五种,那么对于这五种类型都会有参数,返回值和异常吗 ? 先来一个个分析下 : 获取切入点方法的参数所有的通知类型都可以获取参数 JoinPoint:适用于前置、后置、返回后、抛出异常后通知 ProceedingJoinPoint:适用于环绕通知 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究 返回后通知 环绕通知 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究 抛出异常后通知 环绕通知 例子:
package com.itheima.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : Runqiang Jiang
 * @create 2022/5/15 11:42
 */
@Component
@Aspect
public class MyAdvice1 {
    @Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
    private void pt(){}

    //JoinPoint:用于描述切入点的对象,必须配置成通知方法中的第一个参数,可用于获取原始方法调用的参数
    @Before("pt()")
    public void before(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("before advice ..." );
    }

    @After("pt()")
    public void after(JoinPoint jp) {
        Object[] args = jp.getArgs();
        System.out.println(Arrays.toString(args));
        System.out.println("after advice ...");
    }

    //ProceedingJoinPoint:专用于环绕通知,是JoinPoint子类,可以实现对原始方法的调用
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] =99999;
        Object ret = null;
        try {
            ret = pjp.proceed(args);
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return ret;
    }

    //设置返回后通知获取原始方法的返回值,要求returning属性值必须与方法形参名相同
    @AfterReturning(value = "pt()",returning = "ret")
    public void afterReturning(JoinPoint jp,String ret) {
        System.out.println("afterReturning advice ..."+ret);
    }

    //设置抛出异常后通知获取原始方法运行时抛出的异常对象,要求throwing属性值必须与方法形参名相同
   @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }
}
非环绕通知获取方式 在方法上添加 JoinPoint, 通过 JoinPoint 来获取参数 思考 : 方法的参数只有一个,为什么获取的是一个数组 ? 因为参数的个数是不固定的,所以使用数组更通配些。 环绕通知获取方式 环绕通知使用的是 ProceedingJoinPoint ,因为 ProceedingJoinPoint JoinPoint 类的子 类,所以对于 ProceedingJoinPoint 类中应该也会有对应的 getArgs() 方法, 对于返回值,只有返回后 AfterReturing 和环绕 Around 这两个通知类型可以获取,具体如何获取 ? 环绕通知获取返回值 4.5 百度网盘密码数据兼容处理   需求 : 对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理。

 

 

当我们从别人发给我们的内容中复制提取码的时候,有时候会多复制到一些空格,直接粘贴到百度 的提取码输入框,但是百度那边记录的提取码是没有空格的。这个时候如果不做处理,直接对比的话,就会引发提取码不一致,导致无法访问百度盘上的内容 ,所以多输入一个空格可能会导致项目的功能无法正常使用。此时我们就想能不能将输入的参数先帮用户去掉空格再 *** 作呢? 答案是可以的,我们只需要在业务方法执行之前对所有的输入参数进行格式处理 ——trim() 是对所有的参数都需要去除空格么 ? 也没有必要,一般只需要针对字符串处理即可。 以后涉及到需要去除前后空格的业务可能会有很多,这个去空格的代码是每个业务都写么 ? 可以考虑使用AOP来统一处理。 AOP 有五种通知类型,该使用哪种呢 ? 我们的需求是将原始方法的参数处理后在参与原始方法的调用,能做这件事的就只有环绕通知。 综上所述,我们需要考虑两件事 : ①:在业务方法执行之前对所有的输入参数进行格式处理 —— trim() ②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
package com.itheima.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * Created by IntelliJ IDEA.
 *
 * @Author : Runqiang Jiang
 * @create 2022/5/15 15:35
 */
@Component
@Aspect
public class DataAdvice {

    @Pointcut("execution(boolean com.itheima.service.ResourcesService.openURL(*,*))")
    private void servicePt(){}

    @Around("DataAdvice.servicePt()")
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        //对象数组
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            // 判断参数是不是字符串
            if(args[i].getClass().equals(String.class)){
               args[i]= args[i].toString().trim();
            }
        }
        // 改完去掉空格的参数再放进去
        Object proceed = pjp.proceed(args);
        return proceed;
    }
}

 5AOP总结 5.1 AOP 的核心概念 概念: AOP(Aspect Oriented Programming) 面向切面编程,一种编程范式 作用:在不惊动原始设计的基础上为方法进行功能 增强 核心概念 代理(Proxy ): SpringAOP 的核心本质是采用代理模式实现的 连接点(JoinPoint):在SpringAOP中,理解为任意方法的执行 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法 切面(Aspect):描述通知与切入点的对应关系 目标对象( Target ):被代理的原始对象成为目标对象 5.2 切入点表达式 切入点表达式标准格式: 动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常 名) 切入点表达式描述通配符: 作用:用于快速描述,范围描述 *:匹配任意符号(常用) .. :匹配多个连续的任意符号(常用) +:匹配子类类型 切入点表达式书写技巧 1. 标准规范 开发 2. 查询 *** 作的返回值建议使用 * 匹配 3. 减少使用 .. 的形式描述包 4. 对接口 进行描述 ,使用 * 表示模块名,例如 UserService 的匹配描述为 *Service 5. 方法名书写保留动 词,例如 get ,使用 * 表示名词,例如 getById 匹配描述为 getBy* 6. 参数根据实际情况灵活调整 5.3 五种通知类型 前置通知 后置通知 环绕通知(重点) 环绕通知依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用 环绕通知可以隔离原始方法的调用执行 环绕通知返回值设置为 Object 类型 环绕通知中可以对原始方法调用过程中出现的异常进行处理 返回后通知 抛出异常后通知 1 execution(* com.itheima.service.*Service.*(..)) 5.4 通知中获取参数 获取切入点方法的参数,所有的通知类型都可以获取参数 JoinPoint:适用于前置、后置、返回后、抛出异常后通知 ProceedingJoinPoint:适用于环绕通知 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究 返回后通知 环绕通知 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究 抛出异常后通知 环绕通知 6.1 Spring事务简介 6.1.1 相关概念介绍 事务作用:在数据层保障一系列的数据库 *** 作 同成功同失败 Spring 事务作用:在数据层或 业务层 保障一系列的数据库 *** 作同成功同失败 数据层有事务我们可以理解,为什么业务层也需要处理事务呢 ? 举个简单的例子, 转账业务会有两次数据层的调用,一次是加钱一次是减钱 把事务放在数据层,加钱和减钱就有两个事务 没办法保证加钱和减钱同时成功或者同时失败 这个时候就需要将事务放在业务层进行处理。 Spring为了管理事务,提供了一个平台事务管理器PlatformTransactionManager commit 是用来提交事务, rollback 是用来回滚事务。 PlatformTransactionManager 只是一个接口, Spring 还为其提供了一个具体的实现 : 从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。其 内部采用的是JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理 器来管理你的事务。而Mybatis 内部采用的就是 JDBC 的事务,所以后期我们 Spring整合Mybatis就采用的这个DataSourceTransactionManager事务管理器。 6.1.2 转账案例 - 需求分析 接下来通过一个案例来学习下 Spring 是如何来管理事务的。 先来分析下需求 : 需求: 实现任意两个账户间转账 *** 作 需求微缩: A账户减钱,B账户加钱 为了实现上述的业务需求,我们可以按照下面步骤来实现下 : ①:数据层提供基础 *** 作,指定账户减钱(outMoney ),指定账户加钱( inMoney ②:业务层提供转账 *** 作( transfer ),调用减钱与加钱的 *** 作 ③:提供 2 个账号和 *** 作金额执行转账 *** 作 ④:基于Spring整合MyBatis环境搭建上述 *** 作 运行单元测试类,会执行转账 *** 作, Tom 的账户会减少 100 Jerry 的账户会加 100 这是正常情况下的运行结果,但是如果在转账的过程中出现了异常,如 :
import com.itheima.dao.AccountDao;
import com.itheima.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
         int i = 1/0;
        accountDao.inMoney(in,money);
    }

}
这个时候就模拟了转账过程中出现异常的情况,正确的 *** 作应该是转账出问题了, Tom 应该还是 900 Jerry 应该还是 1100 ,但是真正运行后会发现,并没有像我们想象的那样, Tom 账户为 800 Jerry 还是 1100,100 块钱凭空消息了,银行乐疯了。如果把转账换个顺序,银行就该哭了。 不管哪种情况,都是不允许出现的,对刚才的结果我们做一个分析 : ①:程序正常执行时,账户金额 A B 加,没有问题 ②:程序出现异常后,转账失败,但是异常之前 *** 作成功,异常之后 *** 作失败,整体业务失败 当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而 Spring 的事务管理就是用来解决这类问题的。 Spring事务管理具体的实现步骤为: 步骤 1: 在需要被事务管理的方法上添加注解
public interface AccountService {
    /**
     * 转账 *** 作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    @Transactional
    public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    @Transactional
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
         int i = 1/0;
        accountDao.inMoney(in,money);
    }

}
注意 : @Transactional 可以写在接口类上、接口方法上、实现类上和实现类方法上 写在接口类上,该接口的所有实现类的所有方法都会有事务 写在接口方法上,该接口的所有实现类的该方法都会有事务 写在实现类上,该类中的所有方法都会有事务 写在实现类方法上,该方法上有事务 建议写在实现类或实现类的方法上 步骤 2: JdbcConfig 类中配置事务管理器
public class JdbcConfig {
    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String userName;
    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }

    //配置事务管理器,mybatis使用的是jdbc事务
   @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
   }
注意: 事务管理器要根据使用技术进行选择, Mybatis 框架使用的是 JDBC 事务,可以直接使用 DataSourceTransactionManager 步骤 3 :开启事务注解 SpringConfig 的配置类中开启
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
步骤 4: 运行测试类 会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性。

 

6.2 Spring 事务角色 要理解两个概念,分别是 事务管理员事务协调员 1. 未开启 Spring 事务之前 :

 

AccountDao outMoney 因为是修改 *** 作,会开启一个事务 T1 AccountDao inMoney 因为是修改 *** 作,会开启一个事务 T2 AccountService的transfer没有事务, 运行过程中如果没有抛出异常,则T1T2都正常提交,数据正确 如果在两个方法中间抛出异常,T1因为执行成功提交事务,T2因为抛异常不会被执行 就会导致数据出现错误 2. 开启 Spring 的事务管理后 transfer上添加了@Transactional注解,在该方法上就会有一个事务T AccountDao outMoney 方法的事务 T1 加入到 transfer 的事务 T AccountDao inMoney 方法的事务 T2 加入到 transfer 的事务 T 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性 通过上面例子的分析,可以得到如下概念 : 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法 注意 : 目前的事务管理是基于 DataSourceTransactionManagerSqlSessionFactoryBean使用的是同一 个数据源 6.3 Spring 事务属性 除了这两个概念,还有就是事务的其他相关配置都有哪些,就是接下来的内容。 主要学习三部分内容 事务配置转账业务追加日志事务传播行为 6.3.1 事务配置

 

上面这些属性都可以在@Transactional注解的参数上进行设置。 readOnly true 只读事务, false 读写事务,增删改要设为 false, 查询设为 true timeout: 设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚, -1 表示不设置超 时时间。 rollbackFor: 当出现指定异常进行事务回滚 noRollbackFor: 当出现指定异常不进行事务回滚 思考 : 出现异常事务会自动回滚,这个是我们之前就已经知道的 noRollbackFor 是设定对于指定的异常不回滚,这个好理解 rollbackFor 是指定回滚异常,对于异常事务不应该都回滚么,为什么还要指定 ? 这块需要更正一个知识点,并不是所有的异常都会回滚事务,比如下面的代码就不会回滚
 public void transfer(String out,String in ,Double money) throws IOException {
        
            accountDao.outMoney(out,money);
            //int i = 1/0; //这个异常事务会回滚
          if(true){  throw  new IOException();} //这个异常事务就不会回滚
            accountDao.inMoney(in,money);
      
    }
public interface AccountService {
    //rollback:设置当前事务参与回滚的异常,默认非运行时异常不参与回滚
    @Transactional( rollbackFor = IOException.class)
    public void transfer(String out,String in ,Double money) throws IOException;
}
rollbackForClassName 等同于 rollbackFor, 只不过属性为异常的类全名字符串 noRollbackForClassName 等同于 noRollbackFor ,只不过属性为异常的类全名字符串 isolation设置事务的隔离级别 DEFAULT :默认隔离级别, 会采用数据库的隔离级别 READ_UNCOMMITTED : 读未提交 READ_COMMITTED : 读已提交 REPEATABLE_READ : 重复读取 SERIALIZABLE: 串行化 6.3.2 转账业务追加日志案例 6.3.2.1 需求分析 在前面的转案例的基础上添加新的需求,完成转账后记录日志。 需求:实现任意两个账户间转账 *** 作,并对每次转账 *** 作在数据库进行留痕 需求微缩: A 账户减钱, B 账户加钱,数据库记录日志 基于上述的业务需求,我们来分析下该如何实现 : ①:基于转账 *** 作案例添加日志模块,实现数据库中记录日志 ②:业务层转账 *** 作(transfer),调用减钱、加钱与记录日志功能 需要注意一点就是,我们这个案例的预期效果为 : 无论转账 *** 作是否成功,均进行转账 *** 作的日志留痕 添加 LogDao 接口
public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}
添加 LogService 接口与实现类
public interface LogService { 
void log(String out, String in, Double money);
 }
@Service 
public class LogServiceImpl implements LogService { 
@Autowired
 private LogDao logDao;
 @Transactional
 public void log(String out,String in,Double money ) { 
logDao.log("转账 *** 作由"+out+"到"+in+",金额:"+money); } 
}
在转账的业务中添加记录日志
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private LogService logService;

    public void transfer(String out,String in ,Double money) {
        try{
            accountDao.outMoney(out,money);
            int i = 1/0;
            accountDao.inMoney(in,money);
        }finally {
            logService.log(out,in,money);
        }
    }

}
步骤 5: 运行程序 当程序正常运行, tbl_account 表中转账成功, tbl_log 表中日志记录成功 当转账业务之间出现异常(int i =1/0),转账失败,tbl_account成功回滚,但是tbl_log 未添加数据 这个结果和我们想要的不一样,什么原因 ? 该如何解决 ? 失败原因:日志的记录与转账 *** 作隶属同一个事务,同成功同失败 最终效果:无论转账 *** 作是否成功,日志必须保留 6.3.3 事务传播行为

 

对于上述案例的分析 : log方法、inMoney方法和outMoney方法都属于增删改,分别有事务T1,T2,T3 transfer 因为加了 @Transactional 注解,也开启了事务 Spring事务会把T1,T2,T3都加入到事务T 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来 这和需求不符,这个时候我们就想能不能让 log 方法单独是一个事务呢 ? 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是 事务传播行为:事务协调员对事务管理员所携带事务的处理态度。 具体如何解决,就需要用到之前我们没有说的 propagation属性
public interface LogService {
    //propagation设置事务属性:传播行为设置为当前 *** 作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void log(String out, String in, Double money);
}

具体视频链接:

 黑马程序员2022最新SSM框架教程_Spring+SpringMVC+Maven高级+SpringBoot+MyBatisPlus企业实用开发技术_哔哩哔哩_bilibili

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/921392.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存