Spring框架学习笔记-From b站狂神-Part2


Main References

  1. 暑假集训授课同学的帖子(下)
  2. Bilibili 狂神讲Spring
  3. Spring Framework官网
  4. MVN Repository :maven项目架构的各种依赖
  5. MyBatis-Spring中文官网

PS:笔记部分大致是按照狂神的进度来记录的,有时候会插入一些从网上贴子和官网的内容

gitee:https://gitee.com/torchW/spring-study

9. 代理模式

为什么要学习代理模式 ?因为这就是SpringAOP的底层!【SpringAOP 和 SpringMVC】

当一个对象不能直接使用,可以在客户端和目标对象之间创建一个中介,这个中介就是代理

代理模式的分类:

  • 静态代理
  • 动态代理

代理模式的作用:

  1. 控制访问:在代理中,控制是否可以调用目标对象的方法
  2. 功能增强:可以在完成目标对象的调用时,附加一些额外的功能,这些额外的功能叫做功能增强

9.1 静态代理

9.1.1 示例1-租房

角色分析:

在这里插入图片描述

  • 抽象角色:一般会使用接口或者抽象类来解决,他们会共同去完成的事情,上例中就是“租房”
  • 真实角色:被代理的角色
  • 代理角色(核心):代理真实角色,代理真实角色后,我们一般会做一些附属操作
  • 客户:访问代理对象的人

代码步骤:

  1. 接口

    public interface Rent {//租房
        public void rent();
    }
    
    
  2. 真实角色

    public class Host implements Rent{//房东
        @Override
        public void rent() {
            System.out.println("房东要出租房子!");
        }
    }
    
    
  3. 代理角色

    public class Proxy implements Rent{
        private Host host;
        public Proxy() {
        }
        public Proxy(Host host) {
            this.host = host;
        }
        @Override
        public void rent() {
            seeHouse();
            host.rent();
            hetong();
            fare();
        }
        //看房
        public void seeHouse(){System.out.println("中介带你看房");}
        //签合同
        public void hetong(){System.out.println("签租凭合同");}
        //收中介费
        public void fare(){System.out.println("收中介费");}
    }
    
    
  4. 客户端访问代理角色

    public class Client {
        public static void main(String[] args) {
            //房东要租房子
            Host host = new Host();
            //代理,中介帮房东租房子,但是代理角色,一般会有一些附属操作
            Proxy proxy = new Proxy(host);
            //你不用面对房东,直接找中介租房即可
            proxy.rent();
        }
    }
    
    
代理模式优点:
  • 可以使真实角色的操作更加纯粹,不用关注公共业务!
  • 公共业务交给代理角色,实现业务分工!
  • 公共业务发生扩展的时候,方便集中管理!
代理模式缺点:
  • 一个真实角色就会产生一个代理角色;代码量会翻一倍,使得开发效率变低

9.1.2 示例2-CRUD-加深理解

场景描述:原来的代码是,客户直接访问真实对象;但此时新需求是调用真实对象时添加日志信息。

解决方案:通过添加真实对象的代理,完成日志信息的添加。

  1. 接口

    public interface UserService {
        public void add();
        public void delete();
        public void update();
        public void query();
    }
    
    
  2. 真实对象

    public class UserServiceImpl implements UserService{
        @Override
        public void add() {System.out.println("增加了一个用户");}
        @Override
        public void delete() {System.out.println("删除了一个用户");}
        @Override
        public void update() {System.out.println("修改了一个用户");}
        @Override
        public void query() {System.out.println("查询了一个用户");}
    }
    
    
  3. 代理

    public class UserServiceProxy implements UserService{
        private UserServiceImpl userService;
    
        public void setUserService(UserServiceImpl userService) {this.userService = userService;}
        @Override
        public void add() {
            log("add");
            userService.add();
        }
        @Override
        public void delete() {
            log("delete");
            userService.delete();
        }
        @Override
        public void update() {
            log("update");
            userService.update();
        }
        @Override
        public void query() {
            log("query");
            userService.query();
        }
        private void log(String msg){//日志方法
            System.out.println("[Debug] 使用了" + msg + "方法");
        }
    }
    
    
  4. 客户端访问代理

    public class Client {
        public static void main(String[] args) {
            UserServiceImpl userService = new UserServiceImpl();
            UserServiceProxy proxy = new UserServiceProxy();
            proxy.setUserService(userService);
            proxy.query();
        }
    }
    
    
选择代理模式的原因:
  1. 改动原有的业务代码,在公司中是大忌!
  2. 新增一个类的试错成本小,而且它只需要完成新增的业务,职责单一。

aop的实现机制

按照正常的开发,我们从dao 到 前端 的纵向开发。假设在软件发布后,用户提出了新的需求,我们就可以采用横向开发,即通过代理的方式增加业务,不需要修改原来的代码,这就是 aop 的实现机制!

在这里插入图片描述

9.2 动态代理

  • 动态代理和静态代理角色一样

  • 动态代理的代理类是动态生成的,不是我们直接写好的!

  • 动态代理分为两大类:基于接口的动态代理,基于类的动态代理

    • 基于接口:JDK 动态代理(最经典,原生的)【我们在这里使用的】

    • 基于类:CGLib

      主要是对指定的类生成一个子类,覆盖其中的方法进行增强,但是因为采用的是继承,所以该类或方法最好不要声明为final,对于final类或方法,是无法继承的。

    • java字节码实现(用的比较多):Javasist

    JDK动态代理与CGLib动态代理的区别:

    1. JDK动态代理只能为接口创建代理类,而CGLib动态代理而可以直接为类创建代理类。
    2. JDK动态代理创建代理类的速度要比CGLib动态代理创建代理类的速度要快。
    3. CGLib动态代理创建代理类的性能要比JDK动态代理创建代理类的性能要高。
  • Spring如何选择是用JDK还是cglib?

    1. 当bean实现接口时,会用JDK代理模式

    2. 当bean没有实现接口,会用cglib实现

    3. 可以强制使用cglib ,当使用注解实现AOP的时候,在bean配置中添加此标签,见10.3.3

      <aop:aspectj-autoproxy  poxy-target-class="true"/>
      
      
      

