JavaEE简单示例——Spring的事务管理

文章介绍了Spring框架中的事务管理机制,包括事务的概念、PlatformTransactionManager接口、TransactionDefinition接口的事务属性,如隔离级别和传播行为。通过XML配置文件展示了如何设置事务管理,包括数据源、JdbcTemplate、事务管理器的配置,以及事务管理器如何作为切面增强到方法中。文章还提供了测试示例,展示了事务在异常情况下如何保持数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简单介绍:

在之前我们介绍Spring框架中,提到了有一个模块叫做数据访问与集成(Data Access/Intergration),而在这个模块中,除了jdbc之外,还有一个比较重要的地方,也是我们学习这一模块必须掌握的地方,那就是事务管理(Transaction)。

首先来认识一下什么是事务,事务是数据库中的一个概念,主要是为了在我们执行多条SQL语句的时候,如果中间有一条 语句出现了错误,则之前执行成功的语句全都不算数,全部操作都撤销,最主要的是如果有对于数据的操作的部分也全部都 回到事务开始之前的状态。 这个多条SQL语句的集合就被称之为事务,而事务的管理主要就是负责在多条SQL语句执行的时候,如果这时候出现了 错误,就帮我们设置到事务开始之前的状态。

然后我们来介绍一下在Spring中的事务管理。Spring提供了专门用于事务管理的API,可以很方便的帮助我们进行 事务的管理。我们学习这些API是为了更好的理解如何在XML的配置文件里编写我们的事务管理器,毕竟我们之后主要 是使用XML配置文件的方式以及注解这种方式来声明事务的管理。

使用方法:

首先是PlatformTransactionManager接口,译名叫做平台事务管理器。 这个接口的主要作用是用来进行事物的管理的,接口中包含有获取事务状态的方法,提交事务的方法和回滚事务的方法 在实际的医用中,Spring事务的管理实际是由集体的持久化技术完成的,而PlatformTransactionManager只是提供 一个统一的抽象方法。 比如在Spring中就为Spring JDBC和Mybatis等依赖于DataSource(数据源)持久化技术提供了实现类 DataSourcePlatformTransactionManager,这样我们就可以通过这个类来管理我们的事务。

然后是TransactionDefinition接口,这个接口定义了用于描述事务相关的常量,其中包括事务的隔离级别,事务的传播行为 事务的超时时间以及是否为只读。 事务的隔离级别就是指事务之间的隔离程度,比如我在执行某个事务的时候,我已经开始对数据进行修改了,但是这个 修改并没有提交到数据库中,也就是说数据库中的数据并没有开始被修改,而另一个事务可不可以来访问我没有提交的数据。 对于事务的隔离级别,Spring提供了五种固定的常量,其中比较常用的有: ISOLATION_REPEATBLE_READ:读已提交,一个事务修改的数据提交后才能被另一个事务读取,可以避免脏读 ISOLATION_REPEATABLE_READ:默认属性,允许重复读,可以避免脏读,资源消耗上升 还有就是关于事务的传播行为,事务的传播行为指的就是指当我们进行业务嵌套的时候,外层的业务和内层的业务是进行单独的业务管理 还是合并成为一个业务进行管理,其中常用的属性有: PROPAGATION_REQUIRED:默认值,如果已经存在事务,则加入该事务;如果没有事务,则创建一个新的事务。 表示将合并嵌套事务。 PROPAGATION_REQUIRS_NEW:创建一个新的事务,如果已经存在一个事务,则将旧的事务挂起。表示只能执行一个事务 管理,并且在进入内层的事务管理的时候将外层的事务暂时挂起,知道完成内层的事务管理之后在继续执行外层的事务管理。

还有一些其他的设置项,但是这些设置项我们一般不会做更改,我们只需要知道有这些配置项即可 事务的超时时间,指的是如果事务的执行超过了我们预定的时间,则回滚事务。 是否为只读,当事务为只读的时候,该事务不具有向数据库写入数据的能力,如果在只读属性的事务中修改数据 会引发异常,有助于提升性能。

还有最后的TransactionStatus,这个属性主要用于编程式的事务管理的时候用于界定事务的状态,在我们的 声明式事务管理并不会用到。

代码实现:

在了解了这些基础知识之后,我们就可以开始进入我们的编写配置文件的过程了 首先,如果我们是第一次使用XML配置文件的方式去配置声明式的事务管理,那么我们首先要在pom文件中引入依赖项

<!--        spring的核心依赖坐标-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.21.RELEASE</version>
        </dependency>
<!--        spring的Jdbc包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.21.RELEASE</version>
        </dependency>
<!--        spring的用于事务管理的依赖坐标 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.21.RELEASE</version>
        </dependency>
