Spring - 3( AOP )

Spring - 3

AOP 简介

  • AOP ( Aspect Oriented Programming ) 面向切面编程,一种编程范式,指导开发者如何组织程序结构
    • OOP ( Object Oriented Programming ) 面向对象编程
    • OOP 是一种编程思想,那么 AOP 也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式
  • AOP 作用:在不惊动原有代码的前提下对其进行功能增强
  • Spring 理念:无入侵式 / 无侵入式

核心概念

  • 为了能更好的理解 AOP 的相关概念,准备了一个环境,主要看类 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 ...");
        }
    }
    
  • 此时在 main 中使得不仅是执行 save 方法会重复输出一万次并显示执行时间,就连 update 和 delete 方法执行时也会重复输出一万次并显示执行时间,但 select 方法只是输出一句 book dao select ...

    • 上述代码从表面根本看不出来,这就是 AOP 所做
    • 代码无 spring 痕迹,却增加了功能 —— 无入侵式
  • 其中:

    • BookServiceImpl 中的 save、update、delete 和 select 所有的方法 —— 连接点
    • update、delete 仅是需要增强的方法 —— 切入点
    • save 方法中编写的、计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,即:存放共性功能的方法 —— 通知
    • 通知是一个方法,方法不能独立存在需要被写在一个类中,这个类 —— 通知类
    • 通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于通知和切入点之间的关系描述 —— 切面

总结

  • 连接点 ( JoinPoint ):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
    • AOP 不仅是在 spring 中存在,所以上述为较标准说法
    • 在 SpringAOP 中,理解为方法的执行,各个方法
  • 切入点 ( Pointcut ):匹配连接点的式子
    • 在 SpringAOP 中,一个切入点可以描述一个具体方法,也可也匹配多个方法
      • 一个具体的方法:如 BookDao 接口中的无形参无返回值的 save 方法
      • 匹配多个方法:所有的 save 方法,所有的 get 开头的方法,所有以 Dao 结尾的接口中的任意方法,所有带有一个参数的方法
    • 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点
  • 通知 ( Advice ):在切入点处执行的操作,也就是共性功能
    • 在 SpringAOP 中,功能最终以方法的形式呈现
  • 通知类:定义通知的类
  • 切面 ( Aspect ):描述通知与切入点的对应关系

AOP 入门案例

  • 使用 SpringAOP 的注解方式完成在方法执行的前打印出当前系统时间

环境准备

  • 添加 Spring 依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    
  • 添加 BookDao 和 BookDaoImpl 类

    public interface BookDao {
        public void save();
        public void update();
    }
    
    @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 ...");
        }
    }
    
  • 创建 Spring 的配置类

    @Configuration
    @ComponentScan("com.qst")
    public class SpringConfig {
    }
    
  • 编写 App 运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.save();
        }
    }
    

AOP 实现步骤

  1. 添加依赖

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
    
    • 因为 spring-context 中已经导入了 spring-aop,所以不需要再单独导入 spring-aop 依赖
    • 导入 AspectJ 的 jar 包,AspectJ 是 AOP 思想的一个具体实现,Spring 有自己的 AOP 实现,但是相比于 AspectJ 来说比较麻烦,所以我们直接采用 Spring 整合 ApsectJ 的方式进行 AOP 开发
  2. 定义接口与实现类

    • 环境准备时候的 BookDaoImpl 代码不变
  3. 定义通知类和通知

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

      public class MyAdvice {
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      
  4. 通知类中编写切入点

    • BookDaoImpl 中有两个方法,分别是 save 和 update,我们要增强的是 update 方法

      public class MyAdvice {
          @Pointcut("execution(void com.qst.dao.BookDao.update())") // 切入点:表示执行到update方法的时候
          private void pt(){} // 私有的自定义空壳
          
          public void method(){
              System.out.println(System.currentTimeMillis());
          }
      }
      
      • 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑
  5. 制作切面

    public class MyAdvice {
        @Pointcut("execution(void com.qst.dao.BookDao.update())")
        private void pt(){}
        
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
    • 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
    • @Before 表示通知会在切入点方法执行之前执行
  6. 将通知类配给容器并标识其为切面类

    @Component
    // 表示扫描到此时,当作aop来处理:
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.itheima.dao.BookDao.update())")
        private void pt(){}
        
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
  7. 开启注解格式 AOP 功能

    @Configuration
    @ComponentScan("com.qst")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
    • 感觉就像是:@EnableAspectJAutoProxy 开启了 @Aspect 的使用,@Aspect 开启了 @Pointcut 和 @Before 的作用
  8. 运行程序

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.update();
        }
    }
    
    • 若是看到在执行 update 方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP 编程成功

AOP 工作流程