9.2.1 JDK 动态代理

其中我们需要了解两个类:Proxy 代理 和 InvocationHandler 调用处理程序

详细整理官网 关于这两个类的介绍

1. Proxy

java.lang.reflect Class Proxy

Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。

2. InvocationHandler

java.lang.reflect Interface InvocationHandler

InvocationHandler 是由代理实例的 调用处理程序实现 的接口。每个代理实例都有一个关联的调用处理程序。当在代理实例上调用方法时,方法调用被编码并分流到其调用处理程序的invoke方法。

3. 示例

Rent 接口:

public interface Rent {//租房
    public String rent();
}

Host 房东:

public class Host implements Rent {
    @Override
    public String rent() {
        System.out.println("房东要出租房子!");
        return "rent";
    }
}

AddRequire 接口,其中的定义给原始方法的扩展方法:

public interface AddRequire {
    default void addRequireBeforeOriginalMethod(Object... objects){
        System.out.println("在原方法前添加新需求");
    }
    default void addRequireAfterOriginalMethod(Object... objects){
        System.out.println("在原方法后添加新需求");
    }
    default void addRequireBeforeOriginalMethod(){
        System.out.println("在原方法前添加新需求");
    }
    default void addRequireAfterOriginalMethod(){
        System.out.println("在原方法后添加新需求");
    }
}

ProxyInvocationHandler 自定义调用程序,用这个类自动生成代理类:

public class ProxyInvocationHandler implements InvocationHandler {
    private Object proxyInterface;//被代理的接口
    private AddRequire addRequire;//完成新需求的方法
    
    public void setAddRequire(AddRequire addRequire){ 
        this.addRequire = addRequire;
    }
    public void setProxyInterface(Object proxyInterface) {
        this.proxyInterface = proxyInterface;
    }

    //生成得到代理类
    public Object getProxy(){
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), proxyInterface.getClass().getInterfaces(),this);
    }

    //处理代理实例,并返回结果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //动态代理的本质,就是使用反射机制实现!
        addRequire.addRequireBeforeOriginalMethod();
        Object result = method.invoke(proxyInterface,args);
        addRequire.addRequireAfterOriginalMethod();
        return result;
    }
}

Client 客户端:

public class Client {
    public static void main(String[] args) {
        //真实角色
        Host host = new Host();
        //代理角色现在还没有,我们要通过自定义的 调用处理程序 来获得代理类
        ProxyInvocationHandler proxyInvocationHandler = new ProxyInvocationHandler();
        //设置要代理的对象
        proxyInvocationHandler.setProxyInterface(host);
        //设置添加的新需求接口的实现
        proxyInvocationHandler.setProxyInterface(new AddRequire(){});
        //动态生成代理类
        Rent proxy = (Rent) proxyInvocationHandler.getProxy();
        proxy.rent();
    }
}

我这里算是对JDK动态代理的简单的抽象应用,将拓展的方法抽象成接口,让Client自己实现再通过setter注入。当遇到具体应用场景的时候,我们可以调整 自定义调用处理函数中的 invoke方法,或者调整新增需求的接口(直接放在 自定义调用处理函数中也行)。在具体应用场景下会发现更多的使用方法。

动态代理,其实代理的就是接口(真实对象实现的接口,或者可以说是业务),我这里还加了一个接口是新需求扩展的接口,不要搞混了。

4. 动态处理的好处:

  • 可以使真实角色的操作更加纯粹,不用关注公共业务!

  • 公共业务交给代理角色,实现业务分工!

  • 公共业务发生扩展的时候,方便集中管理!

  • 一个动态代理类代理的是一个接口,一般就是对应的一类业务(即同一个接口)

  • 一个动态代理类可以代理多个类,只要是实现了同一个接口即可(每次代理其他类(实现用以接口)的时候,重新set接口给 自定义的调用处理程序,通过get代理实例的方法获得新的代理)

10.AOP

10.1 什么是AOP

AOP (Aspect Oriented Programming)意为:面向切面编程,通过预编译方式运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

10.2 AOP在Spring中的作用

  • 提供声明式事务
  • 允许用户自定义切面

需要了解的名词:

  • 横切关注点:跨越应用程序多个模块的方法或功能。也就是:与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志,安全,缓存,事务等等…(添加的扩展功能)

  • 切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。(日志类,检测类…)

  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。(就是日志类里的方法)

  • 目标(Target):被通知对象。(接口或者方法)

  • 代理(Proxy):向目标对象应用通知之后创建的对象。(生成的代理类)

  • 切入点(PointCut):切面通知执行的“地点"的定义(切入点、连接点:说白了就是在哪里执行)

  • 连接点 (JointPoint):与切入点匹配的执行点。

在这里插入图片描述

SpringAOP中,通过Advice定义横切逻辑,Spring中支持5种类型的Advice:

通知类型连接点实现接口
前置通知方法前org.springframework.aop.MethodeBeforeAdvice
后置通知方法后org.springframework.aop.AfterReturningAdvice
环绕通知方法前后org.aopalliance.intercept.MethodInterceptor
异常抛出通知方法抛出异常org.springframework.aop.ThrowsAdvice
引介通知类中增加新的方法org.springframework.aop.IntroductionInterceptor

即aop在不修改原有代码的基础上,增加新的方法

10.3 使用Spring实现Aop

重点:使用AOP织入,需要导入一个依赖包 maven的aspectjweaver依赖代码

