Spring系列:Spring框架的认知

Spring系列:Spring框架的认知

Spring 简介

概述

  • pring 的主要作用就是为代码“解耦”,降低代码间的耦合度。

  • 根据功能的不同,可以将一个系统中的代码分为 主业务逻辑系统级业务逻辑 两类。它们各自具有鲜明的特点:主业务代码间逻辑联系紧密,有具体的专业业务应用场景,复用性相对较低;系统级业务相对功能独立,没有具体的专业业务应用场景,主要是为主业务提供系统级服务,如日志、安全、事务等,复用性强

    • 系统开发过程中
      • 主业务逻辑
        • 银行业务
        • 保险业务
        • 电商业务
        • 物流业务
      • 系统级业务逻辑,交叉业务逻辑
        • JDBC连接数据库
          • 加载驱动
          • 创建连接
          • 开启事物
          • SQL操作具体业务
          • 提交事物
          • 释放连接
  • Spring 根据代码的功能特点,将降低耦合度的方式分为了两类:IoC 与 AOP。IoC 使得主业务在相互调用过程中,不用再自己维护关系了,即不用再自己创建要使用的对象了。而是由 Spring 容器统一管理,自动“注入”。而 AOP 使得系统级服务得到了最大复用,且不用再由程序员手工将系统级服务“混杂”到主业务逻辑中了,而是由 Spring 容器统一完成“织入”。

  • Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的。Spring 的核心是控制反转(IoC)和面向切面编程(AOP)。简单来说,Spring 是一个分层的 Java SE/EE full-stack(一站式)轻量级开源框架。

Spring 体系结构

概述

在这里插入图片描述

Spring 由 20 多个模块组成,它们可以分为数据访问/集成(Data Access/Integration)、Web、面向切面编程(AOP, Aspects)、应用服务器设备管理(Instrumentation)、消息发送(Messaging)、核心容器(Core Container)和测试(Test)。

Spring 的特点

非侵入式

所谓非侵入式是指,Spring 框架的 API 不会在业务逻辑上出现,即业务逻辑是 POJO。由于业务逻辑中没有 Spring 的 API,所以业务逻辑可以从 Spring 框架快速的移植到其他框架, 即与环境无关。

容器

Spring 作为一个容器,可以管理对象的生命周期、对象与对象之间的依赖关系。可以通过配置文件,来定义对象,以及设置与其他对象的依赖关系。

IoC

控制反转(Inversion of Control),即创建被调用者的实例不是由调用者完成,而是由 Spring 容器完成,并注入调用者。

当应用了 IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。即,不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

AOP

面向切面编程(AOP,Aspect Orient Programming),是一种编程思想,是面向对象编程 OOP 的补充。很多框架都实现了对 AOP 编程思想的实现。Spring 也提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如日志和事务管理)进行开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责其它的系统级关注点,例如日志或事务支持。

我们可以把日志、安全、事务管理等服务理解成一个“切面”,那么以前这些服务一直是直接写在业务逻辑的代码当中的,这有两点不好:首先业务逻辑不纯净;其次这些服务被很多业务逻辑反复使用,完全可以剥离出来做到复用。那么 AOP 就是这些问题的解决方案, 可以把这些服务剥离出来形成一个“切面”,以期复用,然后将“切面”动态的“织入”到业务逻辑中,让业务逻辑能够享受到此“切面”的服务。

Spring 与 IoC

概述

控制反转(IoC,Inversion of Control),是一个概念,是一种思想。指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理。控制反转就是对对象控制权的转移,从程序代码本身反转到了外部容器。

IoC 是一个概念,是一种思想,其实现方式多种多样。当前比较流行的实现方式有两种: 依赖注入和依赖查找。依赖注入方式应用更为广泛。

  • 依赖查找:Dependency Lookup,DL,容器提供回调接口和上下文环境给组件,程序代码则需要提供具体的查找方式。比较典型的是依赖于 JNDI 系统的查找。
  • **依赖注入:**Dependency Injection,DI,程序代码不做定位查询,这些工作由容器自行完成。

依赖注入 DI 是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

Spring 的依赖注入对调用者与被调用者几乎没有任何要求,完全支持 POJO 之间依赖关系的管理。

依赖注入是目前最优秀的解耦方式。依赖注入让 Spring 的 Bean 之间以配置文件的方式组织在一起,而不是以硬编码的方式耦合在一起的。

第一个 Spring 应用程序

POM

创建一个工程名为 hello-spring 的项目,pom.xml 文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.luxiu</groupId>
    <artifactId>hello-spring</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.17.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

主要增加了 org.springframework:spring-context 依赖

创建接口与实现

创建 UserService 接口

package com.luxiu.hello.spring.service;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName UserService
 * @date 2020/4/13 21:18
 * @company /
 */
public interface UserService {
    public void sayHi();
}

创建 UserServiceImpl 实现

package com.luxiu.hello.spring.service.impl;

import com.luxiu.hello.spring.service.UserService;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName UserServiceImpl
 * @date 2020/4/13 21:18
 * @company 
 */
public class UserServiceImpl implements UserService {
    public void sayHi() {
        System.out.println("Hello Spring");
    }
}

创建 Spring 配置文件

src/main/resources 目录下创建 spring-context.xml 配置文件,从现在开始类的实例化工作交给 Spring 容器管理(IoC),配置文件如下:

<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.luxiu.hello.spring.service.impl.UserServiceImpl" />
</beans>
  • <bean />:用于定义一个实例对象。一个实例对应一个 bean 元素。

  • id:该属性是 Bean 实例的唯一标识,程序通过 id 属性访问 Bean,Bean 与 Bean 间的依赖关系也是通过 id 属性关联的。

  • class:指定该 Bean 所属的类,注意这里只能是类,不能是接口。

测试 Spring IoC

创建一个 UserTests 测试类,测试对象是否能够通过 Spring 来创建,代码如下:

package com.luxiu.hello.spring.tests;

import com.luxiu.hello.spring.service.UserService;
import com.luxiu.hello.spring.service.impl.UserServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName UserTests
 * @date 2020/4/13 21:22
 * @company 
 */

public class UserTests {

    @Test
   public void testByNew(){
       UserService userService = new UserServiceImpl();
       userService.sayHi();
   }