AOP 工作流程

  • 由于 AOP 是基于 Spring 容器管理的 bean 做的增强,所以整个工作过程需要从 Spring 加载 bean 说起
  1. Spring 容器启动
    • 容器启动就需要去加载 bean:
      • 需要被增强的类,如:BookServiceImpl
      • 通知类,如:MyAdvice
      • 注意,此时 bean 对象还没有创建成功
  2. 读取所有切面配置中的切入点
    • 将需要的 @Pointcut 注解的切入点加以使用
  3. 初始化 bean,判定 bean 对应的类中的方法是否匹配到任意切入点
    • 注意第一步在容器启动的时候,bean 对象还没有被创建成功。
    • 要被实例化 bean 对象的类中的方法和切入点进行匹配
      • 即:看 @Pointcut 注解的 execution 路径对应的 bean 对象
    • 匹配失败,创建原始对象,如 UserDao ( 上述案例中 @Pointcut("execution(void com.itheima.dao.BookDao.update())") 不是 UserDao 对象 )
      • 匹配失败说明不需要增强,直接调用原始对象的方法即可
    • 匹配成功,创建原始对象 ( 目标对象 ) 的代理对象,如:BookDao
      • 匹配成功说明需要对其进行增强
      • 对哪个类做增强,这个类对应的对象就叫做目标对象
      • 因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象
      • 最终运行的是代理对象的方法,在该方法中会对原始方法进行功能增强
  4. 获取 bean 执行方法
    • 获取的 bean 是原始对象时,调用方法并执行,完成操作
    • 获取的 bean 是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
  • 即 AOP 核心是使用的代理模式

验证容器中是否为代理对象

  • App 类中:

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            System.out.println(bookDao);
            System.out.println(bookDao.getClass());
        }
    }
    
  • 修改 MyAdvice 类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.itheima.dao.BookDao.update1())")
        private void pt(){}
        
        @Before("pt()")
        public void method(){
            System.out.println(System.currentTimeMillis());
        }
    }
    
    • 因为定义的切入点中,被修改成 update1,所以 BookDao 中的 update 方法在执行的时候,就不会被增强,所以容器中的对象应该是目标对象本身
  • 但若是改回来 update:@Pointcut("execution(void com.itheima.dao.BookDao.update())")

    • 就会发现 bookDao.getClass() 打印出类似 class com.sun.proxy.$Proxy19,而 bookDao 打印出的结果没有变化,所以查看时,就只使用 .class

AOP核心概念

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

AOP 配置管理

AOP 切入点表达式

  • 就是上述案例中的 @Pointcut 中的 execution( ...... ) 部分

语法格式

  • 首先要先明确两个概念:

    • 切入点:要进行增强的方法
    • 切入点表达式:要进行增强的方法的描述方式
  • 对于切入点的描述,其实是有两种方式:

    • 一是只有方法名的 dao 接口,执行 com.qst.dao 包下的 BookDao 接口中的无参数 update 方法

      execution(void com.qst.dao.BookDao.update())
      
    • 二是继承了接口的 daoImpl 类,执行 com.qst.dao.impl 包下的 BookDaoImpl 类中的无参数 update 方法

      execution(void com.qst.dao.impl.BookDaoImpl.update())
      
  • 因为调用接口方法的时候最终运行的还是其实现类的方法,所以上面两种描述方式都是可以的

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

    • 切入点表达式标准格式:动作关键字 ( 访问修饰符 返回值 包名.类 / 接口名.方法名 ( 参数 ) 异常名)
    • 不需要硬记,而是通过一个例子理解:

      execution(public User com.qst.service.UserService.findById(int))
      
    • execution:动作关键字,描述切入点的行为动作,例如 execution 表示执行到指定切入点
    • public:访问修饰符,还可以是 public,private 等,可以省略
    • User:返回值,写返回值类型
    • com.qst.service:包名,多级包使用符号 . 点来连接
    • UserService:类 / 接口名称
    • findById:方法名
    • int:参数,直接写参数的类型,多个类型用逗号隔开
    • 异常名:方法定义中抛出指定异常,可以省略
  • 切入点表达式就是要找到需要增强的方法,所以它就是对一个具体方法的描述,但是方法的定义会有很多,所以如果每一个方法对应一个切入点表达式,想想就会觉得编写起来会比较麻烦,就需要用到下面所学习的通配符