<!-- https: / /mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

10.3.1 实现一:使用String的API接口**【主要 Spring API接口实现】**

  1. 首先编写我们的业务接口和实现类

    public interface UserService {
       public void add();
       public void delete();
       public void update();
       public void search();
    
    }
    
    public class UserServiceImpl implements UserService{
       @Override
       public void add() {
           System.out.println("增加用户");
      }
       @Override
       public void delete() {
           System.out.println("删除用户");
      }
       @Override
       public void update() {
           System.out.println("更新用户");
      }
       @Override
       public void search() {
           System.out.println("查询用户");
      }
    }
    
  2. 接下来写增强类,即我们动态代理的代理类

    public class Log implements MethodBeforeAdvice {
       //method : 要执行的目标对象的方法
       //args : 被调用的方法的参数
       //target : 目标对象
       @Override
       public void before(Method method, Object[] args, Object target) throws Throwable {
           System.out.println( target.getClass().getName() + "的" + method.getName() + "方法被执行了");
      }
    }
    
    public class AfterLog implements AfterReturningAdvice {
       //returnValue 返回值
       //method被调用的方法
       //args 被调用的方法的对象的参数
       //target 被调用的目标对象
       @Override
       public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
           System.out.println("执行了" + target.getClass().getName()
           +"的"+method.getName()+"方法,"
           +"返回值:"+returnValue);
      }
    }
    

    MethodBeforeAdvice 和 AfterReturningAdvice 是 SpringFramework 中的类

  3. 最后在spring的文件中进行注册,并实现aop切入实现

    注意导入约束,在创建aop标签的时候会根据IDEA提示可以自动添加约束。

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:aop="http://www.springframework.org/schema/aop"
          xsi:schemaLocation="<http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd>"
    
       <!--注册bean-->
       <bean id="userService" class="com.torch.service.UserServiceImpl"/>
       <bean id="log" class="com.torch.log.Log"/>
       <bean id="afterLog" class="com.torch.log.AfterLog"/>
    
       <!--aop的配置-->
       <aop:config>
           <!--切入点,就是我们要在哪里执行我们Spring的方法-->
           <!--expression:表达式,execution(要执行的位置,怎么判断:* * * * * :(修饰词 返回值 类名 方法名 参数) )-->
           <!--  * com.torch.service.UserServiceImpl.*(..)   指UserServiceImpl 下的所有方法 , (..) 代表了方法可以有任意个参数 -->
           <aop:pointcut id="pointcut" expression="execution(* com.torch.service.UserServiceImpl.*(..))"/>
           <!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
           <!-- 这个意思就是把 Log 这个类,切入到 pointcut表达式的地方  -->
           <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
           <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
           <!--同时可以配置多个切入点,甚至固定切入点的表达式,固定方法-->
       </aop:config>
    </beans>
    

    可以在<aop:config >中添加多个切入点,但是一般使用时要区分开来,不要冲突。在执行环绕就可以选择不同的切入点。

  4. 测试

    public class MyTest {
       @Test
       public void test(){
           ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
           UserService userService = (UserService) context.getBean("userService");
           userService.search();
      }
    }
    

Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 。领域业务更纯粹 , 程序猿专注领域业务 , 其本质还是动态代理。

AspectJ切入点语法简单介绍

Reference: AspectJ切入点语法详解

类型匹配语法

  • 通配符

    • *:匹配任何数量字符
    • ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包,而在方法参数模式中匹配任何数量参数
    • +:匹配指定类型的子类型,仅能作为后缀放在类型模式的后边
  • 匹配类型 (XX? ,表示可选)

    注解?  类的全限定名字  
    
    • 注解:可选,类型上持有的注解,如@Deprecated
    • 类的全限定名:必填,可以是任何类全限定名
  • 匹配方法执行

    注解?  修饰符?  返回值类型  类型声明?  方法名  (参数列表)  异常列表?
    
    • **注解:**可选,方法上持有的注解,如@Deprecated;

    • **修饰符:**可选,如public、protected;

    • 返回值类型: 必填,可以是任何类型模式;“*”表示所有类型

    • **类型声明:**可选,可以是任何类型模式;

    • 方法名: 必填,可以使用“*”进行模式匹配

    • 参数列表: 必填() 表示方法没有任何参数;(..)表示匹配接受任意个参数的方法;

      (..,java.lang.String)表示匹配 java.lang.String类型的参数结束,且前边接受有任意个参数的方法;

      (java.lang.String,..) 表示匹配 java.lang.String类型的参数开始,且后边可以接受任意个参数的方法;

      (*,java.lang.String) 表示匹配 java.lang.String类型的参数结束,且前边有一个任意类型参数的方法;

    • **异常列表:**可选,以“throws 异常全限定名列表”声明,异常全限定名列表如有多个以“,”分割,如throws java.lang.IllegalArgumentException, java.lang.ArrayIndexOutOfBoundsException。

execution 的匹配语法就是参考匹配方法执行

10.3.2 实现二:自定义来实现AOP**【主要是切面自定义】**

目标业务类不变依旧是userServiceImpl

  1. 自定义一个切入类

    public class DiyPointcut {
       public void before(){
           System.out.println("---------方法执行前---------");
      }
       public void after(){
           System.out.println("---------方法执行后---------");
      }
    }
    
  2. 去spring配置文件中配置

    <!--注册bean-->
    <bean id="diy" class="com.torch.log.DiyPointCut"/>
    
    <!--aop的配置-->
    <aop:config>
        <!--自定义切面,ref 要引用的类-->
       <aop:aspect ref="diy">
            <!--切入点-->
           <aop:pointcut id="diyPonitcut" expression="execution(* com.torch.service.UserServiceImpl.*(..))"/>
           <!--通知-->
           <aop:before pointcut-ref="diyPonitcut" method="before"/>
           <aop:after pointcut-ref="diyPonitcut" method="after"/>
       </aop:aspect>
    </aop:config>
    
  3. 测试:

    public class MyTest {
       @Test
       public void test(){
           ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
           UserService userService = (UserService) context.getBean("userService");
           userService.add();
      }
    }
    
    