    @Test
    public void testBySpring(){
        // 获取 Spring 容器
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-context.xml");
        // 从 Spring 容器中获取对象
        UserService userService = (UserService) applicationContext.getBean("userService");
        userService.sayHi();
    }
}

Bean 的装配方式

概述

截止目前为止,咱们 Bean 的装配方式是通过代码 getBean() 的方式从容器获取指定的 Bean 实例,容器首先会调用 Bean 类的无参构造器,创建空值的实例对象。除了使用 getBean() 的装配方式外,还可以使用注解的装配方式。

容器中 Bean 的作用域

在学习 Bean 的装配方式之前,我们先了解一下 Bean 的作用域。当通过 Spring 容器创建一个 Bean 实例时,不仅可以完成 Bean 的实例化,还可以通过 scope 属性,为 Bean 指定特定的作用域。Spring 支持 5 种作用域。

  • singleton:单态模式。即在整个 Spring 容器中,使用 singleton 定义的 Bean 将是单例的,只有一个实例。默认为单态的。
  • prototype:原型模式。即每次使用 getBean 方法获取的同一个 <bean /> 的实例都是一个新的实例。
  • request:对于每次 HTTP 请求,都将会产生一个不同的 Bean 实例。
  • session:对于每个不同的 HTTP session,都将产生一个不同的 Bean 实例。
  • global session:每个全局的 HTTP session 对应一个 Bean 实例。典型情况下,仅在使用 portlet 集群时有效,多个 Web 应用共享一个 session。一般应用中,global-session 与 session 是等同的。

注意事项:

  • 对于 scope 的值 request、session 与 global session,只有在 Web 应用中使用 Spring 时,该作用域才有效。
  • 对于 scope 为 singleton 的单例模式,该 Bean 是在容器被创建时即被装配好了。
  • 对于 scope 为 prototype 的原型模式,Bean 实例是在代码中使用该 Bean 实例时才进行装配的。

基于注解的装配方式

对于 DI 使用注解,将不再需要在 Spring 配置文件中声明 Bean 实例。Spring 中使用注解, 需要在原有 Spring 运行环境基础上再做一些改变

需要在 Spring 配置文件中配置组件扫描器,用于在指定的基本包中扫描注解。

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
">

    <context:annotation-config />
    <context:component-scan base-package="com.luxiu.shop"/>
</beans>

@Component

需要在类上使用注解 @Component,该注解的 value 属性用于指定该 bean 的 id 值。

@Component(value = "student")
public class Student {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Spring 还提供了 3 个功能基本和 @Component 等效的注解:

  • @Repository:用于对 DAO 实现类进行注解
  • @Service:用于对 Service 实现类进行注解
  • @Controller:用于对 Controller 实现类进行注解

@Scope

需要在类上使用注解 @Scope,其 value 属性用于指定作用域。默认为 singleton。

在这里插入图片描述

@Value

需要在属性上使用注解 @Value,该注解的 value 属性用于指定要注入的值。

在这里插入图片描述

使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。

@Autowired

需要在域属性上使用注解 @Autowired,该注解默认使用 按类型自动装配 Bean 的方式。

使用该注解完成属性注入时,类中无需 setter。当然,若属性有 setter,则也可将其加到 setter 上。

在这里插入图片描述

在这里插入图片描述

@Resource

需要在域属性上使用注解 @Resource,该注解有一个 name 属性,可以创建指定的 bean

@Resource(name = "userService")
private UserService userService;

@PostConstruct

在方法上使用 @PostConstruct 相当于初始化

在这里插入图片描述

注解与 XML 配置的区别

注解的好处是,配置方便,直观。但其弊端也显而易见:以硬编码的方式写入到了 Java 代码中,其修改是需要重新编译代码的。

XML 配置方式的最大好处是,对其所做修改,无需编译代码,只需重启服务器即可将新的配置加载。

若注解与 XML 同用,XML 的优先级要高于注解。这样做的好处是,需要对某个 Bean 做修改,只需修改配置文件即可。

Spring 事务管理简介

具体代码参考 https://github.com/luguangdong/spring-transaction.git

概述

事务原本是数据库中的概念,用于数据访问层。但一般情况下,需要将事务提升到业务层,即 Service 层。这样做是为了能够使用事务的特性来管理具体的业务。

在 Spring 中通常可以通过以下三种方式来实现对事务的管理:

  • 使用 Spring 的事务代理工厂管理事务(已过时)
  • 使用 Spring 的事务注解管理事务
  • 使用 AspectJ 的 AOP 配置管理事务

Spring 事务管理 API

Spring 的事务管理,主要用到两个事务相关的接口。

事务管理器接口

事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。

该接口定义了 3 个事务方法:

  • void commit(TransactionStatus status):事务的提交
  • TransactionStatus getTransaction(TransactionDefinition definition):获取事务的状态
  • void rollback(TranscationStatus status):事务的回滚
常用的两个实现类

PlatformTransactionManager 接口有两个常用的实现类:

  • DataSourceTransactionManager:使用 JDBC 或 MyBatis 进行持久化数据时使用。
  • HibernateTransactionManager:使用 Hibernate 进行持久化数据时使用。
Spring 的回滚方式

Spring 事务的默认回滚方式是:发生运行时异常回滚

事务定义接口

事务定义接口 TransactionDefinition 中定义了事务描述相关的三类常量:事务隔离级别事务传播行为事务默认超时时限,及对它们的操作。

事务的四种隔离级别
  • DEFAULT:采用 DB 默认的事务隔离级别。MySql 默认为 REPEATABLE_READ;Oracle 默认为:READ_COMMITTED;

  • READ_UNCOMMITTED:读未提交。未解决任何并发问题。

  • READ_COMMITTED:读已提交。解决脏读,存在不可重复读与幻读。

  • REPEATABLE_READ:可重复读。解决脏读、不可重复读。存在幻读。

  • SERIALIZABLE:串行化。不存在并发问题。

事务的七种传播行为

即然是传播,那么至少有两个东西,才可以发生传播。单体不存在传播这个行为。

事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

所谓事务传播行为是指,处于不同事务中的方法在相互调用时,执行期间事务的维护情况。如,A 事务中的方法 a() 调用 B 事务中的方法 b(),在调用执行期间事务的维护情况,就称为事务传播行为。事务传播行为是加在方法上的。

例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的