<!--        用于解析切入点表达式-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.7.2</version>
        </dependency>

然后在我们的Bean管理XML配置文件中添加事务管理有关的约束

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

继续在Bean管理XML配置文件中添加dataSource类,我们以后就叫他数据源类,洋文不喜欢,在数据源类中我们需要添加四个属性

分别是driverClassName: 表示数据库驱动的类

url:表示数据库的链接地址

username:表示数据库的用户名

password:表示数据库用户的密码

这都是我们之前登录数据库的时候所必须的参数

<!--    创建数据源对象-->
    <bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/jdbc"/>
        <property name="username" value="root"/>
        <property name="password" value="@gaoyang1"/>
    </bean>

jdbcTemplate类,用于定义模板类,因为这个类中有我们需要的对于数据库的操作的方法,并且我们需要将数据源类通过依赖注入的方式注入进模板类中的dabaSource属性中

<!--    创建模板类对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dateSource"/>
    </bean>

自定义数据库操作类,这个类是我们自己编写的,这个类中必须要包含一个属性,属性的类型必须是模板类的类型,然后我们将我们刚才配置的模板类通过依赖注入的方式注入到我们的自定义数据库操作类的jdbcTemplat属性中 以及我们的DataSourceTransactionManager事务管理类,这个类就是我们之前介绍的Spring基于dataSource实现的数据库管理类,表示这个类也是依赖于数据源的,所以我们也需要将之前的数据源类通过依赖注入的方式注入到事务管理类的dataSource属性中

<!--    创建自定义数据库操作类的对象,最终我们只需要或者这个对象就可以了-->
    <bean id="manipulatingDatabase" class="Semester_4.SpringJDBC.TransactionManagement.ByNote.ManipulatingDatabase">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
package Semester_4.SpringJDBC.TransactionManagement.ByConfigurationFile;

import org.springframework.jdbc.core.JdbcTemplate;

public class ManipulatingDatabase {
    private JdbcTemplate jdbcTemplate;

    @Override
    public String toString() {
        return "ManipulatingDatabase{" +
                "jdbcTemplate=" + jdbcTemplate +
                '}';
    }

    public JdbcTemplate getJdbcTemplate() {
        return jdbcTemplate;
    }

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public ManipulatingDatabase(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public ManipulatingDatabase() {
    }
//    模拟一个转账的事务,在Java中,事务的封装是通过方法来完成的,即多条SQL语句放在同一个方法中,这个方法就成为了一个事务
    public void Transfer(String payer , String payee , int money){
//        定义数据的调整的SQL语句
        String SQL1 = "update user set money = money - ? where name = ?";
        String SQL2 = "update user set money = money + ? where name = ?";
        this.jdbcTemplate.update(SQL1,money,payer);
//        模拟在执行事务的时候,如果中间有一个异常发生了
        int i = 1/0;
        this.jdbcTemplate.update(SQL2,money,payee);
    }
}

 这个类中除了要定义JDBC操作类的对象,还要创建一个封装了事务的方法

基于XML配置文件的事务管理其实就是将我们的事务管理类当作切面,增强到我们已有的事务方法中 所以说,我们可以将事务管理类当作是一个切面,只不过这个切面在使用之前需要进行一些特殊的配置

<!--    然后开始进行对事务管理的配置-->
    <tx:advice id="interceptor" transaction-manager="transactionManager">
        <tx:attributes>
<!--            name并不是名字的意思,而是要指定匹配的方法名-->
<!--            表示这个事务是可读可写的,使用默认的隔离级别,使用默认的传播行为-->
            <tx:method name="*" isolation="DEFAULT" read-only="false" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--        然后将事务管理器作为切面将增强插入到已有的方法中-->
    <aop:config>
<!--        配置切入点,切入点就是我们的转账方法-->
        <aop:pointcut id="pointcut" expression="execution(* Semester_4.SpringJDBC.TransactionManagement.ByConfigurationFile.ManipulatingDatabase.*(..))"/>
<!--        配置切面,因为我们的切入点和切面都已经写好了,并且因为我们的切面是一个事务管理器,所以他也自己封装了和增强和切入点的关系,所我们只需要去引入即可-->
        <aop:advisor advice-ref="interceptor" pointcut-ref="pointcut"/>
    </aop:config>

我们所有的对于事务管理类的操作全部都是在tx:advice根标签下面的,这个根标签有两个属性,id就是唯一标识 transaction_manager属性的值就是我们刚才的事务管理类的id的值,表示引用这个类进行事务的管理 在根标签下面,就是tx:attributes标签,这个标签主要用于包含多个tx:methods,而tx:methods才是用来设置 事务管理的标签,主要用于设置我们之前说过的,比如隔离级别,传播行为之类的这些属性

其中tx:methods的重要的属性有以下几个:

name:用于指定方法名的匹配模式,这个属性是必选属性

propagation:指定事务的传播范围

isolation:指定事务的隔离级别

read-only:指定事务是否为只读

然后我们还需要定义一个用来封装查询结果的类,并且将其注册到IoC中:

<!--    创建结果集映射类的bean-->
    <bean id="user" class="Semester_4.SpringJDBC.TransactionManagement.ByNote.user"/>
package Semester_4.SpringJDBC.TransactionManagement.ByConfigurationFile;

public class user {
    private int id;
    private String name;
    private int money;

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