10.3.3 实现三:使用注解实现

  1. 编写一个注解实现的增强类,UserServiceImpl 见前面

    @Aspect  //标注这个类是一个切面
    public class AnnotationPointCut {
       @Before("execution(* com.torch.service.UserServiceImpl.*(..))")
       public void before(){
           System.out.println("###########方法执行前##########");
      }
    
       @After("execution(* com.torch.service.UserServiceImpl.*(..))")
       public void after(){
           System.out.println("###########方法执行后##########");
      }
    
       @Around("execution(* com.torch.service.UserServiceImpl.*(..))")
       public void around(ProceedingJoinPoint jp) throws Throwable {
           System.out.println("环绕前");
           System.out.println("signature:" + jp.getSignature());
           //执行目标方法proceed
           Object proceed = jp.proceed();
           System.out.println("环绕后");
           System.out.println("proceed:" + proceed);
      }
    }
    
  2. 在Spring配置文件中,注册bean,并增加支持注解的配置

    <!--第三种方式:注解实现-->
    <bean id="annotationPointCut" class="com.torch.diy.AnnotationPointCut"/>
    <aop:aspectj-autoproxy/>
    

    aop:aspectj-autoproxy 标签说明

    通过aop命名空间的 <aop:aspectj-autoproxy /> 声明,自动为spring容器中那些配置 @aspectJ 切面的bean创建代理,织入切面。

    当然,spring 在内部依旧采用 AnnotationAwareAspectJAutoProxyCreator 进行自动代理的创建工作,但具体实现的细节已经被 <aop:aspectj-autoproxy /> 隐藏起来了。

    <aop:aspectj-autoproxy /> 有一个 proxy-target-class 属性,默认为false,表示使用jdk动态代理织入增强,当配为<aop:aspectj-autoproxy poxy-target-class=“true”/>时,表示使用CGLib动态代理技术织入增强。

    不过即使 proxy-target-class 设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。

  3. 结果

    在这里插入图片描述

    原文链接

    • spring-aop5.2.6版本前

      <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aop</artifactId>
          <version>5.2.6.RELEASE</version>
      </dependency>
      

      执行顺序:

      Around(proceed之前)
      Before
      切入代码
      Around(proceed之后)
      After
      
    • spring-aop5.2.6版本后

      <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-aop</artifactId>
          <version>5.2.7.RELEASE</version>
      </dependency>
      

      执行顺序:

      Around(proceed之前)
      Before
      切入代码
      After
      Around(proceed之后)
      

      我在使用的是5.3.21,狂神说[2019-10-13]上课使用的是5.2.0

个人觉得还是比较喜欢第二种方式,不过还是看个人喜好。

11.整合MyBatis

11.1 需要的依赖导入:

  • junit
  • mybatis
  • mybatis-spring【new】:专门整合spring和mybatis的
  • mysql数据库
  • spring相关的(spring操作数据库的话,还需要spring-jdbc)
  • aop织入
<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>
        <!-- <https://mvnrepository.com/artifact/org.mybatis/mybatis-spring> -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.21</version>
        </dependency>
<!--     Spring操作数据库   -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.20</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
    </dependencies>

<!--配置Maven静态资源过滤问题!-->
   <build>
        <resources>
            <resource>
                <directory> src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

mybatis-spring 的版本会与 mybatis和spring framework 的版本有点关联,需要注意下: 见官网

在这里插入图片描述

11.2 回顾MyBatis

  1. 编写pojo实体类

    public class User {
       private int id;  //id
       private String name;   //姓名
       private String pwd;   //密码
       //setter getter toString 省略
    }
    
  2. 实现mybatis的配置文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
           PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
           "<http://mybatis.org/dtd/mybatis-3-config.dtd>">
    <configuration>
    
       <typeAliases>
           <package name="com.torch.pojo"/>
       </typeAliases>
    
       <environments default="development">
           <environment id="development">
               <transactionManager type="JDBC"/>
               <dataSource type="POOLED">
                   <property name="driver" value="com.mysql.jdbc.Driver"/>
                   <property name="url" value="jdbc:mysql://localhost:3306/spring-study?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
                   <property name="username" value="username"/>
                   <property name="password" value="password"/>
               </dataSource>
           </environment>
       </environments>
    
       <mappers>
           <mapper class="com.torch.mapper.UserMapper"/>
       </mappers>
    </configuration>
    
  3. UserDao接口编写

    public interface UserMapper {
       public List<User> selectUser();
    }
    
  4. 接口对应的Mapper映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
           PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           "<http://mybatis.org/dtd/mybatis-3-mapper.dtd>">
    <mapper namespace="com.torch.mapper.UserMapper">
    
       <select id="selectUser" resultType="User">
        select * from user;
       </select>
    
    </mapper>
    
  5. 测试类

    @Test
    public void selectUser() throws IOException {
    
       String resource = "mybatis-config.xml";
       InputStream inputStream = Resources.getResourceAsStream(resource);
       SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
       SqlSession sqlSession = sqlSessionFactory.openSession(true);//开启自动提交
       UserMapper mapper = sqlSession.getMapper(UserMapper.class);
       List<User> userList = mapper.selectUser();
       for (User user: userList){
           System.out.println(user);
      }
       sqlSession.close();
    }
    
    

11.3 MyBatis-Spring 整合

MyBatis-Spring官网 入门等详细资料可看

什么是MyBatis-Spring?

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。