  • REQUIRED:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。这种传播行为是最常见的选择,也是 Spring 默认的事务传播行为。
  • SUPPORTS:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。
  • MANDATORY:指定的方法必须在当前事务内执行,若当前没有事务,则直接抛出异常。
  • REQUIRES_NEW:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。
  • NOT_SUPPORTED:指定的方法不能在事务环境中执行,若当前存在事务,就将当前事务挂起。
  • NEVER:指定的方法不能在事务环境下执行,若当前存在事务,就直接抛出异常。
  • NESTED:指定的方法必须在事务内执行。若当前存在事务,则在嵌套事务内执行;若当前没有事务,则创建一个新事务。

使用 AspectJ 的 AOP 配置管理事务

概述

AspectJ 主要是使用 XML 配置顾问方式自动为每个符合切入点表达式的类生成事务代理。创建测试操作步骤如下:

创建测试项目

创建一个名为 spring-transaction 项目,pom.xml 文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.luxiu.spring.transaction</groupId>
    <artifactId>spring-transaction</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>


    <properties>
        <!-- 环境配置 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>

        <!-- 统一的依赖管理 -->
        <log4j.version>1.2.17</log4j.version>
        <slf4j.version>1.7.25</slf4j.version>
        <spring.version>4.3.17.RELEASE</spring.version>
        <alibaba-druid.version>1.1.6</alibaba-druid.version>
        <mysql.version>5.1.46</mysql.version>
        <mybatis.version>3.2.8</mybatis.version>
        <mybaits-spring.version>1.3.1</mybaits-spring.version>
        <junit.version>4.12</junit.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>

    <dependencies>
        <!-- Test Begin -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <!-- Test Begin -->

        <!-- Spring Begin -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring End -->

        <!-- Log Begin -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <!-- Log End -->

        <!-- Database Begin -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${alibaba-druid.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>${mybaits-spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Database End -->

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>


</project>

创建实体类

TbContentCategory

package com.luxiu.spring.transaction.domain;

import java.io.Serializable;
import java.util.Date;
import lombok.Data;

/**
 * <p>
 * Description: 分类管理
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategory
 * @date 2020/5/23 21:56
 * @company https://www.singlewindow.cn/
 */
@Data
public class TbContentCategory implements Serializable {
    /**
    * 类目ID
    */
    private Long id;

    /**
    * 父类目ID=0时,代表的是一级的类目
    */
    private Long parentId;

    /**
    * 分类名称
    */
    private String name;

    /**
    * 状态。可选值:1(正常),2(删除)
    */
    private Integer status;

    /**
    * 排列序号,表示同级类目的展现次序,如数值相等则按名称次序排列。取值范围:大于零的整数
    */
    private Integer sortOrder;

    /**
    * 该类目是否为父类目,1为true,0为false
    */
    private Boolean isParent;

    /**
    * 创建时间
    */
    private Date created;

    /**
    * 创建时间
    */
    private Date updated;

    private static final long serialVersionUID = 1L;
}

TbContent

package com.luxiu.spring.transaction.domain;

import java.io.Serializable;
import java.util.Date;
import lombok.Data;

/**
 * <p>
 * Description: 内容管理
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContent
 * @date 2020/5/23 21:58
 * @company https://www.singlewindow.cn/
 */
@Data
public class TbContent implements Serializable {
    private Long id;

    /**
    * 内容类目ID
    */
    private Long categoryId;

    /**
    * 内容标题
    */
    private String title;

    /**
    * 子标题
    */
    private String subTitle;

    /**
    * 标题描述
    */
    private String titleDesc;

    /**
    * 链接
    */
    private String url;

    /**
    * 图片绝对路径
    */
    private String pic;

    /**
    * 图片2
    */
    private String pic2;

    /**
    * 内容
    */
    private String content;

    private Date created;

    private Date updated;
    
    private TbContentCategory tbContentCategory;

    private static final long serialVersionUID = 1L;
}

创建数据访问层

TbContentCategoryMapper

package com.luxiu.spring.transaction.mapper;

import com.luxiu.spring.transaction.domain.TbContentCategory;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryMapper
 * @date 2020/5/23 21:56
 * @company https://www.singlewindow.cn/
 */
public interface TbContentCategoryMapper {
    int deleteByPrimaryKey(Long id);

    int insert(TbContentCategory record);

    int insertSelective(TbContentCategory record);

    TbContentCategory selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(TbContentCategory record);

    int updateByPrimaryKey(TbContentCategory record);
}

TbContentMapper

package com.luxiu.spring.transaction.mapper;

import com.luxiu.spring.transaction.domain.TbContent;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentMapper
 * @date 2020/5/23 21:58
 * @company https://www.singlewindow.cn/
 */
public interface TbContentMapper {
    int deleteByPrimaryKey(Long id);

    int insert(TbContent record);

    int insertSelective(TbContent record);

    TbContent selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(TbContent record);

    int updateByPrimaryKey(TbContent record);
}

创建业务逻辑层

TbContentCategoryService

package com.luxiu.spring.transaction.service;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryService
 * @date 2020/5/23 22:02
 * @company https://www.singlewindow.cn/
 */
public interface TbContentCategoryService {
    void save(TbContentCategory tbContentCategory, TbContent tbContent);
}
package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceImpl
 * @date 2020/5/23 22:03
 * @company https://www.singlewindow.cn/
 */
@Service(value = "TbContentCategoryServiceConf")
class TbContentCategoryServiceConfImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;
    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        tbContentCategoryMapper.insert(tbContentCategory);
        tbContentService.save(tbContent);
    }
}

TbContentService

package com.luxiu.spring.transaction.service;

import com.luxiu.spring.transaction.domain.TbContent;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentService
 * @date 2020/5/23 22:05
 * @company https://www.singlewindow.cn/
 */
public interface TbContentService {
    void save(TbContent tbContent);
}
package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentServiceImpl
 * @date 2020/5/23 22:06
 * @company https://www.singlewindow.cn/
 */
@Service
public class TbContentServiceImpl implements TbContentService {
    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
    }
}

