同步资源与事务 (Synchronizing Resources with Transactions)
现在你应该已经清楚如何创建不同的事务管理器,以及它们如何与需要同步到事务的相关资源相关联(例如 DataSourceTransactionManager 关联到 JDBC DataSource,HibernateTransactionManager 关联到 Hibernate SessionFactory 等)。本节描述应用程序代码(直接或间接地,通过使用 JDBC、Hibernate 或 JPA 等持久化 API)如何确保正确地创建、重用和清理这些资源。本节还讨论了如何通过相关的 TransactionManager(可选地)触发事务同步。
高层同步方法
首选的方法是使用 Spring 的最高层模板化持久化集成 API,或者使用带有事务感知(transaction-aware)工厂 Bean 或代理的原生 ORM API,来管理原生的资源工厂。这些事务感知解决方案内部处理了资源的创建和重用、清理、可选的资源事务同步以及异常映射。因此,用户的数据访问代码无需处理这些任务,而可以纯粹专注于非样板式的持久化逻辑。通常,你可以使用原生的 ORM API,或者对于 JDBC 访问,采取使用 JdbcTemplate 的模板方法。这些解决方案在参考文档的后续章节中会有详细介绍。
底层同步方法
在较低层级上,存在诸如 DataSourceUtils(用于 JDBC)、EntityManagerFactoryUtils(用于 JPA)、SessionFactoryUtils(用于 Hibernate)等类。当你希望应用程序代码直接处理原生持久化 API 的资源类型时,你会使用这些类来确保获取到由 Spring 框架管理的正确实例、确保事务(可选地)得到同步,并且将过程中发生的异常正确映射到一致的 API。
例如,在 JDBC 的情况下,不再使用传统的从 DataSource 调用 getConnection() 方法的方法,而是改用 Spring 的 org.springframework.jdbc.datasource.DataSourceUtils 类,如下所示:
Connection conn = DataSourceUtils.getConnection(dataSource);如果现有事务已经有一个与之同步(链接)的连接,则返回该实例。否则,该方法调用会触发创建一个新连接,该连接会(可选地)同步到任何现有事务,并可供同一事务中的后续重用。如前所述,任何 SQLException 都会被包装在 Spring 框架的 CannotGetJdbcConnectionException 中,这是 Spring 框架非检查型 DataAccessException 类型层级中的一种。这种方法比单纯从 SQLException 中获取信息更丰富,并确保了跨数据库、甚至跨不同持久化技术的移植性。
这种方法在没有 Spring 事务管理的情况下也能工作(事务同步是可选的),因此无论你是否使用 Spring 的事务管理,都可以使用它。
当然,一旦你使用了 Spring 的 JDBC 支持、JPA 支持或 Hibernate 支持,你通常就不会再想使用 DataSourceUtils 或其他辅助类了。因为你会更乐于通过 Spring 抽象来工作,而不是直接使用相关的 API。例如,如果你使用 Spring 的 JdbcTemplate 或 jdbc.object 包来简化 JDBC 的使用,正确的连接获取过程会在后台自动发生,你无需编写任何特殊代码。
TransactionAwareDataSourceProxy
在最底层存在 TransactionAwareDataSourceProxy 类。这是目标 DataSource 的代理,它包装了目标 DataSource,以增加对 Spring 管理事务的感知。在这方面,它类似于 Jakarta EE 服务器提供的事务性 JNDI DataSource。
你几乎永远不需要也不应该使用这个类,除非必须调用现有的代码,并且该代码必须传入一个标准的 JDBC DataSource 接口实现。在这种情况下,虽然该代码是可以运行的,但它也能参与到 Spring 管理的事务中。对于新编写的代码,你应该使用前面提到的更高层抽象。
补充教学
1. 资源同步背后的“黑盒”:ThreadLocal
Spring 是如何保证在同一个事务方法里,多次调用 DAO 获取的是同一个数据库连接?
关键在于 TransactionSynchronizationManager。
- 当事务开启时,Spring 会从连接池获取一个连接,并将其对应的“连接持有者” (Connection Holder) 绑定到当前线程的
ThreadLocal中。 - 后续无论是
JdbcTemplate还是DataSourceUtils.getConnection(),都会先去ThreadLocal查看是否有已绑定的连接。 - 同步 (Synchronization):在事务提交或回滚后,Spring 会负责从
ThreadLocal中解绑并关闭连接。
2. DataSourceUtils 存在的意义:原生 API 的“救命稻草”
如果你因为某些特殊原因(例如需要调用一个极老的遗留库,或者使用底层数据库厂商的特定 API)不得不操作原生 Connection 对象:
- 错误示范:
dataSource.getConnection()。这会从池里拿一个新连接,它不受当前 Spring 事务控制,也不会自动关闭。 - 正确示范:
DataSourceUtils.getConnection(dataSource)。它会帮你检查当前是否有事务,如果有,就给你那个事务正在用的连接;如果没有,它会给一个新连接,但在你由于缺乏@Transactional而没法自动管理时,它还能提供一致的异常转译。
3. 解读 TransactionAwareDataSourceProxy
这个代理模式通常只出现在集成场景中。
场景举例:你有一段使用了 10 年的老代码,它写死了只接收一个 javax.sql.DataSource 对象,并在内部直接调用 conn.close()。
- 如果你传个普通的
HikariDataSource进去,事务会乱套,因为老代码会提前关闭连接。 - 如果你传个
TransactionAwareDataSourceProxy包装后的数据源,当老代码调用close()时,代理会检查:“哦,这个连接还在 Spring 事务里呢,我现在不能真的关掉它,我先留着。等事务提交了,我再关。”
4. 黄金原则:尽可能留在高层
在 99% 的 Spring 项目中,你都不应该看到 DataSourceUtils 或 TransactionAwareDataSourceProxy。
- JDBC:用
JdbcTemplate。 - JPA:用
JpaRepository或EntityManager。 - MyBatis:用
mybatis-spring提供的集成。 只要留在这些框架内,Spring 就会自动帮你处理好所有的资源同步和生命周期管理。