通配符

  • 我们使用通配符描述切入点,主要的目的就是简化之前的配置

  • *:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

    execution (public * com.qst.*.UserService.find*(*))
    
    • 匹配 com.qst 包下的任意包中的 UserService 类或接口中所有 find 开头的带有一个参数的方法
  • ..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

    execution (public User com..UserService.findById(..))
    
    • 匹配 com 包下的任意包中的 UserService 类或接口中所有名称为 findById 的方法
    • 参数列表处写 .. 表示任意参数,但不包含无参情况 ( 即一定是有参的任意 )
  • +:专用于匹配子类类型 ( 很少用 )

    execution(* *..*Service+.*(..))
    
    • 这个使用率较低,描述子类的,做 JavaEE 开发,继承机会就一次,使用都很慎重,所以很少用。*Service+,表示所有以 Service 结尾的接口的子类
  • 环境若为:com.qst.dao 下有 BookDao 接口,内含无参 save、update 方法;com.qst.dao.impl 下有 BookDaoImpl implements BookDao,完善 save、update 方法功能

    execution(void com.qst.dao.BookDao.update())
    匹配接口,能匹配到
    
    execution(void com.qst.dao.impl.BookDaoImpl.update())
    匹配实现类,能匹配到
    
    execution(* com.qst.dao.impl.BookDaoImpl.update())
    返回值任意,能匹配到
    
    execution(* com.qst.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.qst.*.*Service.find*(..))
    将项目中所有业务层方法的以find开头的方法匹配
    
    execution(* com.qst.*.*Service.save*(..))
    将项目中所有业务层方法的以save开头的方法匹配
    

书写技巧

对于切入点表达式的编写其实是很灵活的,在编写的时候有一些好的技巧使用:

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类
    • 如果描述到实现类,就出现紧耦合了
  • 访问控制修饰符针对接口开发均采用 public 描述 ( 可省略访问控制修饰符描述 )
  • 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用 * 通配快速描述
  • 包名书写尽量不使用 .. 匹配,效率过低,常用 * 做单个包描述匹配,或精准匹配
  • 接口名 / 类名书写名称与模块相关的采用 * 匹配,例如 UserService 书写成 *Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用 * 匹配,例如 getById 书写成 getBy*,selectAll 直接书写成 selectAll 就可
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

AOP 通知类型

类型介绍

image-20230823154713858

  • 共提供了 5 种通知类型:

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

环境准备

  • pom.xml 添加 Spring 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
    </dependencies>
    
  • 添加 BookDao 和 BookDaoImpl 类

    public interface BookDao {
        public void update();
        public int select();
    }
    
    @Repository
    public class BookDaoImpl implements BookDao {
        public void update(){
            System.out.println("book dao update is running ...");
        }
        public int select() {
            System.out.println("book dao select is running ...");
            return 100;
        }
    }
    
  • 创建 Spring 的配置类

    @Configuration
    @ComponentScan("com.qst")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  • 创建通知类

    @Component
    @Aspect
    public class MyAdvice {
        // @Pointcut("execution(void com.qst.dao.BookDao.update())")
        // private void pt(){}
    }
    
    
  • 编写 App 运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            bookDao.update();
        }
    }
    

通知类型的使用

前置通知 - @Before

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.qst.dao.BookDao.update())")
    private void pt(){}
    
    @Before("pt()")
    //此处也可以写成 @Before("MyAdvice.pt()"),但一般pt()都是在通知类种,所以不建议这样写
    
    public void before() {
        System.out.println("before advice ...");
    }
}

后置通知 - @After

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.qst.dao.BookDao.update())")
    private void pt(){}
    
    @After("pt()")
    
    public void after() {
        System.out.println("after advice ...");
    }
}
  • 后置通知是不管原始方法 update() 有没有抛出异常都会被执行

环绕通知 - @Around

  • 类似前后置的方法不完全

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.qst.dao.BookDao.update())")
        private void pt(){}
        
        @Around("pt()")
        public void around(){
            System.out.println("around before advice ...");
            System.out.println("around after advice ...");
        }
    }
    
    • 这样输出打印的话只有通知内容,却没有了原始方法的内容

      around before advice ...
      around after advice ...
      
    • 因为环绕通知需要在原始方法的前后进行增强 ( 否则怎么知道哪个是环绕前,哪个是环绕后 ),所以环绕通知就必须要能对原始操作进行调用

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(void com.qst.dao.BookDao.update())")
        private void pt(){}
        
        @Around("pt()")
        public void around(ProceedingJoinPoint pjp) throws Throwable{
            System.out.println("around before advice ...");
            //表示对原始操作的调用
            pjp.proceed();
            System.out.println("around after advice ...");
        }
    }
    
    • proceed() 为什么要抛出异常 —— 因为无法保证原始方法是否异常,所以在这里就统一都抛异常了

      • 只管加强,不管异常的解决
    • 运行结果

      around before advice ...
      book dao update is running ...
      around after advice ...
      
  • 但若是有返回值的方法如:select() 中 return 100;

    • 再做环绕通知就会报错 ( select 方法非 void,而是有返回值 int )
    • 而原始 void 就是返回 Null
  • 所以要修改 MyAdvice,对 BookDao 中的 select 方法添加环绕通知

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(int com.qst.dao.BookDao.select())")
        private void pt2(){}
        
        @Around("pt2()")
        public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("around before advice ...");
            //表示对原始操作的调用
            Object ret = pjp.proceed();
            
            //想再加操作的话就要强转
            //Integer ret = (Integer) pjp.proceed();
            System.out.println("around after advice ...");
            return ret;
        }
    }
    
    • 一是改 aroundSelect 方法的 void 为 Object
    • 二是 Object 接收返回值
    • 三是有返回值的话就在环绕后 return 出来
  • 环绕后通知注意事项

    • 环绕通知必须依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
    • 通知中如果未使用 ProceedingJoinPoint 对原始方法进行调用将跳过原始方法的执行
      • 没调用原始方法的话,就相当于把原始方法隔离了
      • 在这步可以编写权限的校验,符合才能执行
    • 对原始方法的调用可以不接收返回值,通知方法设置成 void 即可,如果接收返回值,最好设定为 Object 类型
    • 原始方法的返回值如果是 void 类型,通知方法的返回值类型可以设置成 void,也可以设置成 Object
    • 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理 Throwable 异常
      • 否则出了异常可能会被吞掉,反而显示不出来有异常了