创建 Spring 配置

spring-context.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"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.luxiu.spring.transaction">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务通知 -->
    <tx:advice id="myAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="save*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置顾问和切入点 -->
    <aop:config>
        <aop:pointcut id="myPointcut" expression="execution(* com.luxiu.spring.transaction.service.*.*(..))" />
        <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut" />
    </aop:config>
</beans>

spring-context-druid.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 加载配置属性文件 -->
    <context:property-placeholder ignore-unresolvable="true" location="classpath:jdbc.properties"/>

    <!-- 数据源配置, 使用 Druid 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>

        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc.connectionURL}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.pool.init}"/>
        <property name="minIdle" value="${jdbc.pool.minIdle}"/>
        <property name="maxActive" value="${jdbc.pool.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="${jdbc.testSql}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="stat"/>
    </bean>
</beans>

spring-context-mybatis.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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 配置 SqlSession -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 用于配置对应实体类所在的包,多个 package 之间可以用 ',' 号分割 -->
        <property name="typeAliasesPackage" value="com.luxiu.spring.transaction.domain"/>
        <!-- 用于配置对象关系映射配置文件所在目录 -->
        <property name="mapperLocations" value="classpath:/mapper/**/*.xml"/>
        <property name="configLocation" value="classpath:/mybatis-config.xml"></property>
    </bean>

    <!-- 扫描 Mapper -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.luxiu.spring.transaction.mapper" />
    </bean>
</beans>

创建 MyBatis 配置和映射文件

mybatis-config.xml