11.3.1 整合实现一

接着11.2 回顾的代码 进行修改。

  1. 引入Spring配置文件beans.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            https://www.springframework.org/schema/beans/spring-beans.xsd">
        <!-- Add codes here. -->
    </beans>
    
  2. beans.xml 配置文件中 配置数据源替换mybaits的数据源(Spring的、C3P0、DBCP、druid)

    这里使用Spring的jdbc,就是11.1 导入的 spring-jdbc

    <!--配置数据源:数据源有非常多,可以使用第三方的,也可使用Spring的-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
       <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://localhost:3306/spring-study?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
       <property name="username" value="root"/>
       <property name="password" value="123456"/>
    </bean>
    

    配置完后,我们就不需要 mybatis-config配置中的 数据库环境

  3. beans.xml 配置文件中 配置SqlSessionFactory,关联MyBatis

    <!--配置SqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
       <property name="dataSource" ref="dataSource"/>
       <!--关联Mybatis-->
       <property name="configLocation" value="classpath:mybatis-config.xml"/>
       <property name="mapperLocations" value="classpath:com/torch/mapper/UserMapper.xml"/>
    </bean>
    

    配置完后,mybatis-config配置中的 就不需要了;

    还有测试类中的SqlSessionFactory可以通过Spring容器获取,不再需要Builder。

  4. beans.xml 配置文件中 注册sqlSessionTemplate,关联sqlSessionFactory;

    SqlSessionTemplate:在Spring中又很多XxxTemplate的类(模板),算是对原来SqlSession整合的封装,取了新的名称。

    SqlSessionTemplate 是 MyBatis-Spring 的核心。作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession。 SqlSessionTemplate 是线程安全的,可以被多个 DAO 或映射器所共享使用。

    <!--注册sqlSessionTemplate , 关联sqlSessionFactory-->
    <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
       <!--只能利用构造器注入-->
       <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
    

    注册完成后,可以不需要获取 SqlSessionFactory了,直接通过 Spring容器获取。

  5. 增加Mapper接口(or Dao接口)的实现类;私有化sqlSessionTemplate

    public class UserMapperImpl implements UserMapper {
       //sqlSessionTemplate 不用我们自己创建了,Spring来管理
       private SqlSessionTemplate sqlSessionTemplate;
        
       public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
           this.sqlSessionTemplate = sqlSessionTemplate;
      }
       public List<User> selectUser() {
           UserMapper mapper = sqlSessionTemplate.getMapper(UserMapper.class);
           return mapper.selectUser();
      }
    }
    
    
  6. beans.xml 配置文件中 注册Mapper接口(or Dao接口)的实现类

    <bean id="userMapper" class="com.torch.mapper.UserMapperImpl">
        <property name="sqlSessionTemplate" ref="sqlSessionTemplate"/>
    </bean>
    

    注册完后,我们完全不需要通过之前MyBatis那一套操作数据,MyBatis已经完全交由Spring管理,可以从Spring容器中获取 实现类 操作数据。

  7. 测试

    @Test
    public void test() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserMapperImpl userMapper = context.getBean("userMapper", UserMapperImpl.class);
        List<User> list = userMapper.selectUser();
        for (User user:list){
            System.out.println(user);
        }
    }
    

    至此,测试类中的MyBatis彻底消失了。

    spring整合mybatis之后,由spring管理的sqlSeesion在sql方法(增删改查等操作)执行完毕后就自行关闭了sqlSession,不需要我们对其进行手动关闭(封装在SqlSessionTemplate中)。 帖子

  8. 结果成功输出!现在我们的Mybatis配置文件的状态!发现都可以被Spring整合!

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
           PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
           "<http://mybatis.org/dtd/mybatis-3-config.dtd>">
    <configuration>
        <!--  设置日志 使用<settings>标签-->
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
        
       <typeAliases>
           <package name="com.torch.pojo"/>
       </typeAliases>
    </configuration>
    

    引用狂神的话,我们还可以保留mybatis-config配置文件,主要用于 setting 和 别名。

同时我们可以对beans配置文件进行分离,将 dataSource、sqlSessionFactory、sqlSessionTemplate 注册放到 “spring-dao.xml"中,届时还可以将"spring-mvc.xml"分离,最终import 到"ApplicationContext.xml” 配置中。

11.3.2 整合实现二

mybatis-spring1.2.3版以上的才有这个 ,了解即可,重点掌握上面的使用方法

SqlSessionDaoSupport

SqlSessionDaoSupport 是一个抽象的支持类,用来为你提供 SqlSession。调用 getSqlSession() 方法你会得到一个 SqlSessionTemplate,之后可以用于执行 SQL 方法,就像下面这样:

public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
  public User getUser(String userId) {
    return getSqlSession().selectOne("org.mybatis.spring.sample.mapper.UserMapper.getUser", userId);
  }
}

SqlSessionDaoSupport 需要通过属性设置一个 sqlSessionFactorySqlSessionTemplate。如果两个属性都被设置了,那么 SqlSessionFactory 将被忽略。

假设类 UserMapperImplSqlSessionDaoSupport 的子类,可以编写如下的 Spring 配置来执行设置:

<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
测试:
  1. 将我们上面写的UserDaoImpl修改一下

    public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
       public List<User> selectUser() {
           return getSqlSession().getMapper(UserMapper.class).selectUser();
      }
    }
    
  2. 修改bean的配置

    <bean id="userMapper" class="com.torch.mapper.UserMapperImpl">
       <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>
    
    
  3. 测试

    @Test
    public void test2(){
       ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
       UserMapper mapper = (UserMapper) context.getBean("userMapper");
       List<User> user = mapper.selectUser();
       for(User user:list){
           System.out.println(user);
       }
    }
    

spring-dao.xml 中的 SqlSessionTemplate也不需要注册了。