返回后通知 - @AfterReturning

@Component
@Aspect
public class MyAdvice {    
    @Pointcut("execution(int com.qst.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterReturning("pt2()")
    public void afterReturning() {
        System.out.println("afterReturning advice ...");
    }
}
  • 返回后通知是需要在原始方法 select 正常执行后才会被执行,如果 select() 方法执行的过程中出现了异常,那么返回后通知是不会被执行
  • 后置通知是不管原始方法有没有抛出异常都会被执行

异常后通知 - @AfterThrowing

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(int com.qst.dao.BookDao.select())")
    private void pt2(){}
    
    @AfterThrowing ("pt2()")
    public void afterThrowing() {
        System.out.println("afterThrowing advice ...");
    }
}
  • 异常后通知是需要原始方法抛出异常,可以在 select() 方法中添加一行代码 int i = 1/0 即可
    • 如果没有抛异常,异常后通知将不会被执行

业务层接口执行效率

需求分析

  • 需求:任意业务层接口执行均可显示其执行效率 ( 执行时长 )
    • 开始执行方法之前记录一个时间
    • 执行方法
    • 执行完方法之后记录一个时间
    • 用后一个时间减去前一个时间的差值,就是我们需要的结果
  • 所以要在方法执行的前后添加业务,所以采用环绕通知,原始方法如果只执行一次,时间太快,两个时间差可能为 0,所以要执行万次来计算时间差

功能开发

  • 开启 SpringAOP 的注解功能

    • 在 Spring 的主配置文件 SpringConfig 类中添加注解 @EnableAspectJAutoProxy
  • 创建 AOP 的通知类 ProjectAdvice 并添加环绕通知

    @Component
    @Aspect
    public class ProjectAdvice {
        //配置业务层的所有方法
        @Pointcut("execution(* com.itheima.service.*Service.*(..))")
        private void servicePt(){}
        //@Around("ProjectAdvice.servicePt()") 可以简写为下面的方式
        @Around("servicePt()")
        // 环绕改返回值类型
        public void runSpeed(ProceedingJoinPoint pjp){
            
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000; i++) {
               pjp.proceed();
            }
            long end = System.currentTimeMillis();
            System.out.println("业务层接口万次执行时间: "+(end-start)+"ms");
        } 
    }
    
  • 优化通知类,想办法区分是哪个接口的哪个方法的具体执行时间

    //......
        public void runSpeed(ProceedingJoinPoint pjp){
            //获取执行签名信息
            Signature signature = pjp.getSignature();
            //通过签名获取执行操作名称(接口名)
            String className = signature.getDeclaringTypeName();
            //通过签名获取执行操作名称(方法名)
            String methodName = signature.getName();
            
            long start = System.currentTimeMillis();
            for (int i = 0; i < 10000; i++) {
               pjp.proceed();
            }
            long end = System.currentTimeMillis();
            System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
            // 这样就是具体显示哪个接口下的哪个方法的具体执行时间
        } 
    }
    
    • 当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程 ( 这里只是测试了业务层的 )
    • 这块只是通过该案例把 AOP 的使用进行了学习,具体的实际值是有很多因素共同决定的

AOP 通知获取数据

  • 先前介绍通知类型的时候总共讲了五种,那么对于这五种类型却不是都有参数,返回值和异常的
  • 获取切入点方法的参数,所有的通知类型都可以获取参数
    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究
    • 返回后通知 ( @AfterReturning )
    • 环绕通知 ( @Around )
  • 获取切入点方法运行异常信息,前置和返回后通知是不会有,后置通知可有可无,所以不做研究
    • 抛出异常后通知 ( @AfterThrowing )
    • 环绕通知 ( @Around )