<?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>
        <!-- 打印 SQL 语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="false"/>

        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>

        <!-- 是否允许单条 SQL 返回多个数据集 (取决于驱动的兼容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true"/>

        <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
        <setting name="useColumnLabel" value="true"/>

        <!-- 允许 JDBC 生成主键。需要驱动器支持。如果设为了 true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false  -->
        <setting name="useGeneratedKeys" value="false"/>

        <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不映射 PARTIAL:部分 FULL:全部  -->
        <setting name="autoMappingBehavior" value="PARTIAL"/>

        <!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
        <setting name="defaultExecutorType" value="SIMPLE"/>

        <!-- 使用驼峰命名法转换字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>

        <!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
        <setting name="localCacheScope" value="SESSION"/>

        <!-- 设置 JDBC 类型为空时,某些驱动程序 要指定值, default:OTHER,插入空值时不需要指定类型 -->
        <setting name="jdbcTypeForNull" value="NULL"/>
    </settings>
</configuration>

TbContentCategoryMapper.xml

<?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.luxiu.spring.transaction.mapper.TbContentCategoryMapper">
  <resultMap id="BaseResultMap" type="com.luxiu.spring.transaction.domain.TbContentCategory">
    <!--@mbg.generated-->
    <!--@Table tb_content_category-->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="parent_id" jdbcType="BIGINT" property="parentId" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="status" jdbcType="INTEGER" property="status" />
    <result column="sort_order" jdbcType="INTEGER" property="sortOrder" />
    <result column="is_parent" jdbcType="BOOLEAN" property="isParent" />
    <result column="created" jdbcType="TIMESTAMP" property="created" />
    <result column="updated" jdbcType="TIMESTAMP" property="updated" />
  </resultMap>
  <sql id="Base_Column_List">
    <!--@mbg.generated-->
    id, parent_id, `name`, `status`, sort_order, is_parent, created, updated
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    <!--@mbg.generated-->
    select 
    <include refid="Base_Column_List" />
    from tb_content_category
    where id = #{id,jdbcType=BIGINT}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    <!--@mbg.generated-->
    delete from tb_content_category
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.luxiu.spring.transaction.domain.TbContentCategory" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into tb_content_category (parent_id, `name`, `status`, 
      sort_order, is_parent, created, 
      updated)
    values (#{parentId,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{status,jdbcType=INTEGER}, 
      #{sortOrder,jdbcType=INTEGER}, #{isParent,jdbcType=BOOLEAN}, #{created,jdbcType=TIMESTAMP}, 
      #{updated,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.luxiu.spring.transaction.domain.TbContentCategory" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into tb_content_category
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="parentId != null">
        parent_id,
      </if>
      <if test="name != null">
        `name`,
      </if>
      <if test="status != null">
        `status`,
      </if>
      <if test="sortOrder != null">
        sort_order,
      </if>
      <if test="isParent != null">
        is_parent,
      </if>
      <if test="created != null">
        created,
      </if>
      <if test="updated != null">
        updated,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="parentId != null">
        #{parentId,jdbcType=BIGINT},
      </if>
      <if test="name != null">
        #{name,jdbcType=VARCHAR},
      </if>
      <if test="status != null">
        #{status,jdbcType=INTEGER},
      </if>
      <if test="sortOrder != null">
        #{sortOrder,jdbcType=INTEGER},
      </if>
      <if test="isParent != null">
        #{isParent,jdbcType=BOOLEAN},
      </if>
      <if test="created != null">
        #{created,jdbcType=TIMESTAMP},
      </if>
      <if test="updated != null">
        #{updated,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.luxiu.spring.transaction.domain.TbContentCategory">
    <!--@mbg.generated-->
    update tb_content_category
    <set>
      <if test="parentId != null">
        parent_id = #{parentId,jdbcType=BIGINT},
      </if>
      <if test="name != null">
        `name` = #{name,jdbcType=VARCHAR},
      </if>
      <if test="status != null">
        `status` = #{status,jdbcType=INTEGER},
      </if>
      <if test="sortOrder != null">
        sort_order = #{sortOrder,jdbcType=INTEGER},
      </if>
      <if test="isParent != null">
        is_parent = #{isParent,jdbcType=BOOLEAN},
      </if>
      <if test="created != null">
        created = #{created,jdbcType=TIMESTAMP},
      </if>
      <if test="updated != null">
        updated = #{updated,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=BIGINT}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.luxiu.spring.transaction.domain.TbContentCategory">
    <!--@mbg.generated-->
    update tb_content_category
    set parent_id = #{parentId,jdbcType=BIGINT},
      `name` = #{name,jdbcType=VARCHAR},
      `status` = #{status,jdbcType=INTEGER},
      sort_order = #{sortOrder,jdbcType=INTEGER},
      is_parent = #{isParent,jdbcType=BOOLEAN},
      created = #{created,jdbcType=TIMESTAMP},
      updated = #{updated,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=BIGINT}
  </update>
</mapper>

TbContentMapper.xml

<?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.luxiu.spring.transaction.mapper.TbContentMapper">
  <resultMap id="BaseResultMap" type="com.luxiu.spring.transaction.domain.TbContent">
    <!--@mbg.generated-->
    <!--@Table tb_content-->
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="category_id" jdbcType="BIGINT" property="categoryId" />
    <result column="title" jdbcType="VARCHAR" property="title" />
    <result column="sub_title" jdbcType="VARCHAR" property="subTitle" />
    <result column="title_desc" jdbcType="VARCHAR" property="titleDesc" />
    <result column="url" jdbcType="VARCHAR" property="url" />
    <result column="pic" jdbcType="VARCHAR" property="pic" />
    <result column="pic2" jdbcType="VARCHAR" property="pic2" />
    <result column="content" jdbcType="LONGVARCHAR" property="content" />
    <result column="created" jdbcType="TIMESTAMP" property="created" />
    <result column="updated" jdbcType="TIMESTAMP" property="updated" />
  </resultMap>
  <sql id="Base_Column_List">
    <!--@mbg.generated-->
    id, category_id, title, sub_title, title_desc, url, pic, pic2, content, created, 
    updated
  </sql>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    <!--@mbg.generated-->
    select 
    <include refid="Base_Column_List" />
    from tb_content
    where id = #{id,jdbcType=BIGINT}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    <!--@mbg.generated-->
    delete from tb_content
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.luxiu.spring.transaction.domain.TbContent" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into tb_content (category_id, title, sub_title, 
      title_desc, url, pic, 
      pic2, content, created, 
      updated)
    values (#{categoryId,jdbcType=BIGINT}, #{title,jdbcType=VARCHAR}, #{subTitle,jdbcType=VARCHAR}, 
      #{titleDesc,jdbcType=VARCHAR}, #{url,jdbcType=VARCHAR}, #{pic,jdbcType=VARCHAR}, 
      #{pic2,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}, #{created,jdbcType=TIMESTAMP}, 
      #{updated,jdbcType=TIMESTAMP})
  </insert>
  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.luxiu.spring.transaction.domain.TbContent" useGeneratedKeys="true">
    <!--@mbg.generated-->
    insert into tb_content
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="categoryId != null">
        category_id,
      </if>
      <if test="title != null">
        title,
      </if>
      <if test="subTitle != null">
        sub_title,
      </if>
      <if test="titleDesc != null">
        title_desc,
      </if>
      <if test="url != null">
        url,
      </if>
      <if test="pic != null">
        pic,
      </if>
      <if test="pic2 != null">
        pic2,
      </if>
      <if test="content != null">
        content,
      </if>
      <if test="created != null">
        created,
      </if>
      <if test="updated != null">
        updated,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="categoryId != null">
        #{categoryId,jdbcType=BIGINT},
      </if>
      <if test="title != null">
        #{title,jdbcType=VARCHAR},
      </if>
      <if test="subTitle != null">
        #{subTitle,jdbcType=VARCHAR},
      </if>
      <if test="titleDesc != null">
        #{titleDesc,jdbcType=VARCHAR},
      </if>
      <if test="url != null">
        #{url,jdbcType=VARCHAR},
      </if>
      <if test="pic != null">
        #{pic,jdbcType=VARCHAR},
      </if>
      <if test="pic2 != null">
        #{pic2,jdbcType=VARCHAR},
      </if>
      <if test="content != null">
        #{content,jdbcType=LONGVARCHAR},
      </if>
      <if test="created != null">
        #{created,jdbcType=TIMESTAMP},
      </if>
      <if test="updated != null">
        #{updated,jdbcType=TIMESTAMP},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.luxiu.spring.transaction.domain.TbContent">
    <!--@mbg.generated-->
    update tb_content
    <set>
      <if test="categoryId != null">
        category_id = #{categoryId,jdbcType=BIGINT},
      </if>
      <if test="title != null">
        title = #{title,jdbcType=VARCHAR},
      </if>
      <if test="subTitle != null">
        sub_title = #{subTitle,jdbcType=VARCHAR},
      </if>
      <if test="titleDesc != null">
        title_desc = #{titleDesc,jdbcType=VARCHAR},
      </if>
      <if test="url != null">
        url = #{url,jdbcType=VARCHAR},
      </if>
      <if test="pic != null">
        pic = #{pic,jdbcType=VARCHAR},
      </if>
      <if test="pic2 != null">
        pic2 = #{pic2,jdbcType=VARCHAR},
      </if>
      <if test="content != null">
        content = #{content,jdbcType=LONGVARCHAR},
      </if>
      <if test="created != null">
        created = #{created,jdbcType=TIMESTAMP},
      </if>
      <if test="updated != null">
        updated = #{updated,jdbcType=TIMESTAMP},
      </if>
    </set>
    where id = #{id,jdbcType=BIGINT}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.luxiu.spring.transaction.domain.TbContent">
    <!--@mbg.generated-->
    update tb_content
    set category_id = #{categoryId,jdbcType=BIGINT},
      title = #{title,jdbcType=VARCHAR},
      sub_title = #{subTitle,jdbcType=VARCHAR},
      title_desc = #{titleDesc,jdbcType=VARCHAR},
      url = #{url,jdbcType=VARCHAR},
      pic = #{pic,jdbcType=VARCHAR},
      pic2 = #{pic2,jdbcType=VARCHAR},
      content = #{content,jdbcType=LONGVARCHAR},
      created = #{created,jdbcType=TIMESTAMP},
      updated = #{updated,jdbcType=TIMESTAMP}
    where id = #{id,jdbcType=BIGINT}
  </update>
</mapper>

创建系统配置文件

log4j.properties

log4j.rootLogger=INFO, console, file

log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d %p [%c] - %m%n

log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File=logs/log.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.A3.MaxFileSize=1024KB
log4j.appender.A3.MaxBackupIndex=10
log4j.appender.file.layout.ConversionPattern=%d %p [%c] - %m%n

jdbc.properties

#============================#
#==== Database settings ====#
#============================#

# JDBC
# MySQL 8.x: com.mysql.cj.jdbc.Driver
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://192.168.137.128:3306/myshop?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=123456

# JDBC Pool
jdbc.pool.init=1
jdbc.pool.minIdle=3
jdbc.pool.maxActive=20

# JDBC Test
jdbc.testSql=SELECT 'x' FROM DUAL

创建测试类

package com.luxiu.spring.transaction.tests;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TestSpringTransaction
 * @date 2020/5/23 22:12
 * @company https://www.singlewindow.cn/
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring-context.xml", "classpath:spring-context-druid.xml", "classpath:spring-context-mybatis.xml"})
public class TestSpringTransaction {
     @Resource(name = "TbContentCategoryServiceConf")
    private TbContentCategoryService TbContentCategoryServiceConf;

    /**
     * 测试说明
     * <ur>
     *     <li>将spring-context.xml配置文件中关于事务的配置开启</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *              tbContent.setTitle("测试事务内容");
     *         测试结果:
     *              tb_content_category 表中的数据回滚,数据没有插入表中,事物生效
     *
     *     </p>
     *     <li>将spring-context.xml配置文件中关于事务的配置关闭</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *               tbContent.setTitle("测试事务内容");
     *         测试结果:
     *               tb_content_category 表中的数据没有回滚,数据插入表中
     *     </p>
     * </ur>
     */
    @Test
    public void test() {
        TbContentCategory tbContentCategory = new TbContentCategory();
        tbContentCategory.setId(1L);
        tbContentCategory.setName("测试事务分类");

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(44L);
        tbContent.setTbContentCategory(tbContentCategory);
        // 在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
        tbContent.setTitle("测试事务内容");

        TbContentCategoryServiceConf.save(tbContentCategory, tbContent);
    }
}

运行观察事务效果:

  • 有事务:数据插入成功则两张表都存在数据,只要出现异常则两张表都没有数据
  • 无事务:如果第一张表数据插入成功,但第二张表报错则第一张表的数据不会回滚

使用 Spring 注解管理事务

概述

通过 @Transactional 注解方式,也可将事务织入到相应方法中。而使用注解方式,只需在配置文件中加入一个 tx 标签,以告诉 Spring 使用注解来完成事务的织入。该标签只需指定一个属性,事务管理器。

<!-- 开启事务注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager" />

@Transactional 注解简介

@Transactional 的所有可选属性:

  • propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED
  • isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举 ,默认值为 Isolation.DEFAULT
  • readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false
  • timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
  • rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • noRollbackForClassName: 指定不需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解 @Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的 @Transaction 注解。

@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。

使用 @Transaction 注解

使用起来很简单,只需要在需要增加事务的业务类上增加 @Transaction 注解即可,案例代码如下:

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceAnnotationImpl
 * @date 2020/5/23 23:11
 * @company https://www.singlewindow.cn/
 */
@Transactional
@Service(value = "TbContentCategoryServiceAnnotation")
public class TbContentCategoryServiceAnnotationImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        tbContentCategoryMapper.insert(tbContentCategory);
        tbContentService.save(tbContent);
    }
}