    public int getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public user(int id, String name, int money) {
        this.id = id;
        this.name = name;
        this.money = money;
    }

    public user() {
    }
}

 最后,我们总结一下我们Bean管理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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:bean="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--    创建数据源对象-->
    <bean id="dateSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/jdbc"/>
        <property name="username" value="root"/>
        <property name="password" value="@gaoyang1"/>
    </bean>
    <!--    创建模板类对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dateSource"/>
    </bean>
    <!--    创建自定义数据库操作类的对象,最终我们只需要或者这个对象就可以了-->
    <bean id="manipulatingDatabase" class="Semester_4.SpringJDBC.TransactionManagement.ByNote.ManipulatingDatabase">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
<!--    创建结果集映射类的bean-->
    <bean id="user" class="Semester_4.SpringJDBC.TransactionManagement.ByNote.user"/>
<!--    创建事务管理类的bean-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--        因为这个类需要依赖与数据源,所以将数据源对象使用依赖注入的方式进行赋值-->
        <property name="dataSource" ref="dateSource"/>
    </bean>

<!--    然后开始进行对事务管理的配置-->
    <tx:advice id="interceptor" transaction-manager="transactionManager">
        <tx:attributes>
<!--            name并不是名字的意思,而是要指定匹配的方法名-->
<!--            表示这个事务是可读可写的,使用默认的隔离级别,使用默认的传播行为-->
            <tx:method name="*" isolation="DEFAULT" read-only="false" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <!--        然后将事务管理器作为切面将增强插入到已有的方法中-->
    <aop:config>
<!--        配置切入点,切入点就是我们的转账方法-->
        <aop:pointcut id="pointcut" expression="execution(* Semester_4.SpringJDBC.TransactionManagement.ByConfigurationFile.ManipulatingDatabase.*(..))"/>
<!--        配置切面,因为我们的切入点和切面都已经写好了,并且因为我们的切面是一个事务管理器,所以他也自己封装了和增强和切入点的关系,所我们只需要去引入即可-->
        <aop:advisor advice-ref="interceptor" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

 然后创建测试类并进行测试:

package Semester_4.SpringJDBC.TransactionManagement.ByConfigurationFile;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;

import java.util.List;

public class test {
    public static void main(String[] args) {
//        创建IoC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("TeansactionManager.xml");
//        获取自定义数据库操作类的对象
        ManipulatingDatabase md = (ManipulatingDatabase)ac.getBean("manipulatingDatabase");
//        创建结果集封装类
        RowMapper<user> mapper = new BeanPropertyRowMapper<>(user.class);
//        输出修改之前的代码
        System.out.println("更改之前的数据:");
        List<user> queryBefore = md.getJdbcTemplate().query("select * from user", mapper);
        for(user u : queryBefore){
            System.out.println(u.toString());
        }
//        调用方法,对数据库中的多条数据进行修改
        md.Transfer("张三","李四",100);
//        输出修改后的结果
        List<user> queryAfter = md.getJdbcTemplate().query("select * from user", mapper);
        System.out.println("更改之后的数据:");
        for(user u : queryAfter){
            System.out.println(u.toString());
        }
    }
}

运行结果:

首先,我们来看数据库中准备的数据:

数据库中有三个人,并且此时他们的money都是300,现在我们要实现一个需求,就是让张三的钱转给李四一百。

正常我们的逻辑都是张三的钱会减少一百,李四的钱会增加一百,我们就按照这个逻辑开始编写我们的方法:

//    模拟一个转账的事务,在Java中,事务的封装是通过方法来完成的,即多条SQL语句放在同一个方法中,这个方法就成为了一个事务
    public void Transfer(String payer , String payee , int money){
//        定义数据的调整的SQL语句
        String SQL1 = "update user set money = money - ? where name = ?";
        String SQL2 = "update user set money = money + ? where name = ?";
        this.jdbcTemplate.update(SQL1,money,payer);
//        模拟在执行事务的时候,如果中间有一个异常发生了
        int i = 1/0;
        this.jdbcTemplate.update(SQL2,money,payee);
    }

我们正常的调用方法对数据进行更新,但是如果在更新的时候出现了一个错误,会导致方法执行失败,那么这时候,如果我们不做事务管理,就会发生下面的这种情况:

方法报错,程序终止,但是数据库中的数据发生了变化: 