环境准备

  • 添加 Spring 依赖

    <dependencies>
        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-context</artifactId>
          <version>5.2.10.RELEASE</version>
        </dependency>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjweaver</artifactId>
          <version>1.9.4</version>
        </dependency>
      </dependencies>
    
  • 添加 BookDao 和 BookDaoImpl 类

    public interface BookDao {
        public String findName(int id);
    }
    @Repository
    public class BookDaoImpl implements BookDao {
    
        public String findName(int id) {
            System.out.println("id:"+id);
            return "qst";
        }
    }
    
  • 创建 Spring 的配置类

    @Configuration
    @ComponentScan("com.qst")
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    
  • 编写通知类

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
        private void pt(){}
    
        @Before("pt()")
        public void before() {
            System.out.println("before advice ..." );
        }
    
        @After("pt()")
        public void after() {
            System.out.println("after advice ...");
        }
    
        @Around("pt()")
        public Object around() throws Throwable{
            Object ret = pjp.proceed();
            return ret;
        }
        @AfterReturning("pt()")
        public void afterReturning() {
            System.out.println("afterReturning advice ...");
        }
    
        @AfterThrowing("pt()")
        public void afterThrowing() {
            System.out.println("afterThrowing advice ...");
        }
    }
    
  • 编写 App 运行类

    public class App {
        public static void main(String[] args) {
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
            BookDao bookDao = ctx.getBean(BookDao.class);
            String name = bookDao.findName(100);
            System.out.println(name);
        }
    }
    

获取参数

非环绕通知获取方式

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint jp) 
        Object[] args = jp.getArgs(); // 对象数组存储
        System.out.println(Arrays.toString(args)); // 输出此对象内容参数,运维运行类执行的是查100,则输出时就为 [100]
        System.out.println("before advice ..." );
    }
	//......
}
  • 方法的参数只有一个,但获取的是一个数组
    • 因为参数的个数是不固定的,所以使用数组更通配些

环绕通知获取方式

  • ProceedingJoinpoint 继承了 Joinpoint,所以也是可以 pjp.getArgs() 的方式进行获取

  • 但 pjp.proceed() 方法是有两个构造方法,所以可以把数组放入

    • 但实际上数组 args 放与不放运行都一样,但若是加上、指定数组的话,就可以在环绕通知里,数组获取后,方法执行前对 args 数组进行修改与更正原始方法的参数
      • 无参的方法会在调用的过程中自动传入参数
    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
        private void pt(){}
    
        @Around("pt()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable{
            Object[] args = pjp.getArgs();
            System.out.println(Arrays.toString(args));
            args[0] = 666;
            Object ret = pjp.proceed(args);
            return ret;
        }
    	//......输出后[]内没变,但是return的ret里原来的100变成了666
    }
    
    • 有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤,避免由于参数的问题导致程序无法正确运行,保证代码的健壮性

返回值获取

  • 对于返回值,只有返回后 AfterReturing 和环绕 Around 这两个通知类型可以获取

环绕通知获取返回值

  • 似上获取参数

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
        private void pt(){}
    
        @Around("pt()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable{
            Object[] args = pjp.getArgs();
            System.out.println(Arrays.toString(args));
            args[0] = 666;
            Object ret = pjp.proceed(args);
            return ret;
        }
    	//......
    }
    

返回后通知获取返回值

  • 需要编写注解的属性

    @Component
    @Aspect
    public class MyAdvice {
        @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
        private void pt(){}
    
        @AfterReturning(value = "pt()",returning = "ret")
        public void afterReturning(Object ret) {
            System.out.println("afterReturning advice ..."+ret);
        }
    	//......
    }
    
    • returning 要与 参数列表中的 ret 匹配
    • 如果参数列表里想要加上 JoinPoint 参数,就必须放在第一位,如: public void afterReturning(JoinPoint jp,Object ret),两者位置反了就会报错

获取异常 ( 了解 )

  • 对于获取抛出的异常,只有抛出异常后 AfterThrowing 和环绕 Around 这两个通知类型可以获取

环绕通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
    private void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        try{
            ret = pjp.proceed(args);
        }catch(Throwable throwable){
            t.printStackTrace();
        }
        return ret;
    }
	//......
}
  • 在 catch 方法中就可以获取到异常,至于获取到异常以后该如何处理,就和业务需求有关了

抛出异常后通知获取异常

@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(* com.qst.dao.BookDao.findName(..))")
    private void pt(){}

    @AfterThrowing(value = "pt()",throwing = "t")
    public void afterThrowing(Throwable t) {
        System.out.println("afterThrowing advice ..."+t);
    }
	//......
}
  • 此处注解属性 throwing 指向值必须和下面形参名一致

  • 让原始方法抛出异常的方式进行测试上述操作

    @Repository
    public class BookDaoImpl implements BookDao {
    
        public String findName(int id,String password) {
            System.out.println("id:"+id);
            if(true){
                throw new NullPointerException();
            }
            return "qst";
        }
    }
    

实例

  • 在百度网盘提供网址和提取码时,如果在复制时提取码后面多了空格的话,不处理就比对是错误的,所以后端内部就将提取码前后的空格给 trim() 去掉了再进行匹配
  • 这里的格式处理就是通过 AOP 环绕通知完成的操作
    • 在业务方法执行之前对所有的输入参数进行格式处理 —— trim()
    • 使用处理后的参数调用原始方法 —— 环绕通知中存在对原始方法的调用