创建测试类

package com.luxiu.spring.transaction.tests;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TestSpringTransaction
 * @date 2020/5/23 22:12
 * @company https://www.singlewindow.cn/
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring-context.xml", "classpath:spring-context-druid.xml", "classpath:spring-context-mybatis.xml"})
public class TestSpringTransaction {
    @Resource(name = "TbContentCategoryServiceConf")
    private TbContentCategoryService TbContentCategoryServiceConf;

    @Resource(name = "TbContentCategoryServiceAnnotation")
    private TbContentCategoryService TbContentCategoryServiceAnnotation;
    /**
     * 测试说明: 使用配置文件的方式来测试spring的事物
     * <ur>
     *     <li>将spring-context.xml配置文件中关于事务的配置开启</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *              tbContent.setTitle("测试事务内容");
     *         测试结果:
     *              tb_content_category 表中的数据回滚,数据没有插入表中,事物生效
     *
     *     </p>
     *     <li>将spring-context.xml配置文件中关于事务的配置关闭</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *               tbContent.setTitle("测试事务内容");
     *         测试结果:
     *               tb_content_category 表中的数据没有回滚,数据插入表中
     *     </p>
     * </ur>
     */
    @Test
    public void testTbContentCategoryServiceConf() {
        TbContentCategory tbContentCategory = new TbContentCategory();
        tbContentCategory.setId(1L);
        tbContentCategory.setName("测试事务分类");

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(45L);
        tbContent.setTbContentCategory(tbContentCategory);
        // 在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
        tbContent.setTitle("测试事务内容");

        TbContentCategoryServiceConf.save(tbContentCategory, tbContent);
    }


    /**
     * 测试说明: 使用注解的方式来测试spring的事物
     * <ur>
     *     <li>将spring-context.xml配置文件中关于事务的配置开启</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *              tbContent.setTitle("测试事务内容");
     *         测试结果:
     *              tb_content_category 表中的数据回滚,数据没有插入表中,事物生效
     *
     *     </p>
     *     <li>将spring-context.xml配置文件中关于事务的配置关闭</li>
     *     <p>
     *         在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
     *               tbContent.setTitle("测试事务内容");
     *         测试结果:
     *               tb_content_category 表中的数据没有回滚,数据插入表中
     *     </p>
     * </ur>
     */
    @Test
    public void testTbContentCategoryServiceAnnotation() {
        TbContentCategory tbContentCategory = new TbContentCategory();
        tbContentCategory.setId(1L);
        tbContentCategory.setName("测试事务分类");

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(48L);
        tbContent.setTbContentCategory(tbContentCategory);
        // 在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
        tbContent.setTitle("测试事务内容");

        TbContentCategoryServiceAnnotation.save(tbContentCategory, tbContent);
    }



}

运行观察事务效果:

  • 有事务:数据插入成功则两张表都存在数据,只要出现异常则两张表都没有数据
  • 无事务:如果第一张表数据插入成功,但第二张表报错则第一张表的数据不会回滚