13.声明式事务

13.1 回顾事务

  • 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
  • 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性。

事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用。

事务四个属性ACID

  1. 原子性(atomicity)

    事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用

  2. 一致性(consistency)

    一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中。

  3. 隔离性(isolation)

    可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏

  4. 持久性(durability)

    事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中

13.2 测试

将 11.3.2 (SqlSessionDaoSupport整合实现方法)的代码拷贝到一个新项目中

在之前的案例中,我们给UserMapper接口新增两个方法,删除和增加用户;

//添加一个用户
int addUser(User user);

//根据id删除用户
int deleteUser(int id);

UserMapper映射文件,我们故意把 deletes 写错,测试!

<insert id="addUser" parameterType="com.torch.pojo.User">
insert into user (id,name,pwd) values (#{id},#{name},#{pwd})
</insert>

<delete id="deleteUser" parameterType="int">
deletes from user where id = #{id}
</delete>

编写接口的实现类,在实现类中,我们去操作一波

public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
    //增加一些操作,这里将select add delete 放在一个方法里面假设为一个 事务(业务)
    public List<User> selectUser() {
        UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
        mapper.addUser(new User(4,"小明","123456"));
        mapper.deleteUser(4);
        return mapper.selectUser();
    }
    //新增
    public int addUser(User user) {
        return getSqlSession().getMapper(UserMapper.class).addUser(user);
    }
    //删除
    public int deleteUser(int id) {
        return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
    }
}

测试

@Test
public void test2(){
   ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
   UserMapper mapper = context.getBean("userMapper",UserMapper.class);
   List<User> list = mapper.selectUser();
   for(User user:list){
       System.out.println(user);
   }
}

报错:sql异常,delete写错了

结果 :插入成功!

没有进行事务的管理;我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要事务!

以前我们都需要自己手动管理事务,但是Spring给我们提供了事务管理,我们只需要配置即可;

13.3 事务管理

MyBatis 的 SqlSession 提供几个方法来在代码中处理事务。但是当使用 MyBatis-Spring 时,你的 bean 将会注入由 Spring 管理的 SqlSession 或映射器。也就是说,Spring 总是为你处理了事务。

你不能在 Spring 管理的 SqlSession 上调用 SqlSession.commit()SqlSession.rollback()SqlSession.close() 方法。如果这样做了,就会抛出 UnsupportedOperationException 异常。在使用注入的映射器时,这些方法也不会暴露出来。

无论 JDBC 连接是否设置为自动提交,调用 SqlSession 数据方法或在 Spring 事务之外调用任何在映射器中方法,事务都将会自动被提交。

一个使用 MyBatis-Spring 的其中一个主要原因是它允许 MyBatis 参与到 Spring 的事务管理中。而不是给 MyBatis 创建一个新的专用事务管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 来实现事务管理(13.3.1)。

一旦配置好了 Spring 的事务管理器,你就可以在 Spring 中按你平时的方式来配置事务。并且支持 @Transactional 注解和 AOP 风格的配置。在事务处理期间,一个单独的 SqlSession 对象将会被创建和使用。当事务完成时,这个 session 会以合适的方式提交或回滚

事务配置好了以后,MyBatis-Spring 将会透明地管理事务。这样在你的 DAO 类中就不需要额外的代码了。

Spring在不同的事务管理API之上定义了一个抽象层,使得开发人员不必了解底层的事务管理API就可以使用Spring的事务管理机制。Spring支持编程式事务管理和声明式的事务管理。

  • 声明式事务管理
    • 一般情况下比编程式事务好用。
    • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
    • 将事务管理作为横切关注点,通过aop方法模块化。Spring中通过Spring AOP框架支持声明式事务管理。
  • 编程式事务管理
    • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
    • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

13.3.1 声明式事务管理

1. 配置事务管理器(Spring的)

事务管理器

  • 无论使用Spring的哪种事务管理策略(编程式或者声明式)事务管理器都是必须的。
  • 事务管理器 就是 Spring的核心事务管理抽象,管理封装了一组独立于技术的方法。

实现方式

  • 在 Spring 的配置文件(“spring-dao.xml”)中创建一个 DataSourceTransactionManager 对象: 【这里采用的】

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <constructor-arg ref="dataSource" />
    </bean>
    
    
  • 注解开发方式:

    @Configuration
    public class DataSourceConfig {
        @Bean
        public DataSourceTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }
    }
    

    传入的 DataSource 可以是任何能够与 Spring 兼容的 JDBC DataSource。包括连接池和通过 JNDI 查找获得的 DataSource。

    注意:为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的同一个数据源,否则事务管理器就无法工作了。

接下来结合AOP实现事务的织入

2. 配置事务通知
  • 使用Spring管理事务,注意头文件的约束导入 : tx

    xmlns:tx="<http://www.springframework.org/schema/tx>"
    
    <http://www.springframework.org/schema/tx>
    <http://www.springframework.org/schema/tx/spring-tx.xsd>">   
    

    可以输入<tx:advice 会有IDEA自动弹出选择,选择上方的约束即可自动填写

  • 配置事务通知

    <!--配置事务通知,事务管理使用Spring唯一提供的管理器(DataSourceTransactionManager,前面的注册的bean)-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--配置哪些方法使用什么样的事务: <tx:method name="xxx">  -->
        <!--配置事务的传播特性: propagation="xxx"-->
       <tx:attributes>
           <tx:method name="insert" propagation="REQUIRED"/>
           <tx:method name="delete" propagation="REQUIRED"/>
           <tx:method name="update" propagation="REQUIRED"/>
           <!-- read--only 常用在只能查询的方法上-->
           <tx:method name="select" read-only="true"/>
           <!-- name为'*'指代了所有的方法-->
           <tx:method name="*" propagation="REQUIRED"/>
       </tx:attributes>
    </tx:advice>
    

    spring事务传播特性:

    事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。spring支持7种事务传播行为:

    • REQUIRED:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。
    • SUPPORTS:支持当前事务,如果没有当前事务,就以非事务方法执行。
    • MANDATORY:使用当前事务,如果没有当前事务,就抛出异常。
    • REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。
    • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    • NEVER:以非事务方式执行操作,如果当前事务存在则抛出异常。
    • NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

    使用 REQUIRED 和 NESTED ,能够保证进行事务处理。

    Spring 默认的事务传播行为是 PROPAGATION_REQUIRED ,它适合于绝大多数的情况。

    假设 ServiceX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链: Service1#method1()->Service2#method2()->Service3#method3(), 那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。就好比,我们刚才的几个方法存在调用,所以会被放在一组事务当中!