代码编写

  1. 开启 SpringAOP 的注解功能
@Configuration
@ComponentScan("com.qst")
@EnableAspectJAutoProxy
public class SpringConfig {
}
  1. 编写通知类
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.qst.service.*Service.*(*,*))")
    private void servicePt(){}
    
}
  1. 添加环绕通知
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.qst.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        Object ret = pjp.proceed();
        return ret;
    }
    
}
  1. 完成核心业务,在上述操作的基础上处理参数中的空格
@Component
@Aspect
public class DataAdvice {
    @Pointcut("execution(boolean com.itheima.service.*Service.*(*,*))")
    private void servicePt(){}
    
    @Around("DataAdvice.servicePt()")
    // @Around("servicePt()")这两种写法都对
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        //获取原始方法的参数
        Object[] args = pjp.getArgs();
        // foreach循环不能更改数组内容,此处需用for循环
        for (int i = 0; i < args.length; i++) {
            //判断参数是不是字符串
            if(args[i].getClass().equals(String.class)){
                args[i] = args[i].toString().trim();
            }
        }
        //将修改后的参数传入到原始方法的执行中
        Object ret = pjp.proceed(args);
        return ret;
    }
    
}
  1. 运行程序

不管密码 root 前后是否加空格,最终控制台打印的都是 true

  1. 优化测试

    • 在 dao 层实现类中可以打印出密码长度到控制器,可以发现长度是经过 trim 格式后没有空格的密码长度
    @Repository
    public class ResourcesDaoImpl implements ResourcesDao {
        public boolean readResources(String url, String password) {
            System.out.println(password.length());
            //模拟校验,比对
            return password.equals("root");
        }
    }
    

AOP 总结

  • 作用是在不惊动原始代码的基础上进行功能的增强

  • 核心概念

    • 代理 ( Proxy ):SpringAOP 的核心本质是采用代理模式实现的
    • 连接点 ( JoinPoint ):在 SpringAOP 中,理解为任意方法的执行
    • 切入点 ( Pointcut ):匹配连接点的式子,也是具有共性功能的方法描述
      • 切入点表达式
    • 通知 ( Advice ):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
    • 切面 ( Aspect ):描述通知与切入点的对应关系,两者的结合
    • 目标对象 ( Target ):被代理的原始对象成为目标对象
  • 环绕通知

    • 环绕通知依赖形参 ProceedingJoinPoint 才能实现对原始方法的调用
    • 环绕通知可以隔离原始方法的调用执行
    • 环绕通知返回值设置为 Object 类型
    • 环绕通知中可以对原始方法调用过程中出现的异常进行处理
  • 获取切入点方法的参数,所有的通知类型都可以获取参数

    • JoinPoint:适用于前置、后置、返回后、抛出异常后通知
    • ProceedingJoinPoint:适用于环绕通知
  • 获取切入点方法返回值,前置和抛出异常后通知是没有返回值,后置通知可有可无,所以不做研究

    • 返回后通知
    • 环绕通知

AOP 事务管理

Spring 事务简介

相关概念介绍

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败

  • Spring 事务作用:在数据层或业务层保障一系列的数据库操作同时成功或者失败

  • 业务层也需要处理事务

    • 转账业务会有两次数据层的调用,一次是加钱一次是减钱

    • 把事务放在数据层,加钱和减钱就有两个事务

    • 没办法保证加钱和减钱同时成功或者同时失败

    • 这个时候就需要将事务放在业务层进行处理

  • Spring 为了管理事务,提供了一个平台事务管理器 PlatformTransactionManager

    • 内含 commit 方法用来提交事务,rollback 方法用来回滚事务
  • PlatformTransactionManager 只是一个接口,Spring 还为其提供了一个具体的实现:DataSourceTransactionManager

    • 从名称上可以看出,我们只需要给它一个 DataSource 对象,它就可以帮你去在业务层管理事务。其内部采用的是 JDBC 的事务。所以说如果你持久层采用的是 JDBC 相关的技术,就可以采用这个事务管理器来管理你的事务。而 Mybatis 内部采用的就是 JDBC 的事务 ( 后期 Spring 整合 Mybatis 也采用此 DataSourceTransactionManager 事务管理器 )

转账案例 - 需求分析

  • 需求:实现任意两个账户间转账操作 ( 即 A 账户减钱,B 账户加钱 )

  • 为了实现上述的业务需求,要按照下面步骤来实现:

    • 数据层提供基础操作,指定账户减钱 ( outMoney ),指定账户加钱 ( inMoney )

    • 业务层提供转账操作 ( transfer ),调用减钱与加钱的操作

    • 提供两个账号和操作金额执行转账操作

    • 基于 Spring 整合 MyBatis 环境搭建上述操作