Spring事务嵌套引发的问题思考和解决-Transaction rolled back because it has been marked as rollback-only

模拟该异常出现情况

内容实现类中模拟 int i = 1/0异常,该类中没有处理异常

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentServiceImpl
 * @date 2020/5/23 22:06
 * @company https://www.singlewindow.cn/
 */
@Service
@Transactional
public class TbContentServiceImpl implements TbContentService {
    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
        int i = 1/0;

    }
}

内容分类实现类TbContentCategoryServiceRollbackOnlyImpl中的save()方法调用内容实现类TbContentServiceImpl的save方法,并且捕获异常

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceRollbackOnlyImpl
 * @date 2020/5/23 23:11
 * @company https://www.singlewindow.cn/
 */
@Transactional
@Service(value = "tbContentCategoryServiceRollbackOnly")
public class TbContentCategoryServiceRollbackOnlyImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        try {
            tbContentCategoryMapper.insert(tbContentCategory);
            tbContentService.save(tbContent);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

编写测试类来验证

  @Resource(name = "tbContentCategoryServiceRollbackOnly")
    private TbContentCategoryService tbContentCategoryServiceRollbackOnly;

/**
     * Spring事务嵌套引发的血案测试
     * Transaction rolled back because it has been marked as rollback-only
     */
    @Test
    public void testTbContentCategoryServiceByRollbackOnly() {
        TbContentCategory tbContentCategory = new TbContentCategory();
        tbContentCategory.setId(1L);
        tbContentCategory.setName("测试事务分类");

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(48L);
        tbContent.setTbContentCategory(tbContentCategory);
        // 在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
        tbContent.setTitle("测试事务内容");
        tbContentCategoryServiceRollbackOnly.save(tbContentCategory, tbContent);
    }

运行结果为:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

原因分析

spring的@Transactional可以注解到方法上或者类上,并且只有在该类被作为spring容器托管的bean的时候,也就是使用@autowird被注入到其他类中使用,调用该类的方法的时候才生效, 因为此时调用方法会被spring容器的 TransactionInterceptor拦截器拦截并开启事务 。

使用默认的@Transactional 注解 ,事物的传播性默认为Propagation.REQUIRED ,调用其他类的事务方法 tbContentService.save(tbContent); 这个方法开启的事务是默认Propagation.REQUIRED ,也就是沿用外层调用方法tbContentCategoryServiceRollbackOnly.save(tbContentCategory, tbContent)的事务 (如果有事务存在 则使用原事务 如果不存在则开启新事务), 外层方法开启了一个事务 ,内层方法调用的是其他类的事务方法 ,内层方法发现异常了, 会标记整个事务为roll-back ,但是外层方法捕获异常 return的时候 会执行commit事务, 但是此时发现已经标记异常 所以才会出错。

看看处理回滚的源码:

在最后commit方法中,会判断到时rollback,就会调用processRollback(defStatus,true),第二个参数会赋值给unexpectedRollback,所以就会报这个错了。分发

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
	try {
		boolean unexpectedRollback = unexpected;

		try {
			triggerBeforeCompletion(status);

			if (status.hasSavepoint()) {
				if (status.isDebug()) {
					logger.debug("Rolling back transaction to savepoint");
				}
				status.rollbackToHeldSavepoint();
			}
			else if (status.isNewTransaction()) {
				if (status.isDebug()) {
					logger.debug("Initiating transaction rollback");
				}
				doRollback(status);
			}
			else {
				// Participating in larger transaction
				if (status.hasTransaction()) {
					if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
						}
						doSetRollbackOnly(status);
					}
					else {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
						}
					}
				}
				else {
					logger.debug("Should roll back transaction but cannot - no transaction available");
				}
				// Unexpected rollback only matters here if we're asked to fail early
				if (!isFailEarlyOnGlobalRollbackOnly()) {
					unexpectedRollback = false;
				}
			}
		} catch (RuntimeException | Error ex) {
			triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
			throw ex;
		}

		triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

		// Raise UnexpectedRollbackException if we had a global rollback-only marker
		if (unexpectedRollback) {
			throw new UnexpectedRollbackException(
					"Transaction rolled back because it has been marked as rollback-only");
		}
	}
	finally {
		cleanupAfterCompletion(status);
	}
}

解决方案

Spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeException的异常

  • 换句话说:service上的事务方法不要自己try catch(或者catch后throw new runtimeExcetpion()也成)这样程序异常时才能被aop捕获进而回滚。
  • 在service层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(这也是比较推荐的做法)
  • 最简单的决绝办法是就是调用的外部类中的方法中不要添加事物(这需要看具体的业务逻辑)
  • 可以用一个新事务也是能解决这个问题的
  • <property name="globalRollbackOnParticipationFailure" value="false" />,这个方法也能解决,但显然影响到全局的事务属性,所以极力不推荐使用。

解决方案一

这种方式如果业务场景不需要查看错误,则可以不需要catch后throw new runtimeExcetpion(),换句话就是不做任何处理

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceRollbackOnlyImpl
 * @date 2020/5/23 23:11
 * @company https://www.singlewindow.cn/
 */
@Transactional
@Service(value = "tbContentCategoryServiceRollbackOnly")
public class TbContentCategoryServiceRollbackOnlyImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        try {
            tbContentCategoryMapper.insert(tbContentCategory);
            tbContentService.save(tbContent);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }


    }
}

解决方案二(强烈推荐)

catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceRollbackOnlyImpl
 * @date 2020/5/23 23:11
 * @company https://www.singlewindow.cn/
 */
@Transactional
@Service(value = "tbContentCategoryServiceRollbackOnly")
public class TbContentCategoryServiceRollbackOnlyImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        try {
            tbContentCategoryMapper.insert(tbContentCategory);
            tbContentService.save(tbContent);
        } catch (Exception e) {
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }


    }
}

解决方案三

外部方法不使用事物

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;



/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentServiceImpl
 * @date 2020/5/23 22:06
 * @company https://www.singlewindow.cn/
 */
@Service
public class TbContentServiceImpl implements TbContentService {
    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
        int i = 1/0;


    }
}

解决方案四

外部方法开启一个新的事物

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;