 因为我们在方法失败之前,我们已经执行了一条SQL语句,并且向数据库中写入了数据,这就会导致我们的数据发生错误,现在我们添加事务管理:

正常还是模拟报错,方法终止,我们继续来看数据库中的数据:

 数据库中的数据没有发生变化,因为方法报错,事务管理会检测,只有当整个方法成功执行之后,才会将数据的修改写入到数据库中,完成数据的持久化操作,所以,如果当方法中出现异常导致方法无法顺利的执行完成,事务管理器是不会让数据的修改写入到数据库中的,这就是事务管理的基本逻辑。

注意点:

关于本章的知识点主要在如事务管理器的配置,以及如何将事务管理与需要被事务管理的方法联系起来,主要是使用到了切面增强的方法。

### Java EE 中捕获异常后事务仍回滚的原因 在 Java EE 的开发环境中,`@Transactional` 注解用于控制事务的行为。然而,在某些情况下,即使已经通过 `try-catch` 捕获了异常,事务仍然可能回滚。这主要是由于以下几个原因: #### 1. 默认回滚策略 `@Transactional` 注解默认会在运行时异常(`RuntimeException`)或错误(`Error`)发生时触发事务回滚[^2]。如果程序中的异常属于这些类型,并且未被重新抛出,则 Spring 或其他框架可能会误判该异常已被处理完毕而继续执行后续逻辑。 #### 2. AOP 动态代理机制限制 事务管理依赖于面向切面编程(AOP),其核心原理是通过动态代理拦截方法调用并应用事务规则。当异常被捕获但未传播至外部时,AOP 切面无法感知到内部发生的异常状况,从而导致事务未能按预期完成回滚操作[^3]。 #### 3. 非受检异常的影响 只有非受检异常(即继承自 `RuntimeException` 和 `Error` 的类)能够激活默认的回滚行为。因此,即便开发者手动捕获了一个可恢复的已知问题并通过日志记录下来,只要它本质上是非受检性质的,系统依旧会尝试去撤销当前正在进行的操作序列以便保持一致性[^1]。 --- ### 解决方案 针对以上提到的各种情形,可以采取如下措施来确保期望的结果得以实现: #### 方法一:显式声明需回滚的具体异常类型 可以通过设置 `rollbackFor` 参数指定哪些特定类型的异常应该引起事务回卷动作的发生。例如修改原有代码片段如下所示: ```java @Transactional(rollbackFor = Exception.class) public Object save(User user) { ... } ``` 这样做的好处在于扩大了识别范围,使得即使是普通的 `Exception` 子类别也能成为促使整个过程终止的因素之一[^2]。 #### 方法二:重新抛出异常 另一种简单有效的方式就是在捕捉到目标事件之后再次将其释放出去供更高层决定如何应对。像下面这样的调整可以让上级组件意识到出现了不可忽视的情况进而启动相应的保护机制: ```java @RequestMapping("/save") @Transactional public Object save(User user) throws Exception{ int result = userService.save(user); try { int i = 10 / 0; } catch (ArithmeticException e){ throw new RuntimeException("Division by zero occurred",e); } return result; } ``` 这里我们将算术溢出转换成了一种更严重的条件并向上传达给调用者知道发生了什么严重的事情需要特别关注[^4]。 #### 方法三:手动控制事务边界 最后一种选择涉及更加精细的手动干预——直接操控底层连接对象的状态变化以达成精确的目的。具体做法包括但不限于显示定义何时开始以及结束一段隔离的工作单元;同时也要留意适时地还原初始配置以免影响未来请求正常运作。示例代码如下: ```java @Autowired private PlatformTransactionManager transactionManager; @RequestMapping("/saveManualTx") public void saveWithManualTransaction() { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); TransactionStatus status = transactionManager.getTransaction(def); try { // Perform business logic here... if(someConditionThatShouldCauseARollback()){ transactionManager.rollback(status); return ; } transactionManager.commit(status); }catch(Exception ex){ transactionManager.rollback(status); throw ex; // Optionally rethrow the exception after rollback. } } ``` 这种方法虽然复杂度较高但也提供了最大的灵活性允许我们完全掌控每一个细节环节直至最终成功提交或者安全撤退为止。 --- ### 总结 综上所述,理解并合理运用上述三种途径可以帮助克服因不当使用 `try-catch` 结构而导致意外回滚现象的问题。无论是调整注解参数还是改变架构设计思路都能有效地改善这一局面达到理想效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值