环境搭建

  1. 准备数据库表
create database spring_db character set utf8;
use spring_db;
create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
);
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);
  1. 创建项目导入 jar 包,添加相关依赖
<dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.6</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.23</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

  </dependencies>
  1. 根据表创建模型类
public class Account implements Serializable {

    private Integer id;
    private String name;
    private Double money;
	//setter...getter...toString
}
  1. 创建 Dao 数据层接口
public interface AccountDao {

    @Update("update tbl_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String name, @Param("money") Double money);

    @Update("update tbl_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String name, @Param("money") Double money);
}
  1. 创建 Service 业务层接口和实现类
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    public void transfer(String out,String in ,Double money) ;
}

@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    
    public void transfer(String out,String in ,Double money) {
        accountDao.outMoney(out,money);
        accountDao.inMoney(in,money);
    }

}
  1. 添加 jdbc.properties 文件

  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;
    }
}
  1. 创建 MybatisConfig 配置类
public class MybatisConfig {

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        // 映射的bean类
        ssfb.setTypeAliasesPackage("com.qst.domain");
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.qst.dao");
        return msc;
    }
}
  1. 创建 SpringConfig 配置类
@Configuration
@ComponentScan("com.qst")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
public class SpringConfig {
}

  1. 编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() throws IOException {
        accountService.transfer("Tom","Jerry",100D);
    }
}

事务管理

  • 正常情况下,上述环境,运行单元测试类,会执行转账操作,Tom 的账户会减少 100,Jerr 的账户会加 100。

  • 但是如果在转账的过程中出现了异常,如:

@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 账户为 800 而 Jerry 还是 1100
  • 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
    • 当程序出问题后,我们需要让事务进行回滚,而且这个事务应该是加在业务层上,而 Spring 的事务管理就是用来解决这类问题的。

Spring 事务管理具体的实现步骤为:

  1. 在需要被事务管理的方法上添加注解
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    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 可以写在接口类上、接口方法上、实现类上和实现类方法上

    • 写在接口类上,该接口的所有实现类的所有方法都会有事务

    • 写在接口方法上,该接口的所有实现类的该方法都会有事务

    • 写在实现类上,该类中的所有方法都会有事务

    • 写在实现类方法上,该方法上有事务

  • 通常写在业务层接口上而不是实现类中 —— 降低耦合

  1. 在 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 transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }
}
  • 注意:事务管理器要根据使用技术进行选择,Mybatis 框架使用的是 JDBC 事务,可以直接使用 PlatformTransactionManager、DataSourceTransactionManager ( spring 提供 )
  1. 开启事务注解
  • 在 SpringConfig 的配置类中开启
@Configuration
@ComponentScan("com.qst")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})

//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
  1. 运行测试类
  • 会发现在转换的业务出现错误后,事务就可以控制回顾,保证数据的正确性

@EnableTransactionManagement:设置当前 Spring 环境中开启注解式事务支持

@Transactional:为当前业务层方法添加事务 ( 如果设置在类或接口上方则类或接口中所有方法均添加事务 )

Spring 事务角色

这节中我们重点要理解两个概念,分别是事务管理员和事务协调员

  1. 未开启 Spring 事务之前:
  • AccountDao 的 outMoney() 因为是修改操作,会开启一个事务 T1
  • AccountDao 的 inMoney() 因为是修改操作,会开启一个事务 T2
  • AccountService 的 transfer() 没有事务
    • 运行过程中如果没有抛出异常,则 T1 和 T2 都正常提交,数据正确
    • 如果在两个方法中间抛出异常,T1 因为执行成功提交事务,T2 因为抛异常不会被执行,T1 不会回滚
    • 就会导致数据出现错误
  1. 开启 Spring 的事务管理后
  • transfer() 上添加了 @Transactional 注解,在该方法上就会有一个事务 T
  • AccountDao 的 outMoney() 的事务 T1 加入到 transfer() 的事务 T 中
  • AccountDao 的 inMoney() 的事务 T2 加入到 transfer() 的事务 T 中
  • 这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。
  1. 上述例子可得如下概念:
  • 事务管理员:发起事务方,在 Spring 中通常指代业务层开启事务的方法

    • transfer() 的事务 T
  • 事务协调员:加入事务方,在 Spring 中通常指代数据层方法,也可以是业务层方法

    • outMoney() 的事务 T1 和 inMoney() 的事务 T2
  • 注意:目前的事务管理是基于 DataSourceTransactionManagerSqlSessionFactoryBean 使用的是同一个数据源

    • 即 JdbcConfig 与 MybatisConfig 两个配置文件用的是同一个数据源,所以才能统一管理

Spring 事务属性

事务配置

  • 注意:并不是所有的异常都会回滚
    • Spring 的事务只会对 Error 异常和 RuntimeException 异常及其子类进行事务回顾,其他的异常类型是不会回滚的,例如 IOException 不符合上述条件所以就不会回滚
  • 了解 @Transactional 注解的各属性:

1630250069844

上面这些属性都可以在 @Transactional 注解的参数上进行设置。

  • readOnly:true 只读事务,false 读写事务,增删改要设为 false,查询设为 true

  • timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1 表示不设置超时时间。

  • rollbackFor:当出现指定异常进行事务回滚 ( 就是因为有的异常不会回滚 )

    • 如下使用 rollbackFor 属性来设置出现 IOException 异常回滚

      public interface AccountService {
          @Transactional(rollbackFor = {IOException.class})
          public void transfer(String out,String in ,Double money) throws IOException;
      }
      
      // .....................
      
      @Service
      public class AccountServiceImpl implements AccountService {
      
          @Autowired
          private AccountDao accountDao;
      	 
          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);
          }
      
      }
      
  • noRollbackFor:当出现指定异常不进行事务回滚

    • rollbackForClassName 等同于 rollbackFor,只不过属性为异常的类全名字符串
  • noRollbackForClassName 等同于 noRollbackFor,只不过属性为异常的类全名字符串

  • isolation 设置事务的隔离级别

    • DEFAULT:默认隔离级别, 会采用数据库的隔离级别
    • READ_UNCOMMITTED:读未提交
    • READ_COMMITTED:读已提交
    • REPEATABLE_READ:重复读取
    • SERIALIZABLE:串行化
  • 还有最后一个事务的传播行为,为了讲解该属性的设置,我们需要完成下面的案例

转账业务追加日志案例

需求分析

  • 在前面的转案例的基础上添加新的需求,完成转账后记录日志

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

  • 需求微缩:A 账户减钱,B 账户加钱,数据库记录日志

  • 分析:

    • 基于转账操作案例添加日志模块,实现数据库中记录日志
    • 业务层转账操作 ( transfer ),调用减钱、加钱与记录日志功能
  • 预期效果为:无论转账操作是否成功,均进行转账操作的日志留痕

环境准备

该环境是基于转账环境来完成的,在其基础上,我们继续往下写

  1. 创建日志表
create table tbl_log(
   id int primary key auto_increment,
   info varchar(255),
   createDate datetime
)
  1. 添加 LogDao 接口
public interface LogDao {
    @Insert("insert into tbl_log (info,createDate) values(#{info},now())")
    void log(String info);
}

  1. 添加 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);
    }
}
  1. 在转账的业务中添加记录日志
public interface AccountService {
    /**
     * 转账操作
     * @param out 传出方
     * @param in 转入方
     * @param money 金额
     */
    //配置当前接口方法具有事务
    public void transfer(String out,String in ,Double money)throws IOException ;
}
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    @Autowired
    private LogService logService;
    
	@Transactional
    public void transfer(String out,String in ,Double money) {
        try{
            accountDao.outMoney(out,money);
            // int i =1/0; 
            accountDao.inMoney(in,money);
        }finally {
            // 不常使用,这里是为了百分百会执行此log方法
            logService.log(out,in,money);
        }
    }

}
  1. 运行程序
  • 当程序正常运行,tbl_account 表中转账成功,tbl_log 表中日志记录成功

  • 当转账业务之间出现异常 ( int i =1/0 ),转账失败,tbl_account 成功回滚,但是 tbl_log 表未添加数据

  • 这个结果和我们想要的不一样:

    • 失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
    • 期望效果:无论转账操作是否成功,日志必须保留

事务传播行为

  • 分析:

    • log 方法、inMoney 方法和 outMoney 方法都属于增删改,分别有事务 T1,T2,T3;transfer 因为加了 @Transactional 注解,也开启了事务 T
    • 之前所讲 Spring 事务会把 T1,T2,T3 都加入到事务T中
    • 所以当转账失败后,所有的事务都回滚,导致日志没有记录下来
  • 这里就需要让 log 方法单独是一个事务

    • 要想解决这个问题,就需要用到事务传播行为,所谓的事务传播行为指的是:
      • 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
      • 具体如何解决,就需要用到之前我们没有说的 propagation 属性
  • 修改 logService 改变事务的传播行为
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
    
	//propagation设置事务属性:传播行为设置为当前操作需要新事务,不受 T 事务干扰
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    
    public void log(String out,String in,Double money ) {
        logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
    }
}
  • 运行后,就能实现我们想要的结果,不管转账是否成功,都会记录日志

  • 事务传播行为的可选值

    • 常为 REQUIRED 默认
    • 而 REQUIRES_NEW 就是不管事务管理员如何,事务协调员都会新建事务

    1630254257628

    • 对于我们开发实际中使用的话,因为默认值需要事务是常态的,根据开发过程选择其他的就可以了,例如案例中需要新事务就需要手工配置。其实入账和出账操作上也有事务,采用的就是默认值
posted @ 2023-09-16 19:16  朱呀朱~  阅读(7)  评论(0编辑  收藏  举报