/**
 * <p>
 * Description:
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentServiceImpl
 * @date 2020/5/23 22:06
 * @company https://www.singlewindow.cn/
 */
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class TbContentServiceImpl implements TbContentService {
    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
        int i = 1/0;


    }
}

解决方案五

配置数据源事物管理器globalRollbackOnParticipationFailure属性为false

 <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
        <property name="globalRollbackOnParticipationFailure" value="false" />
    </bean>

Spring事务嵌套再思考

案例

如果是同一个类中的方法调用该类的其他方法 ,第二个方法的事务是不起作用的

package com.luxiu.spring.transaction.service.impl;

import com.luxiu.spring.transaction.domain.TbContent;
import com.luxiu.spring.transaction.domain.TbContentCategory;
import com.luxiu.spring.transaction.mapper.TbContentCategoryMapper;
import com.luxiu.spring.transaction.mapper.TbContentMapper;
import com.luxiu.spring.transaction.service.TbContentCategoryService;
import com.luxiu.spring.transaction.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;



/**
 * <p>
 * Description: 测试git合并部分代码
 *
 * </p>
 *
 * @author luguangdong
 * @version 1.0
 * @ClassName TbContentCategoryServiceRollbackOnlyImpl
 * @date 2020/5/23 23:11
 * @company https://www.singlewindow.cn/
 */
@Transactional
@Service(value = "tbContentCategoryServiceRollbackOnly")
public class TbContentCategoryServiceRollbackOnlyImpl implements TbContentCategoryService {
    @Autowired
    private TbContentCategoryMapper tbContentCategoryMapper;
    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
        try {
            tbContentCategoryMapper.insert(tbContentCategory);
            save(tbContent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Autowired
    private TbContentMapper tbContentMapper;
    public void save(TbContent tbContent) {
        tbContentMapper.insert(tbContent);
        int i = 1/0;
    }
}

此时执行测试类后发现,两张表的数据都入库了

 /**
     * Spring事务嵌套引发的血案测试
     * Transaction rolled back because it has been marked as rollback-only
     */
    @Test
    public void testTbContentCategoryServiceByRollbackOnly() {
        TbContentCategory tbContentCategory = new TbContentCategory();
        tbContentCategory.setId(1L);
        tbContentCategory.setName("测试事务分类20200706");

        TbContent tbContent = new TbContent();
        tbContent.setCategoryId(48L);
        tbContent.setTbContentCategory(tbContentCategory);
        // 在这里你可以将内容设置为超出数据库字段的存储范围来验证事务是否开启
        tbContent.setTitle("测试事务内容20200706");
        tbContentCategoryServiceRollbackOnly.save(tbContentCategory, tbContent);
    }

在这里插入图片描述

总结

spring的@Transactional可以注解到方法上或者类上,并且只有在该类被作为spring容器托管的bean的时候,也就是使用@autowird被注入到其他类中使用,调用该类的方法的时候才生效, 因为此时调用方法会被spring容器的 TransactionInterceptor拦截器拦截并开启事务 。

使用默认的@Transactional 注解 ,事物的传播性默认为Propagation.REQUIRED 。

同一个类中的方法调用该类的其他方法 ,第二个方法的事务是不起作用的,所以入库成功。

调用其他类的事务方法 ,其他类的方法如果有事务存在则使用原事务 如果不存在则开启新事务, 外层方法开启了一个事务 ,内层方法调用的是其他类的事务方法 ,内层方法发现异常了, 会标记整个事务为roll-back ,但是外层方法捕获异常 return的时候 会执行commit事务, 但是此时发现已经标记异常 所以才会出错。

Spring 原始整合 Web

容器初始化

启动容器时需要自动装载 ApplicationContext,Spring 提供的 ContextLoaderListener 就是为了自动装配 ApplicationContext 的配置信息

POM

需要在 pom.xml 增加 org.springframework:spring-web 依赖

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.17.RELEASE</version>
</dependency>

配置 web.xml

web.xml 配置如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-context*.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

ApplicationContextAware

当一个类实现了这个接口(ApplicationContextAware)之后,这个类就可以方便获得 ApplicationContext 中的所有 bean。换句话说,就是这个类可以直接获取 Spring 配置文件中,所有有引用到的 Bean 对象。

package com.luxiu.beyond.shop.commons.context;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class SpringContext implements ApplicationContextAware, DisposableBean {

    private static final Logger logger = LoggerFactory.getLogger(SpringContext.class);

    private static ApplicationContext applicationContext;

    /**
     * 获取存储在静态变量中的 ApplicationContext
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        assertContextInjected();
        return applicationContext;
    }

    /**
     * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
     * @param name
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        assertContextInjected();
        return applicationContext.getBean(clazz);
    }

    /**
     * 实现 DisposableBean 接口,在 Context 关闭时清理静态变量
     * @throws Exception
     */
    public void destroy() throws Exception {
        logger.debug("清除 SpringContext 中的 ApplicationContext: {}", applicationContext);
        applicationContext = null;
    }

    /**
     * 实现 ApplicationContextAware 接口,注入 Context 到静态变量中
     * @param applicationContext
     * @throws BeansException
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContext.applicationContext = applicationContext;
    }

    /**
     * 断言 Context 已经注入
     */
    private static void assertContextInjected() {
        Validate.validState(applicationContext != null, "applicationContext 属性未注入,请在 spring-context.xml 配置中定义 SpringContext");
    }
}

还需要在 spring-context.xml 配置文件中装配 <bean id="springContext" class="com.luxiu.beyond.shop.commons.context.SpringContext" />;

注意:请将该 Bean 放在配置顶部,否则使用时会报错

POM

需要在 pom.xml 中增加 org.apache.commons:commons-lang3 依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.5</version>
</dependency>

附:完整的 POM 文件

截止目前所学知识点,完整的 pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.luxiu.beyond.shop</groupId>
    <artifactId>shop</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <dependencies>
        <!-- Spring Begin -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.17.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>4.3.17.RELEASE</version>
        </dependency>
        <!-- Spring End -->

        <!-- Servlet Begin -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Servlet End -->

        <!-- Log Begin -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <!-- Log End -->

        <!-- Commons Begin -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
        <!-- Commons End -->
    </dependencies>
</project>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值