3. 配置事务的切入(AOP)

在"spring-dao.xml" 中编写

<!--配置aop织入事务-->
<aop:config>
    <!-- 切入点:任意返回类型 com.torch.mapper.UserMapper的所有方法(任意参数)  -->
   <aop:pointcut id="txPointcut" expression="execution(* com.torch.mapper.UserMapper.*(..))"/>
    <!-- 切入:上面配置的事务通知都会被织入 切入点中 -->
   <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
4. 进行测试
@Test
public void test2(){
   ApplicationContext context = new ClassPathXmlApplicationContext("ApplicationContext.xml");
   UserMapper mapper = (UserMapper) context.getBean("userMapper");
   List<User> list = mapper.selectUser();
   for(User user:list){
       System.out.println(user);
   }
}

这是我们可以看到,虽然程序还是抛出异常,但是之间User数据并没有插入到表中。

5. 思考问题?

为什么需要配置事务?

  • 如果不配置,可能存在数据提交不一致的情况;
  • 如果我们不再Spring中配置声明式事务,我们就需要在代码中手动配置事务;
  • 事务在项目开发过程非常重要,涉及到数据的一致性和完整性的问题,不容马虎!

13.3.2 将事务交由容器管理

如果你正使用一个 JEE 容器而且想让 Spring 参与到容器管理事务(Container managed transactions,CMT)的过程中,那么 Spring 应该被设置为使用 JtaTransactionManager由容器指定的一个子类作为事务管理器

最简单的实现方式:

  • Spring设置为使用 JtaTransactionManager :【最最简单】

    在“spring-dao.xml” 中添加下面的标签,表示使用 JtaTransactionManager

    <tx:jta-transaction-manager />
    

    使用IDEA自动弹出的选项,能够自动完成约束(如下)的填写

    xmlns:tx="<http://www.springframework.org/schema/tx>"
    
    <http://www.springframework.org/schema/tx>
    <http://www.springframework.org/schema/tx/spring-tx.xsd>">
    
  • 由容器指定的一个子类作为事务管理器:

    在配置类中,注册 使用 JtaTransactionManagerFactoryBean 获得 JtaTransactionManager(管理器)

    @Configuration
    public class DataSourceConfig {
      @Bean
      public JtaTransactionManager transactionManager() {
        return new JtaTransactionManagerFactoryBean().getObject();
      }
    }
    

在这个配置中,MyBatis 将会和 其它由容器管理事务配置的 Spring 事务资源 一样。Spring 会自动使用任何一个存在的容器事务管理器,并注入一个 SqlSession。 如果没有正在进行的事务,而基于事务配置需要一个新的事务的时候,Spring 会开启一个新的由容器管理的事务

注意,如果你想使用由容器管理的事务,而不想使用 Spring 的事务管理,你就不能配置任何的 Spring 事务管理器。并必须配置 SqlSessionFactoryBean 以使用基本的 MyBatisManagedTransactionFactory

通过 Spring配置文件实现:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <property name="dataSource" ref="dataSource" />
 <property name="transactionFactory">
     <bean class="org.apache.ibatis.transaction.managed.ManagedTransactionFactory" />
 </property>
</bean>

or 通过配置类实现:

@Configuration
public class MyBatisConfig {
 @Bean
 public SqlSessionFactory sqlSessionFactory() {
     SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
     factoryBean.setDataSource(dataSource());
     factoryBean.setTransactionFactory(new ManagedTransactionFactory());
     return factoryBean.getObject();
 }
}

13.3.3 编程式事务管理

如果你想编程式地控制事务,请参考 the Spring reference document(Data Access -Programmatic transaction management-)

本章只是简单地摘抄了官网的文章,编程式事务管理没有做太多的深究,狂神并没有详细讲解,且MyBatis-Spring中文官网中也没有过多的提及,具体内容还是看上面去往Spring官网的连接,等以后有空再深究。

Spring 框架提供了两种程序化事务管理方法,通过使用:

  • TransactionTemplateTransactionalOperator
  • 一个 TransactionManager 直接实现。

Spring 团队通常推荐 TransactionTemplate 用于命令式流中的程序化事务管理,而 TransactionalOperator 用于响应式代码。第二种方法类似于使用 JTA UserTransaction API,尽管异常处理不那么麻烦。

1. 使用事务模板

TransactionTemplate 采用与其他 Spring 模板相同的方法,例如 JdbcTemplate。它使用回调方法(将应用程序代码从必须进行样板获取和释放事务资源中解放出来)并产生意图驱动的代码,因为您的代码只关注您想要做的事情。

正如下面的示例所示,使用 TransactionTemplate 绝对可以将您与 Spring 的事务基础设施和 API 结合起来。程序化事务管理是否适合您的开发需求是您必须自己做出的决定。

必须在事务上下文中运行 并且显式使用 TransactionTemplate 的应用程序代码类似于下一个示例。

作为应用程序开发人员,您可以编写一个 TransactionCallback 实现(通常表示为匿名内部类),其中包含您需要在事务上下文中运行的代码。然后,您可以将自定义 TransactionCallback 的实例传递给 TransactionTemplate 上公开的 execute(…) 方法。以下示例显示了如何执行此操作:

public class SimpleService implements Service {
    // 这个实例中的所有方法共享一个TransactionTemplate
    private final TransactionTemplate transactionTemplate;
    
    // 使用构造函数注入来提供PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // 此方法中的代码在事务上下文中运行
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}

如果没有返回值,可以使用方便的 TransactionCallbackWithoutResult 类与匿名类一起使用,如下:

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});

回调中的代码可以通过在提供的 TransactionStatus 对象上调用 setRollbackOnly() 方法回滚事务,如下所示:

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});

指定事务设置

您可以通过编程或配置在 TransactionTemplate 上指定事务设置(例如传播模式、隔离级别、超时等)。默认情况下,TransactionTemplate 实例具有默认的事务设置。

  • 以下示例显示了特定 TransactionTemplate 的事务设置的编程自定义:

    public class SimpleService implements Service {
        private final TransactionTemplate transactionTemplate;
    
        public SimpleService(PlatformTransactionManager transactionManager) {
            this.transactionTemplate = new TransactionTemplate(transactionManager);
            // 如果需要,可以在这里显式设置事务设置
      		this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
            this.transactionTemplate.setTimeout(30); // 30 seconds
            // 等等...
        }
    }
    
  • 以下示例使用 Spring XML 配置定义了带有一些自定义事务设置的 TransactionTemplate:

    <bean id="sharedTransactionTemplate"
            class="org.springframework.transaction.support.TransactionTemplate">
        <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
        <property name="timeout" value="30"/>
    </bean>
    

    然后,您可以根据需要将 sharedTransactionTemplate 注入到任意数量的服务中。

最后,TransactionTemplate 类的实例是线程安全的,因为这些实例不维护任何会话状态。但是,TransactionTemplate 实例确实维护配置状态。因此,虽然多个类可能共享一个 TransactionTemplate 实例,但如果一个类需要使用具有不同设置(例如,不同的隔离级别)的 TransactionTemplate,则需要创建两个不同的 TransactionTemplate 实例

2. 使用事务运算符

TransactionOperator 遵循类似于其他反应式运算符的运算符设计。它使用回调方法(将应用程序代码从必须进行样板获取和释放事务资源中解放出来)并产生意图驱动的代码,因为您的代码只关注您想要做的事情。(和事务处理模板一样同样是使用回调方法…)

正如下面的示例所示,使用 TransactionOperator 绝对可以将您与 Spring 的事务基础设施和 API 结合起来。程序化事务管理是否适合您的开发需求是您必须自己做出的决定。

必须在事务上下文中运行并显式使用 TransactionOperator 的应用程序代码类似于下一个示例:

public class SimpleService implements Service {
    // 一个TransactionOperator,在此实例中的所有方法之间共享
    private final TransactionalOperator transactionalOperator;

    // 使用构造函数注入来提供ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionOperator = TransactionalOperator.create(transactionManager);
    }

    public Mono<Object> someServiceMethod() {
        // 此方法中的代码在事务上下文中运行
        Mono<Object> update = updateOperation1();
        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}

TransactionalOperator 可以通过两种方式使用:

  • 使用 Project Reactor 类型的操作符样式

    mono.as(transactionalOperator::transactional)

  • 其他所有情况的回调样式

    transactionalOperator.execute(TransactionCallback<T>)

回调中的代码可以通过在提供的 ReactiveTransaction 对象上调用 setRollbackOnly() 方法来回滚事务,如下所示:

transactionalOperator.execute(new TransactionCallback<>() {
    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
});

取消信号

在 Reactive Streams 中,订阅者可以取消其订阅并停止其发布者。 Project Reactor 以及其他库中的运算符,例如 next()、take(long)、timeout(Duration) 等,都可以发出取消。无法知道取消的原因,无论是由于错误还是只是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑在事务发布者下游使用的运算符。特别是在 Flux 或其他多值发布者的情况下,必须消耗全部输出以允许交易完成。

指定事务设置

您可以为 TransactionalOperator 指定事务设置(例如传播模式、隔离级别、超时等)。默认情况下, TransactionalOperator 实例具有默认的事务设置。以下示例显示了特定 TransactionalOperator 的事务设置的自定义:

public class SimpleService implements Service {
    private final TransactionalOperator transactionalOperator;

    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        // 如果需要,可以在这里显式设置事务设置
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 seconds
        // 等等
        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}
3. 使用事务管理器

以下部分解释了命令式和反应式事务管理器的编程用法。

使用 PlatformTransactionManager

对于命令式事务,您可以直接使用 org.springframework.transaction.PlatformTransactionManager 来管理您的事务。为此,通过 bean 引用将您使用的 PlatformTransactionManager 的实现传递给您的 bean。然后,通过使用 TransactionDefinitionTransactionStatus 对象,您可以启动事务、回滚和提交。以下示例显示了如何执行此操作:

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 显式设置事务名称只能通过编程方式完成
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
    // 将您的业务逻辑放在这里
} catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
使用 ReactiveTransactionManager

使用响应式事务时,您可以直接使用 org.springframework.transaction.ReactiveTransactionManager 来管理您的事务。为此,请通过 bean 引用将您使用的 ReactiveTransactionManager 的实现传递给您的 bean。然后,通过使用 TransactionDefinitionReactiveTransaction 对象,您可以启动事务、回滚和提交。以下示例显示了如何执行此操作:

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 显式设置事务名称只能通过编程方式完成
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);

reactiveTx.flatMap(status -> {

    Mono<Object> tx = ...; // put your business logic here

    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值