参考文档的这一部分涉及数据访问以及数据访问层与业务或服务层之间的交互.

本节全面详细的介绍了 Spring 的事务管理支持,然后介绍了 Spring 框架所集成的各种数据访问框架和技术.

1. 事务管理

全面的事务支持是使用 Spring 框架最有说服力的原因之一. Spring 框架为事务管理提供了一致性的抽象,具有以下优势:

  • 跨不同事务 API 的具有一致性的编程模式,例如 JAVA 事务 API(JTA) 、JDBC 和 JAVA 持久化 API(JPA) ;

  • 支持 声名式事务管理.

  • 与诸如 JTA 之类的复杂事务 API 相比, 具有更简单的 编程式事务管理

  • 与 Spring 的数据访问抽象的完美集成

以下部分描述了 Spring Framework 的特性和实现技术:

(本章还包括对最佳实践、 应用程序服务器集成常见问题的解决方案的讨论. )

1.1. Spring Framework 的事务支持模型优点

传统上,Java EE 开发者在事务管理上有两种选择: 全局事务管理或者本地事务管理,两种事务都有很大的局限性. 在接下来的两小节中会对全局事务和本地事务进行介绍,并讨论在 Spring Framework 的事务管理中如何解决全局事务和本地事务的局限性.

1.1.1. 全局事务

全局事务可以使用多个事务源,通常是关系型数据库和消息队列. 应用程序服务器通过 JTA 管理全局事务,这是一 个繁琐的 API(部分原因是它的异常模型) . 此外,JTA 的 UserTransaction 通常从 JNDI 获取,这意味着当你要使用 JTA 时,还需要使用 JNDI. 全局事务的使用限制了应用程序代码的潜在重用性,因为 JTA 只有在应用程序服务器环境中才有效.

以前,使用全局事务的首选方法是通过 EJB CMT(容器管理事务) . CMT 是一种声明式事务管理(不同于编程式事务管理) . EJB CMT 移除了与事务相关的 JNDI 查找的需求,尽管使用 EJB 需要使用到 JNDI. 它移除了大部分需要编 写 Java 代码去控制事务的需要. 致命的缺点是 CMT 与 JTA 和应用程序服务器环境相关联. 此外,仅当选择在 EJB 中实现业务逻辑时(或至少在事务性的 EJB 之后) ,它才可用. 一般来说,EJB 的负面影响之大,以至于这不是一个有吸引力的选择,尤其是在面对声明式事务管理的令人信服的替代方案时.

1.1.2. 本地事务

本地事务是资源特定的,例如与 JDBC 连接相关联的事务. 本地事务使用更简单,但是有一个明显的缺点: 它们无法跨多个事务源工作. 例如,通过 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行. 由于应用程序服务器不参与事务管理,因此无法确保跨多个资源的正确性. (值得注意的是,大多数应用程序使用单个事务资源. ) 另一个缺点 是本地事务对编程模型是侵入式的.

1.1.3. Spring Framework 的一致性编程模式

Spring 解决了全局事务和本地事务的缺点. 它使应用程序开发人员在任何环境中都使用一致的编程模式. 一次编码, 它可以在不同环境中的不同事务管理策略中使用. Spring Framework 提供了声明式和编程式事务管理. 大多数用户 更喜欢声明式事务管理,我们也建议在大多数情况下使用声明式事务管理.

在编程式事务中,开发人员可以使用 Spring Framework 的事务抽象化,它可以在任何底层事务基础结构上运行. 在更喜欢使用的声明式事务管理中,开发人员一般很少或者几乎不编写与事务相关的代码,因此,不会依赖于 Spring Framework 的事务 API 或者其他的事务 API.

你的应用程序需要事务管理吗?

Spring Framework 的事务管理支持改变了关于企业 Java 应用程序何时需要应用程序服务器的传统规则.

特别是,你完全不需要使用一个通过 EJB 进行声明式事务的应用程序服务器. 实际上,即使您的应用程序服务器具有强大的 JTA 功能,您也可以决定让 Spring Framework 的声明式事务提供比 EJB CMT 更强大的功能和更高效的编程模型.

通常,只有当应用程序需要处理跨多个资源的事务时,才需要应用程序服务器的 JTA 功能,这对许多应 用程序来说并不是必需的. 许多高端应用程序使用单个高度可伸缩的数据库(例如 Oracle RAC) . 独立的事务管理器(例如 Atomikos TransactionsJOTM) 是其他选择. 当然,您可能需要其他应用 程序服务器功能,例如 Java消息服务(JMS) 和 Java EE 连接器体系结构(JCA) .

Spring Framework 使您可以选择何时将应用程序扩展到完全加载的应用程序服务器. 使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的代码) 编写代码,并且如果您需要在全局容器管理 的事务中运行代码,则会面临大量的返工. 使用 Spring Framework,只需要更改配置文件中的一些 bean 定义(而不是代码) .

1.2. 理解 Spring Framework 的事务抽象化

Spring 事务抽象的关键是事务策略的概念. 事务策略由 TransactionManager 定义,特别是用于命令式事务管理的 org.springframework.transaction.PlatformTransactionManager 接口和用于响应式事务管理的 org.springframework.transaction.ReactiveTransactionManager 接口. 以下清单显示了 PlatformTransactionManager API 的定义: 如下所示:

public interface PlatformTransactionManager extends TransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

尽管您可以从应用程序代码中以编程方式 使用它, 但这是一个 service provider interface (SPI) . 因为 PlatformTransactionManager 是一个接口,所以可以根据需要轻松地 mock 或者 stubbed. 它与查询策略无关,例如 JNDI. PlatformTransactionManager 实现的定义与 Spring Framework IoC 容器中的其他对象(或 bean ) 相同. 仅使用此优势使 Spring Framework 事务成为一种有价值的抽象,即使您使用 JTA 也是如此. 与直接使用JTA 相比,您可以更轻松地测试事务代码.

同样,为了与 Spring 的理念保持一致,可以取消选中任何 PlatformTransactionManager 接口的方法抛出的 TransactionException(即,它扩展了 java.lang.RuntimeException 类) . 事务基础故障总是致命的. 在极少数情况下,应用程序代码实际上可以从事务失败中恢复,应用程序开发人员仍然可以选择捕获并处理 TransactionException. 重点是开发人员不会被强迫这样做.

getTransaction(..) 方法返回 TransactionStatus 对象,它具体取决于 TransactionDefinition 参数. 如果当前调用的堆栈中存在匹配的事务,则返回的 TransactionStatus 可能表示新事务或可表示现有事务. 后一种情况的含义是,与 Java EE 事务上下文一样,TransactionStatus 与执行线程相关联.

从 Spring Framework 5.2 开始,Spring 还提供了一个抽象的事务管理用于使用响应式类型或 Kotlin 协程的响应式应用程序. 下列清单显示了由 org.springframework.transaction.ReactiveTransactionManager 定义的事务策略:

public interface ReactiveTransactionManager extends TransactionManager {

    Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;

    Mono<Void> commit(ReactiveTransaction status) throws TransactionException;

    Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

响应式事务管理是一个服务提供接口(SPI),尽管您可以从应用程序代码中以 编程 方式使用它. 但由于 ReactiveTransactionManager 是接口,因此可以根据需要轻松对其进行 mock 或 stubbed.

TransactionDefinition 接口规定:

  • 传播: 通常,在事务范围内执行的所有代码都在该事务中运行. 但是,如果在事务上下文已存在时执行事务方法,则可以指定代码的执行行为. 例如,代码可以继续在现有事务中运行(常见情况) ,或者可以暂停现有事务并创建新事务. Spring 提供了 EJB CMT 熟悉的所有事务 传播选项. 要阅读有关 Spring 中事务传播的语义,请参阅事务传播.

  • 隔离: 此事务与其他事务的工作隔离的程度. 例如,此事务是否可以看到来自其他事务的未提交的写入?

  • 超时: 此事务在超时并由底层事务基础结构自动回滚之前运行多长时间.

  • 只读状态: 当你的代码只能读取但不能修改数据时,你可以使用只读事务. 在某些情况下,只读事务可能是一种有用的优化,例如当您使用 Hibernate 时.

这些设置反映了标准的事务概念. 如有必要,请参阅讨论事务隔离级别和其他核心事务概念的资源. 理解这些概念对于使用 Spring Framework 或其他任何事务管理解决方案至关重要.

TransactionStatus 接口为事务代码提供了一种控制事务执行和查询事务状态的简单方法. 这些概念应该被熟知的,因为它们对于所有事务 API 都是通用的. 以下清单显示了 TransactionStatus 接口:

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

    @Override
    boolean isNewTransaction();

    boolean hasSavepoint();

    @Override
    void setRollbackOnly();

    @Override
    boolean isRollbackOnly();

    void flush();

    @Override
    boolean isCompleted();
}

无论您是在 Spring 中选择声明式还是程序化事务管理,定义正确的 TransactionManager 实现都是绝对必要的. 您通常通过依赖注入来定义此实现.

TransactionManager 实现通常需要了解它们工作的环境: JDBC,JTA,Hibernate 等. 以下示例显示了如何定义本地 PlatformTransactionManager 实现(在本例中,使用普通 JDBC) .

你可以通过创建一个类似与以下内容的 bean 来定义一个JDBC DataSource:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

然后,在相关的 PlatformTransactionManagerbean 的定义中引用 DataSource 的定义. 它应该类似与以下定义:

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果在 Java EE 容器中使用 JTA,则使用通过 JNDI 获得的容器 DataSource 以及Spring的 JtaTransactionManager. 以下示例显示了JTA和JNDI查找版本:

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

    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>

    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />

    <!-- other <bean/> definitions here -->

</beans>

JtaTransactionManager 不需要了解 DataSource(或任何其他特定资源) ,因为它使用容器的全局事务管理基础结构.

dataSource bean 的前面定义使用 jee 命名空间中的 <jndi-lookup/> 标签. 有关更多信息,请参阅参阅 JEE 架构.
如果您使用 JTA,那么无论您使用何种数据访问技术,无论是 JDBC,Hibernate JPA 还是任何其他支持的技术, 您的事务管理器定义都应该看起来相同. 这是因为 JTA 事务是全局事务,可以登记任何事务资源

在所有这些情况下,应用程序代码不需要更改.您可以仅通过更改配置来更改事务的管理方式,即使该更改意味着从本地事务转移到全局事务,反之亦然. .

1.2.1. Hibernate Transaction 设置

如以下示例所示,您还可以轻松使用 Hibernate 本地事务.在这种情况下,您需要定义一个 Hibernate LocalSessionFactoryBean,您的应用程序 代码可以使用它来获取 Hibernate Session 实例.

DataSource bean 定义和前面的本地 JDBC 示例一样, 因此,在以下示例中未显示。

如果 DataSource(由任何非 JTA 事务管理器使用) 通过 JNDI 查找并由 Java EE 容器管理,则它应该是非事务性的,因为 Spring Framework (而不是Java EE容器) 管理事务.

在这种情况下,txManager bean 是 HibernateTransactionManager 类型. 与 DataSourceTransactionManager 需要对 DataSource 的引用一样, HibernateTransactionManager 需要应用 SessionFactory 的引用. 以下示例声明了 SessionFactorytxManager bean:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>

如果使用 Hibernate 和 Java EE 容器管理的 JTA 事务,则应使用与之前的 JDBC JTA 示例相同的 JtaTransactionManager,如以下示例所示,另外,建议让 Hibernate 通过它的 JTA 了解事务协调器,可能还有其连接释放配置:

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
            hibernate.transaction.coordinator_class=jta
            hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

或者,您可以将 JtaTransactionManager 传递到 LocalSessionFactoryBean 中. 强制执行相同的操作

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
    <property name="jtaTransactionManager" ref="txManager"/>
</bean>

<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

1.3. 事务和资源同步

现在应该清楚如何创建不同的事务管理器以及它们如何链接到需要与事务同步的相关资源(例如, 将 DataSourceTransactionManager 连接到JDBC DataSource,HibernateTransactionManager 连接到 Hibernate SessionFactory 等等) . 本节描述应用程序代码(直接或间接使用诸如 JDBC,Hibernate 或 JPA 之类的持久化 API) 如何确保正确创建,重用和清理这些资源. 本节还讨论了如何(可选) 通过相关的 TransactionManager 触发事务同步.

1.3.1. 高等级同步方法

首选方法是使用 Spring 最高级别的基于模板的持久化集成 API,或者将本地 ORM API 与事务感知工厂 bean 或代理一起使用, 以管理本地资源工厂. 这些事务感知解决方案在内部处理资源创建、重用和清理,资源的可选事务同步以及异常映射. 因此,用户数据访问代码不必解决这些任务,而是可以完全专注于非样板持久化逻辑. 通常,您使用本地 ORM API 或使 用模板方法通过使用 JdbcTemplate 进行 JDBC 访问. 这些解决方案将在本参考文档的后续章节中详细介绍.

1.3.2. 低等级同步方法

诸如 DataSourceUtils(用于 JDBC) ,EntityManagerFactoryUtils(用于JPA) ,SessionFactoryUtils(用于 Hibernate) 等的类存在于较低级别. 当您希望应用程序代码直接处理本地持久化 API 的资源类型时, 您可以使用这些类来确保获得正确的 Spring Framework 托管实例、同步事务(可选) ,并且在此过程中发生的异常是被正确映射到一致的 API.

例如,在使用 JDBC 的情况下,您可以使用 Spring 的 org.springframework.jdbc.datasource.DataSourceUtils 类, 而不是传统的 JDBC 方法来调用 DataSource 上的 getConnection() 方法,如下所示:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果现有事务已经与其同步(链接) 了连接,则返回该实例. 否则,方法调用会触发新连接的创建,该连接(可选) 与任何现有事务同步, 并可在随后的同一事务中重用. 如前所述,任何 SQLException 都包含在 Spring Framework CannotGetJdbcConnectionException 中, Spring Framework 是未经检查的 DataAccessException 类型的层次结构之一. 这种方法为您提供了比从 SQLException 轻松获得的更多信息, 并确保跨数据库甚至跨不同持久化技术的可移植性.

这种方法在没有 Spring 事务管理(事务同步是可选的) 的情况下也可以工作,因此无论您是否使用 Spring 进行事务管理,都可以使用它.

当然,一旦您使用了 Spring 的 JDBC 支持、JPA 支持或 Hibernate 支持,您通常不希望使用 DataSourceUtils 或其他帮助类, 因为您通过 Spring 抽象化工作比直接使用相关 API 更快乐. 例如,如果您使用 Spring JdbcTemplatejdbc.object 包来简化 JDBC 的使用, 则在后台进行正确的连接检索,您无需编写任何特殊代码.

1.3.3. TransactionAwareDataSourceProxy

最低级别存在于 TransactionAwareDataSourceProxy 类. 这是目标 DataSource 的代理,它包装目标 DataSource 以添加对 Spring 管理的事务的感知. 在这方面,它类似于 Java EE 服务器提供的事务性 JNDI 数据源.

您几乎从不需要或不想使用此类,除非必须调用现有代码并传递标准 JDBC DataSource 接口实现. 在这种情况下,此代码可能可用但参与 Spring 管理的事务. 您可以使用前面提到的更高级别的抽象来编写新代码.

1.4. 声名式事务管理

大多数 Spring Framework 用户选择声明式事务管理. 此选项对应用程序代码的影响最小,因此与非侵入式轻量级容器的理想最为一致.

随着 Spring 面向切面编程(AOP) 的使用,Spring Framework 的声明式事务管理成为可能. 但是,由于事务切面代码随 Spring Framework 发行版一起提供, 并且提供一些样板代码,因此您可以不需要理解 AOP 概念也能使用此代码.

Spring Framework 的声明式事务管理类似于 EJB CMT,因为您可以将事务行为(或缺少它) 指定为单个方法级别. 如有必要,您可以在事务上下文中进行 setRollbackOnly() 调用. 两种类型的事务管理之间的区别是:

  • 与绑定到 JTA 的 EJB CMT 不同,Spring Framework 的声明式事务管理适用于任何环境. 它可以通过调整配置文件 使用 JDBC、JPA 或 Hibernate 来处理 JTA 事务或本地事务.

  • 您可以将 Spring Framework 声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类.

  • Spring Framework 提供了声明式 回滚规则,这是一个 EJB 没有的功能. 编程式事务和声明式事务都提供了对回滚规则的支持.

  • Spring Framework 允许您使用 AOP 自定义事务行为. 例如,您可以在事务回滚的情况下插入自定义行为. 您还可以添加任意通知以及事务性的通知. 使用 EJB CMT,除了使用 setRollbackOnly() 之外,您无法影响容器的事务管理.

  • Spring 框架像高端应用程序服务器那样,不支持跨远程调用传播事务上下文. 如果您需要此功能, 我们建议您使用 EJB. 但是,在使用此类功能之前请仔细考虑,因为通常情况下,人们不希望事务跨越远程调用.

回滚规则的概念很重要. 它们允许您指定哪些异常(和 throwables) 应该被自动回滚. 您可以在配置中以声明方式指定它, 而不是在 Java 代码中. 因此,尽管您仍然可以在 TransactionStatus 对象上调用 setRollbackOnly() 来回滚当前事务, 但大多 数情况下您可以指定 MyApplicationException 必须始终触发回滚的规则. 此选项的显着优势是业务对象不依赖于事务基础结构. 例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API.

虽然 EJB 容器默认行为会自动回滚系统异常(通常是运行时异常) 上的事务,但 EJB CMT 不会在应用程序异常(即 java.rmi.RemoteException 以外的已检查异常) 上自动回滚事务. 虽然声明式事务管理的 Spring 默认行为遵循 EJB 约定(回滚仅在未经检查的异常时自动进行) ,但自定义此行为通常很有用.

1.4.1. 理解 Spring Framework 的声明式事务的实现

仅仅通过 @Transactional 注解是不够的,将 @EnableTransactionManagement 添加到您的配置中,并期望您了解它是如何工作的. 为了更深入地理解,本节解释了在发生与事务相关的问题时 Spring Framework 的声明式事务基础结构的内部工作原理.

关于 Spring Framework 的声明式事务支持,最重要的概念是通过 AOP 代理启用此支持,并且事务性的通知由元数据(当前基于 XML或基于注解) 驱动. AOP 与事务元数据的组合产生一个 AOP 代理,该代理使用 TransactionInterceptor 和适当的 TransactionManager 实现来驱动方法调用事务.

Spring AOP 在AOP 部分介绍.

Spring Framework 的 TransactionInterceptor 为命令式和响应式编程模型提供事务管理. 拦截器通过检查方法返回类型来检测所需的事务管理风格. 返回诸如 Publisher 或 Kotlin Flow(或它们的子类型)之类的响应式的方法符合响应式事务管理的条件. 所有其他返回类型(包括 void)都将代码路径用于命令式事务管理.

@Transactional 通常与由 PlatformTransactionManager 事务管理的线程绑定在一起使用 ,将事务暴露给所有内部正在执行数据访问操作的线程 . 注意: 在方法内部不会 not 传播到新启动的线程.

ReactiveTransactionManager 管理的响应式事务使用 Reactor 上下文而不是线程局部属性. 所以,所有执行数据访问的操作需要在相同的响应式 pipeline 中的相同 Reactor 上下文中执行.

下图显示了在事务代理上调用方法的概念视图:

tx

1.4.2. 声明式事务实现的例子

请考虑以下接口及其实现. 此示例使用 FooBar 类作为占位符,以便您可以专注于事务使用,而无需关注特定的 domain 模型. 出于此示例的目的,DefaultFooService 类在每个实现的方法的主体中抛出 UnsupportedOperationException 异常. 该行为允许您查看被创建的事务,然后回滚以响应 UnsupportedOperationException 实例. 以下清单显示了 FooService 接口的详情:

Java
public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}
Kotlin
interface FooService {

    fun getFoo(fooName: String): Foo

    fun getFoo(fooName: String, barName: String): Foo

    fun insertFoo(foo: Foo)

    fun updateFoo(foo: Foo)
}

下面的例子展示了以上接口的实现方式:

Java
public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }

    override fun insertFoo(foo: Foo) {
        // ...
    }

    override fun updateFoo(foo: Foo) {
        // ...
    }
}

假设 FooService 接口的前两个方法 getFoo(String)getFoo(String, String), 必须在具有 read-only 的事务上下文中执行,并且方法 insertFoo(Foo)updateFoo(Foo),必须 在具有 read-write 的事务上下文中执行. 以下配置将在接下来的几段中详细说明:

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

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings (see below) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- ensure that the above transactional advice runs for any execution
        of an operation defined by the FooService interface -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- don't forget the DataSource -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <!-- similarly, don't forget the TransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>

查看以上配置,它假定你要创建一个具有事务属性的服务对象,fooService bean, 要应用的事务语义在 <tx:advice/> 标签中定义. <tx:advice/> 中的定义为: 所有方法中,以 get 开始的方法,将在 read-only 事务的上下文中执行,其他方法将在默认的事务语义下执行”. <tx:advice/> 标签的 transaction-manager 的属性用来设置驱动事务的 TransactionManager 的名称. (在本例中为 txManager bean)

如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略事务通知(<tx:advice/>) 中的 transaction-manager 属性. 如果要连接的 TransactionManager bean 具有任何其他名称, 则必须显式使用 transaction-manager 属性进行指定,如上例所示.

<aop:config/> 定义确保 txAdvice bean 定义的事务性的通知在程序中的适当位置执行. 首先,定义一个切入点,该切入点与 FooService 接口(fooServiceOperation) 中定义的任何操作的执行相匹配. 然后使用通知者将切入点与 txAdvice 相关联. 结果表明,在执行 fooServiceOperation 时,将运行 txAdvice 定义的通知.

<aop:pointcut/> 元素中定义的表达式是切面切入点表达式. 有关 Spring 中切入点表达式的更多详细信息,请参阅AOP 部分.

常见的要求是使整个服务层具有事务性. 执行此操作的最佳方法是更改切入点表达式以匹配服务层中的任何操作. 以下示例显示了如何执行此操作:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
在前面的示例中,假设您的所有服务接口都在 x.y.service 包中定义. 有关详细信息,请参阅AOP 部分 .

现在我们已经分析了配置,您可能会问自己,"所有这些配置实际上做了什么? "

前面显示的配置用于创建一个环绕对象的事务代理,此对象在 fooService bean 中定义. 代理配置有事务性通知,以便在代理上调用适当的方法时, 根据与该方法关联的事务配置,将事务启动、挂起、标记为 read-only 等操作. 思考以下程序,用来测试前面的配置:

Java
public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
        FooService fooService = ctx.getBean(FooService.class);
        fooService.insertFoo(new Foo());
    }
}
Kotlin
import org.springframework.beans.factory.getBean

fun main() {
    val ctx = ClassPathXmlApplicationContext("context.xml")
    val fooService = ctx.getBean<FooService>("fooService")
    fooService.insertFoo(Foo())
}

运行前面程序的输出应该类似于以下内容(为清楚起见,LogFJ 输出和 DefaultFooService 类的 insertFoo(..) 方法抛出的 UnsupportedOperationException 堆栈跟踪已被截断) :

<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors

<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]

<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

要使用响应式事务管理,代码必须使用响应式风格.

Spring Framework 使用 ReactiveAdapterRegistry 来确定方法返回类型是否是响应式.

下面的代码使用以前的 FooService 的修改版本,但是这次代码使用了响应类型:

Java
public interface FooService {

    Flux<Foo> getFoo(String fooName);

    Publisher<Foo> getFoo(String fooName, String barName);

    Mono<Void> insertFoo(Foo foo);

    Mono<Void> updateFoo(Foo foo);

}
Kotlin
interface FooService {

    fun getFoo(fooName: String): Flow<Foo>

    fun getFoo(fooName: String, barName: String): Publisher<Foo>

    fun insertFoo(foo: Foo) : Mono<Void>

    fun updateFoo(foo: Foo) : Mono<Void>
}

以下示例显示了上述接口的实现:

Java
public class DefaultFooService implements FooService {

    @Override
    public Flux<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Publisher<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Publisher<Foo> {
        // ...
    }

    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }

    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

强制性和响应式事务管理对事务边界和事务属性定义共享语义.强制性事务和响应式事务之间的主要区别在于后者的延迟性质. TransactionInterceptor 用事务运算符修饰返回的响应类型,以开始和清理事务. 因此,调用事务响应式方法将实际的事务管理推迟到激活响应式类型的处理的响应类型.

响应式事务管理的另一方面涉及数据转义,这是编程模型的自然结果.

成功终止方法后,从事务方法返回命令性事务的方法返回值,以使部分计算的结果不会逃脱方法的关闭.

响应式事务方法返回一个响应式包装器类型,该类型表示一个计算序列以及一个开始和完成计算的承诺.

当事务正在进行但不一定完成时, Publisher 可以发出数据. 因此,依赖于成功完成整个事务的方法需要确保完成并缓冲调用代码中的结果.

1.4.3. 回滚声明式事务

上一节概述了如何在应用程序中以声明方式指定类(通常是服务层类) 的事务设置的基础知识. 本节介绍如何在 XML 中以简单的声明方式控制事务回滚,有关使用 @Transactional 注解以声明方式控制回滚语义的详细信息,请参阅 @Transactional 配置.

在 Spring Framework 的事务基础结构中指明事务的工作被回滚的推荐方法是在当前事务上下文中执行的代码中抛出 Exception Spring Framework 的事务基础结构代码捕获任何未处理的 Exception,因为它冒泡调用堆栈并确定是否将事务标记为回滚.

在其默认配置中,Spring Framework 的事务基础结构代码仅在运行时和未经检查的异常情况下标记事务回滚. 也就是说,抛出的异常是 RuntimeException 的实例或子类. (默认情况下,Error 实例也会导致回滚) . 默认配置中,事务方法中抛出的已检查异常不会导致事务回滚.

您可以准确配置哪些 Exception 类型标记用于回滚的事务,通过指定回滚规则包括已检查的异常.

Rollback rules

Rollback rules determine if a transaction should be rolled back when a given exception is thrown, and the rules are based on patterns. A pattern can be a fully qualified class name or a substring of a fully qualified class name for an exception type (which must be a subclass of Throwable), with no wildcard support at present. For example, a value of "javax.servlet.ServletException" or "ServletException" will match javax.servlet.ServletException and its subclasses.

Rollback rules may be configured in XML via the rollback-for and no-rollback-for attributes, which allow patterns to be specified as strings. When using @Transactional, rollback rules may be configured via the rollbackFor/noRollbackFor and rollbackForClassName/noRollbackForClassName attributes, which allow patterns to be specified as Class references or strings, respectively. When an exception type is specified as a class reference its fully qualified name will be used as the pattern. Consequently, @Transactional(rollbackFor = example.CustomException.class) is equivalent to @Transactional(rollbackForClassName = "example.CustomException").

You must carefully consider how specific the pattern is and whether to include package information (which isn’t mandatory). For example, "Exception" will match nearly anything and will probably hide other rules. "java.lang.Exception" would be correct if "Exception" were meant to define a rule for all checked exceptions. With more unique exception names such as "BaseBusinessException" there is likely no need to use the fully qualified class name for the exception pattern.

Furthermore, rollback rules may result in unintentional matches for similarly named exceptions and nested classes. This is due to the fact that a thrown exception is considered to be a match for a given rollback rule if the name of thrown exception contains the exception pattern configured for the rollback rule. For example, given a rule configured to match on com.example.CustomException, that rule would match against an exception named com.example.CustomExceptionV2 (an exception in the same package as CustomException but with an additional suffix) or an exception named com.example.CustomException$AnotherException (an exception declared as a nested class in CustomException).

以下 XML 片段演示了如何通过 rollback-for 属性提供 exception pattern 来为已检查的、特定于应用程序的 Exception 类型配置回滚:

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
    <tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

如果您不希望在抛出异常时回滚事务,则还可以指定 'no rollback' 规则. 以下示例告诉 Spring Framework 的事务基础结构即使 面对未处理的 InstrumentNotFoundException 也要提交事务:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
    <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

当 Spring Framework 的事务基础结构捕获异常并且它参考配置的回滚规则以确定是否将事务标记为回滚时,最先匹配的规则获胜. 因此,在以下配置的情况下,除了 InstrumentNotFoundException 之外的任何异常都会导致后续事务的回滚:

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

您还可以以编程方式指示所需的回滚. 虽然它很简单,但这个过程非常具有侵入性,并且会将您的代码紧密地耦合到 Spring Framework 的事务结构中. 以下示例显示如何以编程方式指示所需的回滚:

Java
public void resolvePosition() {
    try {
        // some business logic...
    } catch (NoProductInStockException ex) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
Kotlin
fun resolvePosition() {
    try {
        // some business logic...
    } catch (ex: NoProductInStockException) {
        // trigger rollback programmatically
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

如果可能的话,强烈建议您使用声明式方法进行回滚. 只有在必要的情况下,使用编程化回滚,但它的使用方式可以实现基于 POJO 的简单体系结构.

1.4.4. 为不同的 bean 配置不同的事务语义

考虑具有多个服务层对象的情况,并且您希望对每个对象应用完全不同的事务配置. 您可以通过使用不同的 pointcutadvice-ref 属性值定义不同的 <aop:advisor/> 元素来实现.

作为比较,首先假设您的所有服务层类都在根 x.y.service 包中定义. 要使所有作为在该包(或子包中) 中定义的类的实例的 bean 以及以 Service 结尾的类具有默认的事务配置,您可以编写以下内容:

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

    <aop:config>

        <aop:pointcut id="serviceOperation"
                expression="execution(* x.y.service..*Service.*(..))"/>

        <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>

    </aop:config>

    <!-- these two beans will be transactional... -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... and these two beans won't -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

    <tx:advice id="txAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

以下示例显示如何使用完全不同的事务设置配置两个不同的 bean:

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

    <aop:config>

        <aop:pointcut id="defaultServiceOperation"
                expression="execution(* x.y.service.*Service.*(..))"/>

        <aop:pointcut id="noTxServiceOperation"
                expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>

        <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>

        <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>

    </aop:config>

    <!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this bean will also be transactional, but with totally different transactional settings -->
    <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>

    <tx:advice id="defaultTxAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <tx:advice id="noTxAdvice">
        <tx:attributes>
            <tx:method name="*" propagation="NEVER"/>
        </tx:attributes>
    </tx:advice>

    <!-- other transaction infrastructure beans such as a TransactionManager omitted... -->

</beans>

1.4.5. <tx:advice/> 配置

本节总结了使用 <tx:advice/> 标签指定各种事务的配置. 默认的 <tx:advice/> 的配置为:

  • 传播配置REQUIRED.

  • 隔离级别是 DEFAULT.

  • 事务可读写

  • 事务超时默认为底层事务的默认超时,若事务不支持超时,怎默认值为 none.

  • 任何 RuntimeException 都会触发回滚,任何未检查 Exception 都不会触发回滚.

你可以修改这些默认的设置. 下表总结了嵌套在 <tx:advice/><tx:attributes/> 标记内的 <tx:method/> 标记的各种属性:

Table 1. <tx:method/> 设置
Attribute Required? Default Description

name

Yes

与事务属性关联的方法名称. 通配符() 字符可用于将相同的事务属性设置与多个方法相关联(例如, get, handle*, on*Event 等) .

propagation

No

REQUIRED

事务传播行为.

isolation

No

DEFAULT

事务隔离级别. 仅适用于 REQUIREDREQUIRES_NEW 的传播设置.

timeout

No

-1

事务超时(秒) . 仅适用于传播 REQUIREDREQUIRES_NEW.

read-only

No

false

读写与只读事务. 仅适用于 REQUIREDREQUIRES_NEW.

rollback-for

No

以逗号分隔的触发回滚的 Exception 实例列表. 例如, com.foo.MyBusinessException,ServletException.

no-rollback-for

No

以逗号分隔的 Exception 实例列表,不会触发回滚. 例如, com.foo.MyBusinessException,ServletException.

1.4.6. 使用 @Transactional

除了基于 XML 的事务配置声明方法之外,您还可以使用基于注解的方法. 直接在 Java 源代码中声明式事务,使声明更接近受影响的代码. 没有太多过度耦合的危险,因为无论如何,用于事务处理的代码几乎总是以这种方式部署.

标准的 javax.transaction.Transactional 注解也支持 Spring 使用自己的注解. 有关更多详细信息,请参阅 JTA 1.2 文档.

使用 @Transactional 注解提供的易用性最好通过一个示例来说明,该示例将在后面的文本中进行说明. 考虑以下类定义:

Java
// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Foo getFoo(String fooName) {
        // ...
    }

    @Override
    public Foo getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public void insertFoo(Foo foo) {
        // ...
    }

    @Override
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
// the service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Foo {
        // ...
    }

    override fun insertFoo(foo: Foo) {
        // ...
    }

    override fun updateFoo(foo: Foo) {
        // ...
    }
}

如上所述在类级别使用,注解指示声明类(及其子类) 的所有方法的默认值. 或者,每个方法都可以单独注解.,请参阅 方法可见性与 @Transactional 获取有关 Spring 事务方法更多的信息。 请注意,类级别注解不适用于类层次结构中的父类; 在这种情况下,需要在本地重新声明方法才能参与子类级别的注解.

当如上所述的 POJO 类在 Spring 上下文中定义为 bean 时,可以通过 @Configuration 类中的 @EnableTransactionManagement 注解使 bean 实例进行事务处理. 有关完整详细信息,请参阅 javadoc.

在 XML 配置文件中,<tx:annotation-driven/> 标签提供了类似的便利:

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

    <!-- this is the service object that we want to make transactional -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- enable the configuration of transactional behavior based on annotations -->
    <!-- a TransactionManager is still required -->
    <tx:annotation-driven transaction-manager="txManager"/> (1)

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (this dependency is defined somewhere else) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- other <bean/> definitions here -->

</beans>
1 使 bean 实例使用事务.
如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略 <tx:annotation-driven/> 标记中的 transaction-manager 属性. 如果要依赖注入的 TransactionManager bean具有任何其他名称,则必须使用 transaction-manager 属性,如上例所示.

相对于响应式,响应式事务方法使用响应式返回类型,如下所示:

Java
// the reactive service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {

    @Override
    public Publisher<Foo> getFoo(String fooName) {
        // ...
    }

    @Override
    public Mono<Foo> getFoo(String fooName, String barName) {
        // ...
    }

    @Override
    public Mono<Void> insertFoo(Foo foo) {
        // ...
    }

    @Override
    public Mono<Void> updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
// the reactive service class that we want to make transactional
@Transactional
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Flow<Foo> {
        // ...
    }

    override fun getFoo(fooName: String, barName: String): Mono<Foo> {
        // ...
    }

    override fun insertFoo(foo: Foo): Mono<Void> {
        // ...
    }

    override fun updateFoo(foo: Foo): Mono<Void> {
        // ...
    }
}

请注意,对于返回的 Publisher,在响应流取消信号方面有一些特殊注意事项. 有关更多详细信息,请参见 取消信号 部分. 有关更多详细信息,请 "使用 TransactionOperator"

方法可见性与 @Transactional

当您在 Spring 的标准配置中使用事务代理时,您应该仅将 @Transactional 注解应用于具有 public 可见性的方法。 如果您使用 @Transactional 注解 protectedprivate 或 package-visible 方法进行注解, 则不会引发错误,但带注解的方法不会显示已配置的事务设置. 如果需要对非公共方法使用注解,请考虑使用基于类的代理的提示或考虑使用 AspectJ 编译时或加载时编织(稍后描述)。

当在 @Configuration 类中使用 @EnableTransactionManagement 时,也可以通过注册自定义 transactionAttributeSource bean 来使基于类的代理的 protected 或包可见方法具有事务性,如下例所示。 但是请注意,基于接口的代理中的事务方法必须始终是 public 并在代理接口中定义。

/**
 * Register a custom AnnotationTransactionAttributeSource with the
 * publicMethodsOnly flag set to false to enable support for
 * protected and package-private @Transactional methods in
 * class-based proxies.
 *
 * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
 */
@Bean
TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

Spring TestContext Framework 默认支持非私有的 @Transactional 测试方法。 参见测试中的Transaction Management 章举例。

您可以将 @Transactional 注解应用于接口定义、接口上的方法、类定义或类上的方法. 但是,仅仅存在 @Transactional 注解不足以激活事务行为. @Transactional 注解仅用于元数据, 某些运行时基础结构使用可使用 @Transactional-aware注解, 并且可以使用元数据来配置具有事 务行为的 Bean. 在前面的示例中,<tx:annotation-driven/> 元素会切换事务行为.

Spring 团队建议仅在具体类(以及具体类的方法) 上使用 @Transactional 注解,而不是在接口上. 你当然可以在接口(以及接口方法) 上使用 @Transactional 注解,但是只有你在使用基于接口的代理时才会工作. Java 注解不从接口继承的事实表明,如果你使用基于类的代理(proxy-target-class="true") 或基于织入的切面(mode="aspectj") , 代理和织入的结构将不会识别事务配置,并且对象未包含在事务中.
在代理模式(默认) 下,代理只拦截外部方法. 这意味着即使被调用的方法用 @Transactional 标记,自调用(实际上是,目标对象的方法调用目标对象的另一个方法) 也不会在运行时产生实际事务. 此外,代理应该完全初始化以提供预期的行为,因此您不应该在初始化代码(即 @PostConstruct) 中依赖此功能.

如果你正在寻找使用事务包装的自我调用,请考虑使用 AspectJ 模式(下表为 mode 的属性) . 在这种情况下,首先没有代理. 相反,被织入的目标类(即修改字节码) 将 @Transactional 注解转换为任何方法的运行时行为.

Table 2. 注解驱动的事务配置
XML 属性 Annotation 属性 默认 描述

transaction-manager

N/A (see TransactionManagementConfigurer javadoc)

transactionManager

用于事务管理的名称. 在之前的例子中,假如事务管理的名称不是 transactionManager,那么此属性是必须的.

mode

mode

proxy

默认模式(proxy) 使用 Spring 的 AOP 框架处理要被注解的 bean (遵循代理语义,如前所述,仅适用于通过代理进入的方法调用) . 替代模式(aspectj) 用 Spring 的 AspectJ 事务切面织入受影响的类,修改目标类字节代码以应用于任何类型的方法调用. AspectJ 织入需要类路径中的 spring-aspects.jar 以及启用加载时织入(或编译时织入) . (有关如何设置加载时织入的详细信息,请参阅Spring配置. )

proxy-target-class

proxyTargetClass

false

仅适用于 proxy 模式. 控制使用 @Transactional 注解注解的类创建的事务代理类型. 如果 proxy-target-class 属性设置为 true, 则创建基于类的代理. 如果 proxy-target-classfalse 或者省略了该属性,则会创建基于标准 JDK 接口的代理. (参见代理机制,详细检查不同的代理类型. )

order

order

Ordered.LOWEST_PRECEDENCE

定义使用 @Transactional 注解注解的 bean 的事务通知的顺序. (关于 AOP 通知排序的相关规则的更多信息,请参考通知排序) 没有指定排序,意味着 AOP 子系统来决定通知的排序.

处理 @Transactional 注解的默认通知模式是 proxy,它允许仅通过代理拦截调用. 这种方式不能拦截同一类中的本地方法调用. 对于更高级别的拦截模式,请考虑结合编译时或加载时织入切换到 aspectj 模式.
proxy-target-class 属性控制着使用 @Transactional 注解注解的类创建的事务代理的类型. 如果 proxy-target-class 的值为 true,创建基于类的代理. 如果 proxy-target-class 的值为 false 或者此属性别省略,则创建基于标准JDK接口的代理. (有关不同代理类型的讨论,请参阅 Proxying Mechanisms ) .
@EnableTransactionManagement<tx:annotation-driven/> 仅在定义它们的同一应用程序上下文的bean上查找 @Transactional. 这意味着,如果你将注解驱动的配置放在 DispatcherServletWebApplicationContext 中, 它只会在你的控制器而不是服务器中检查有 @Transactional 注解的 beans. 有关更多信息,请参阅MVC.

在评估事务方法设置时,precedence 最多的位置优先。 在下面的例子中,DefaultFooService 使用 @Transactional 在类上标注为 只读属性,但同一类中的 updateFoo(Foo) 方法上的 @Transactional 注解 优先于在类级别定义的事务设置。

Java
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        // ...
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // ...
    }
}
Kotlin
@Transactional(readOnly = true)
class DefaultFooService : FooService {

    override fun getFoo(fooName: String): Foo {
        // ...
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    override fun updateFoo(foo: Foo) {
        // ...
    }
}
@Transactional 配置

@Transactional 注解是指定接口、类或方法必须具有事务语义的元数据 (例如,"在调用此方法时启动全新的只读事务,暂停任何现有事务") . 默认的 @Transactional 设置如下:

  • 传播设置为 PROPAGATION_REQUIRED.

  • 隔离级别是 ISOLATION_DEFAULT.

  • 事务可读写.

  • 事务超时默认为基础事务系统的默认超时,如果不支持超时,则默认为 none.

  • 任何 RuntimeExceptionError 都触发回滚,任何受检查 Exception 都不触发回滚.

你可以修改这些默认值. 下表概述了 @Transactional 注解的各种属性:

Table 3. @Transactional 配置
Property Type Description

value

String

可选限定符,指定要使用的事务管理器.

transactionManager

String

Alias for value.

label

Array of String labels to add an expressive description to the transaction.

Labels 可以由事务管理器评估, 并将特定于实现的行为与实际事务相关联.

propagation

enum: Propagation

可选的传播设置.

isolation

enum: Isolation

可选的隔离级别. 只有当传播值为 REQUIREDREQUIRES_NEW 时可用.

timeout

int (in seconds of granularity)

可选的事务超时. 只有当传播值为 REQUIREDREQUIRES_NEW 时可用.

timeoutString

String (in seconds of granularity)

Alternative for specifying the timeout in seconds as a String value — for example, as a placeholder.

readOnly

boolean

读写与只读事务. 只有在 REQUIREDREQUIRES_NEW 下可用.

rollbackFor

Array of Class objects, which must be derived from Throwable.

必定导致回滚的可选异常类数组.

rollbackForClassName

Array of class names. The classes must be derived from Throwable.

必定导致回滚的可选异常类数组.

noRollbackFor

Array of Class objects, which must be derived from Throwable.

必定不会导致回滚的可选异常类数组.

noRollbackForClassName

Array of String class names, which must be derived from Throwable.

必定不会导致回滚的可选异常类数组.

请参阅 Rollback rules,了解有关可能的无意匹配的回滚规则语义、模式和警告的更多详细信息。

目前,你无法明确的控制事务的名称, 'name' 意味着在事务监视器(如果可适用,例如WebLogic的事务监视器) 和输出日志中的事物名称, 对于声明式事务,事务名称始终是完全限定的类名+ .+事务通知类的方法名. 例如,如果 BusinessService 类的方法 handlePayment(..) 开始一个事务, 那么事务的名称将是: com.example.BusinessService.handlePayment.

@Transactional 的多事务管理

大多数 Spring 应用程序只需要一个事务管理器,但在某些情况下,您可能需要在单个应用程序中使用多个独立的事务管理器. 您可以使用 @Transactional 注解的 valuetransactionManager 属性来选择性地指定要使用的 TransactionManager 的标识. 这可以是 bean 名称或事务管理器 bean 的限定符值. 例如,使用限定符表示法,您可以将以下 Java 代码 与应用程序上下文中的以下事务管理器 bean 声明组合在一起:

Java
public class TransactionalService {

    @Transactional("order")
    public void setSomething(String name) { ... }

    @Transactional("account")
    public void doSomething() { ... }

    @Transactional("reactive-account")
    public Mono<Void> doSomethingReactive() { ... }
}
Kotlin
class TransactionalService {

    @Transactional("order")
    fun setSomething(name: String) {
        // ...
    }

    @Transactional("account")
    fun doSomething() {
        // ...
    }

    @Transactional("reactive-account")
    fun doSomethingReactive(): Mono<Void> {
        // ...
    }
}

以下清单显示了 bean 的声明:

<tx:annotation-driven/>

    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="order"/>
    </bean>

    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="account"/>
    </bean>

    <bean id="transactionManager3" class="org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager">
        ...
        <qualifier value="reactive-account"/>
    </bean>

在这个例子中,TransactionalService 上的各个方法在单独的事务管理器下运行,由 orderaccountreactive-account 限定符区分. 如果未找到特定限定符的 TransactionManager bean,则仍将使用默认的 <tx:annotation-driven> 目标 bean 名称 transactionManager.

自定义组合注解

如果您发现在许多不同方法上重复使用与 @Transactional 相同的属性, Spring 的元注解支持 允许您为特定用例定义自定义组合注解. 例如,请思考以下注解定义:

Java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}
Kotlin
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "order", label = ["causal-consistency"])
annotation class OrderTx

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional(transactionManager = "account", label = ["retryable"])
annotation class AccountTx

前面的注解允许我们编写上一节中的示例,如下所示:

Java
public class TransactionalService {

    @OrderTx
    public void setSomething(String name) {
        // ...
    }

    @AccountTx
    public void doSomething() {
        // ...
    }
}
Kotlin
class TransactionalService {

    @OrderTx
    fun setSomething(name: String) {
        // ...
    }

    @AccountTx
    fun doSomething() {
        // ...
    }
}

在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们也可以包含传播行为、回滚规则、超时和其他功能.

1.4.7. 事务传播属性

本节描述了 Spring 中事务传播的一些语义. 请注意,本节不是粗略地介绍事务传播. 相反,它详细介绍了 Spring 中有关事务传播的一些语义.

在 Spring 管理的事务中,要注意物理和逻辑事务之间的区别,以及在这两种事务中如何应用事务传播.

理解 PROPAGATION_REQUIRED
tx prop required

如果在当前范围内未存在事务或参与为更大范围定义的现有 "外部" 事务,PROPAGATION_REQUIRED 会在当前范围内强制执行物理事务. 这是同一线程内公共调用堆栈安排的一个很好的默认值(例如,委托给几个存储库方法的服务,其中所有底层资源都必须参与服务级事务) .

默认情况下,参与的事务加入外部作用域的特征,默认忽略本地隔离级别、超时值或只读标志(如果有) . 如果希望在参与具有不同隔离级别的现有事务时拒绝隔离级别声明, 请考虑在事务管理器上将 validateExistingTransactions 标志切换为 true. 此非宽松模式还拒绝只读不匹配(即,尝试参与只读外部作用域的内部读写事务) .

当传播设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建逻辑事务范围. 每个这样的逻辑事务范围可以单独确定仅回滚状态,外部事务范围在逻辑上独立于内部事务范围. 在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一物理事务. 因此,内部事务范围中的仅回滚标记确实会影响外部事务实际提交的机会.

但是,在内部事务作用域设置仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务作用域静默触发) 是意外的. 此时抛出相应的 UnexpectedRollbackException. 这是预期的行为,因此事务的调用者永远不会被误导以假设在实际上没有执行提交. 因此,如果内部事务(外部调用者不知道) 以静默方式将事务标记为仅回滚,则外部调用者仍会调用 commit. 外部调用者需要接收 UnexpectedRollbackException 以清楚地指示已执行回滚.

理解 PROPAGATION_REQUIRES_NEW
tx prop requires new

PROPAGATION_REQUIRED 相反,PROPAGATION_REQUIRES_NEW 始终对每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务. 在这样的设置中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务不受内部事务的回滚状态的影响,并且内部事务的锁在完成后立即释放. 这样一个独立的内部事务也可以声明它自己的隔离级别、超时和只读设置,而不是继承外部事务的特性.

理解 PROPAGATION_NESTED

PROPAGATION_NESTED 使用具有多个保存点的单个物理事务,它可以回滚到该事务. 这种部分回滚允许内部事务作用域触发其作用域的回滚,外部事务能够继续使用物理事务,尽管已经回滚了一些操作. 此设置通常映射到 JDBC 保存点,因此它仅适用于 JDBC 资源事务. 请参阅 Spring 的 DataSourceTransactionManager.

1.4.8. 为事务操作提供咨询

假设您要执行事务操作和一些基本概要分析通知. 你如何在 <tx:annotation-driven/> 的上下文中实现这一点?

当你调用 updateFoo(Foo) 方法,你希望看到以下操作:

  • 配置分析切面的开始.

  • 事务性通知的执行.

  • 通知对象方法的执行.

  • 事务的提交.

  • 分析切面报告整个事务方法在执行期间的确切时间.

本章不涉及详细解释 AOP(除非它适用于事务) . 有关 AOP 配置和 AOP 的详细信息,请参阅 AOP 部分 .

以下代码显示了前面讨论的简单分析切面:

Java
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;

public class SimpleProfiler implements Ordered {

    private int order;

    // allows us to control the ordering of advice
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    // this method is the around advice
    public Object profile(ProceedingJoinPoint call) throws Throwable {
        Object returnValue;
        StopWatch clock = new StopWatch(getClass().getName());
        try {
            clock.start(call.toShortString());
            returnValue = call.proceed();
        } finally {
            clock.stop();
            System.out.println(clock.prettyPrint());
        }
        return returnValue;
    }
}
Kotlin
class SimpleProfiler : Ordered {

    private var order: Int = 0

    // allows us to control the ordering of advice
    override fun getOrder(): Int {
        return this.order
    }

    fun setOrder(order: Int) {
        this.order = order
    }

    // this method is the around advice
    fun profile(call: ProceedingJoinPoint): Any {
        var returnValue: Any
        val clock = StopWatch(javaClass.name)
        try {
            clock.start(call.toShortString())
            returnValue = call.proceed()
        } finally {
            clock.stop()
            println(clock.prettyPrint())
        }
        return returnValue
    }
}

通过 Ordered 接口控制通知的顺序. 有关通知顺序的完整详细信息,请参阅通知顺序.

以下配置创建了一个 fooServicebean,它以所需的顺序对其应用了分析和事务切面:

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

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- this is the aspect -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <tx:annotation-driven transaction-manager="txManager" order="200"/>

    <aop:config>
            <!-- this advice runs around the transactional advice -->
            <aop:aspect id="profilingAspect" ref="profiler">
                <aop:pointcut id="serviceMethodWithReturnValue"
                        expression="execution(!void x.y..*Service.*(..))"/>
                <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
            </aop:aspect>
    </aop:config>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
        <property name="username" value="scott"/>
        <property name="password" value="tiger"/>
    </bean>

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

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

    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- the profiling advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- run before the transactional advice (hence the lower order number) -->
        <property name="order" value="1"/>
    </bean>

    <aop:config>
        <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
        <!-- runs after the profiling advice (c.f. the order attribute) -->

        <aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
        <!-- order value is higher than the profiling aspect -->

        <aop:aspect id="profilingAspect" ref="profiler">
            <aop:pointcut id="serviceMethodWithReturnValue"
                    expression="execution(!void x.y..*Service.*(..))"/>
            <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
        </aop:aspect>

    </aop:config>

    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- other <bean/> definitions such as a DataSource and a TransactionManager here -->

</beans>

前面配置的结果是一个 fooService bean,它具有按顺序应用于它的分析和事务切面. 如果希望在事务性通知之后执行性能分析通知, 则可以交换性能分析切面 bean 的 order 属性的值,使其高于事务性通知的顺序值.

你也可以以相似的方式配置其他切面.

1.4.9. 将 @Transactional 与 AspectJ 一起使用

您还可以通过 AspectJ 切面在 Spring 容器外部使用 Spring Framework 的 @Transactional 注解支持. 为此,首先使用 @Transactional 注解注解您的类(以及可选的类的方法) , 然后使用 spring-aspects.jar 文件中定义的 org.springframework.transaction.aspectj.AnnotationTransactionAspect 链接(织入) 您的应用程序. 您还必须使用事务管理器配置切面. 您可以使用 Spring Framework 的 IoC 容器来处理依赖注入的切面. 配置事务管理方面的最简单方法是使用 <tx:annotation-driven/> 元素并将 mode 属性指定为 aspectj,如 使用 使用 @Transactional 中所述. 因为我们专注于在 Spring 容器之外运行的应用程序,所以我们将向您展示如何以编程方式执行此操作.

在继续之前,你可能希望分别阅读 使用 使用 @TransactionalAOP .

以下示例显示如何创建事务管理器并配置 AnnotationTransactionAspect 以使用它:

Java
// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);
Kotlin
// construct an appropriate transaction manager
val txManager = DataSourceTransactionManager(getDataSource())

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().transactionManager = txManager
使用此切面时,必须注解实现类(或该类中的方法或两者) ,而不是类实现的(如果有) 接口. AspectJ 遵循 Java 的规则,即接口上的注解不会被继承.

类上的 @Transactional 注解指定了在类中执行任何公共方法的默认事务语义.

类中方法的 @Transactional 注解会覆盖类注解(如果存在) 给出的默认事务语义. 无论可见性如何,您都可以注解任何方法.

AnnotationTransactionAspect 织入应用程序,必须使用 AspectJ 构建应用程序 (请参阅 AspectJ开发指南) 或使用加载时织入. 有关使用 AspectJ 进行加载时编织的讨论,请参阅 Spring Framework 中使用 AspectJ 进行加载时织入.

1.5. 编程式事务管理

Spring Framework 提供了两种编程式事务管理,通过使用:

  • TransactionTemplateTransactionalOperator.

  • 直接使用 TransactionManager.

Spring 团队建议使用命令式 TransactionTemplate 进行事务管理.对于响应式则推荐使用 TransactionalOperator, 第二种方法类似于使用 JTA UserTransaction API,尽管异常处理不那么麻烦.

1.5.1. 使用 TransactionTemplate

TransactionTemplate 采用与其他 Spring 模板相同的方法,例如 JdbcTemplate. 它使用回调方法(使应用程序代码减少了一些必要模板代码和释放事务资源) 并生成您由您想象的代码,因为您的代码仅关注您想要做的事情.

如下面的示例所示,使用 TransactionTemplate 将您与 Spring 的事务基础结构和 API 结合在一起. 所以您需要决定编程式事务管理是否适合您的开发需求.

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

Java
public class SimpleService implements Service {

    // single TransactionTemplate shared amongst all methods in this instance
    private final TransactionTemplate transactionTemplate;

    // use constructor-injection to supply the PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // the code in this method runs in a transactional context
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}
Kotlin
// use constructor-injection to supply the PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {

    // single TransactionTemplate shared amongst all methods in this instance
    private val transactionTemplate = TransactionTemplate(transactionManager)

    fun someServiceMethod() = transactionTemplate.execute<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

如果没有返回值,则可以在匿名类中使用便捷的 TransactionCallbackWithoutResult 类,如下所示:

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});
Kotlin
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

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

Java
transactionTemplate.execute(new TransactionCallbackWithoutResult() {

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

    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        try {
            updateOperation1()
            updateOperation2()
        } catch (ex: SomeBusinessException) {
            status.setRollbackOnly()
        }
    }
})
指定事务配置

您可以通过编程方式或在配置中指定 TransactionTemplate 的事务配置(例如传播模式、隔离级别、超时等) . 默认情况下,TransactionTemplate 实例具有默认的事务配置. 以下示例显示了用户使用编程式自定义 TransactionTemplate 的事务配置:

Java
public class SimpleService implements Service {

    private final TransactionTemplate transactionTemplate;

    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);

        // the transaction settings can be set here explicitly if so desired
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
        // and so forth...
    }
}
Kotlin
class SimpleService(transactionManager: PlatformTransactionManager) : Service {

    private val transactionTemplate = TransactionTemplate(transactionManager).apply {
        // the transaction settings can be set here explicitly if so desired
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 seconds
        // and so forth...
    }
}

以下示例使用 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 实例.

1.5.2. Using the TransactionOperator

TransactionOperator 遵循类似于其他响应式操作的设计. 它使用一种回调方法(使应用程序代码不必进行样板获取和释放事务性资源),并生产您期望的代码,因为您的代码仅专注于您要执行的操作.

如以下示例所示,使用 TransactionOperator 可以使您与 Spring 的事务基础结构和 API 结合. 所以您需要决定编程式事务管理是否适合您的开发需求.

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

Java
public class SimpleService implements Service {

    // single TransactionOperator shared amongst all methods in this instance
    private final TransactionalOperator transactionalOperator;

    // use constructor-injection to supply the ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionOperator = TransactionalOperator.create(transactionManager);
    }

    public Mono<Object> someServiceMethod() {

        // the code in this method runs in a transactional context

        Mono<Object> update = updateOperation1();

        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}
Kotlin
// use constructor-injection to supply the ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

    // single TransactionalOperator shared amongst all methods in this instance
    private val transactionalOperator = TransactionalOperator.create(transactionManager)

    suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

TransactionalOperator 可以以两种方式使用:

  • 使用 Project Reactor 类型的操作符样式 (mono.as(transactionalOperator::transactional))

  • 其他所有情况的回调样式 (transactionalOperator.execute(TransactionCallback<T>))

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

Java
transactionalOperator.execute(new TransactionCallback<>() {

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

    override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
        updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly())
    }
})
取消信号

在 Reactive Streams 中,Subscriber 可以取消其 Subscription 并终止其 Publisher. Project Reactor 以及其他库中的操作,例如 next(),take(long), timeout(Duration) 等,都可以发出取消操作. 没有办法知道取消的原因,无论是由于错误还是仅仅是出于进一步消费的兴趣, 在 5.3 版本中,取消信号导致回滚. 因此,重要的是要考虑 Publisher 下游使用的操作符. 特别是在 Flux 或其他多值的 Publisher 的情况下,全部输出必须被消费才允许事务完成.

指定事务配置

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

Java
public class SimpleService implements Service {

    private final TransactionalOperator transactionalOperator;

    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

        // the transaction settings can be set here explicitly if so desired
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 seconds
        // and so forth...

        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}
Kotlin
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {

    private val definition = DefaultTransactionDefinition().apply {
        // the transaction settings can be set here explicitly if so desired
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED
        timeout = 30 // 30 seconds
        // and so forth...
    }
    private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}

1.5.3. 使用 TransactionManager

以下各节说明命令式和响应式事务管理器的用法.

1.5.4. 使用 PlatformTransactionManager

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

Java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = txManager.getTransaction(def);
try {
    // put your business logic here
}
catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
Kotlin
val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

val status = txManager.getTransaction(def)
try {
    // put your business logic here
} catch (ex: MyException) {
    txManager.rollback(status)
    throw ex
}

txManager.commit(status)
使用 ReactiveTransactionManager

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

Java
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
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)));
});
Kotlin
val def = DefaultTransactionDefinition()
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED

val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->

    val tx = ... // put your business logic here

    tx.then(txManager.commit(status))
            .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }
}

1.6. 在编程式和声明式事务管理之间进行选择

只有在进行少量事务操作时,编程式事务管理通常才是一个好主意. 例如,如果您的 Web 应用程序仅需要针对某些更新操作进行事务处理,则可能不希望使用 Spring 或任何其他技术来设置事务代理. 在这种情况下,使用 TransactionTemplate 可能是一个很好的方法. 它能够明确地设置事务名称,也只能通过使用编程式方法进行事务管理.

另一方面,如果您的应用程序有许多事务操作,那么使用声明式事务管理通常是值得的. 它将事务与业务逻辑分离并且配置简单. 使用 Framework 而不是 EJB CMT 时,声明式事务的配置成本大大降低.

1.7. 事务绑定事件

从 Spring 4.2 开始,事件的监听器可以绑定到事务的一个阶段. 典型示例是在事务成功完成时处理事件. 这样做可以在当前事务的结果对于监听器很重要时,更灵活地使用事件.

您可以使用 @EventListener 注解注册常规事件监听器. 如果需要将其绑定到事务,请使用 @TransactionalEventListener. 执行此操作时,默认情况下,监听器将绑定到事务的 comomit 阶段.

下一个例子展示了这个概念. 假设一个组件发布了一个创建订单的事件, 并且我们想要定义一个只在发布事务成功提交后才应该处理该事件的监听器. 以下示例配置此类事件监听器:

Java
@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        // ...
    }
}
Kotlin
@Component
class MyComponent {

    @TransactionalEventListener
    fun handleOrderCreatedEvent(creationEvent: CreationEvent<Order>) {
        // ...
    }
}

@TransactionalEventListener 注解暴露了一个 phase 属性,该属性允许您自定义监听器应绑定到的事务的阶段. 有效阶段是 BEFORE_COMMITAFTER_COMMIT(默认值) 、 AFTER_ROLLBACKAFTER_COMPLETION,它们聚合事务完成(无论是提交还是回滚) .

如果没有事务正在运行,那么根本不会调用监听器,因为我们无法遵守所需的语义. 然而,你可以通过设置注解的 fallbackExecution 属性值为 true 来覆盖该行为.

@TransactionalEventListener 仅适用于由 PlatformTransactionManager 管理的线程绑定事务. 由 ReactiveTransactionManager 管理的响应式事务使用的是 Reactor 上下文而不是线程局部属性,因此事件监听器,没有可以参与的活动事务.

1.8. 与特定应用程序服务器集成

Spring 的事务抽象通常与应用程序服务器无关. 此外,Spring 的 JtaTransactionManager 类 (可以选择对 JTA UserTransactionTransactionManager 对象执行 JNDI 查找) 自动检测下一个对象的位置,该位置因应用程序服务器而异. 有权访问 JTA TransactionManager 允许增强的事务语义—​特别是支持事务挂起. 有关详细信息,请参阅 JtaTransactionManager文档.

Spring 的 JtaTransactionManager 是在 Java EE 应用程序服务器上运行的标准选择,并且已知可在所有常见服务器上运行. 高级功能(例如事务挂起) 也适用于许多服务器(包括 GlassFish、JBoss 和 Geronimo) ,无需任何特殊配置. 但是,对于完全支持的事务挂起和进一步的高级集成,Spring 包含用于 WebLogic Server 和 WebSphere 的特殊适配器. 以下各节将讨论这些适配器.

对于标准方案(包括 WebLogic Server 和 WebSphere) ,请考虑使用便捷的 <tx:jta-transaction-manager/> 配置元素. 配置后,此元素会自动检测服务器类型并选择可用于平台的最佳事务管理器. 这意味着您无需显式配置特定于服务器的适配器类(如以下部分所述) . 相反,它们是自动选择的,标准的 JtaTransactionManager 作为默认的 fallback.

1.8.1. IBM WebSphere

在 WebSphere 6.1.0.9 及更高版本中,要用推荐 Spring JTA 事务管理器是 WebSphereUowTransactionManager. 此适配器使用 IBM 的 UOWManager API,该 API 在 WebSphere 应用程序服务器 6.1.0.9 及更高版本中可用. 使用此适配器,IBM 正式支持 Spring 驱动的事务挂起(由 PROPAGATION_REQUIRES_NEW 启动的事务的挂起和恢复) .

1.8.2. Oracle WebLogic Server

在 WebLogic Server 9.0 或更高版本上,通常使用 WebLogicJtaTransactionManager 而不是原有的 JtaTransactionManager 类. 这个普通 JtaTransactionManager 的特殊 WebLogic 特定子类在 WebLogic 管理的事务环境中支持 Spring 定义的事务的全部功能,超出了标准的 JTA 语义. 这些功能包括事务名称,事务隔离级别以及在所有情况下正确恢复事务.

1.9. 常见问题的解决方案

本节介绍了一些常见问题的解决方案.

1.9.1. 使用错误的事务管理器来获取指定的 DataSource

根据您选择的事务技术和要求,使用正确的 PlatformTransactionManager 实现. 如果使用得当,Spring Framework 只提供了简单易用的抽象. 如果使用全局事务,则必须对所有事务操作使用 org.springframework.transaction.jta.JtaTransactionManager 类(或它的 特定于应用程序服务器的子类 ) . 否则,事务基础结构尝试在诸如容器 DataSource 实例之类的资源上执行本地事务. 这样的本地事务没有意义,好的应用程序服务器将它们视为错误.

1.10. 更多资源

有关 Spring Framework 的事务支持的更多信息,请参阅:

  • Spring 中的分布式事务 有或没有 XA 是一个 JavaWorld 演示文稿, 其中 Spring 的 David Syer 将引导您完成 Spring 应用程序中分布式事务的七种模式,其中三种使用 XA,四种不使用.

  • Java 事务设计策略 是 available from InfoQ 提供的一本书,它提供了对 Java 中事务的快速介绍. 它还包括如何使用 Spring Framework 和 EJB3 配置和使用事务的并排示例.

2. DAO 支持

Spring 中的数据访问对象(DAO) 支持旨在使您能够以一致的方式轻松使用数据访问技术(如 JDBC,Hibernate 或 JPA) . 这使您可以非常轻松地在上述持久性技术之间切换,并且还可以让您编写代码而无需担心捕获特定于每种技术的异常.

2.1. 一致的异常层次结构

Spring 提供了从特定于技术的异常(例如 SQLException) 到其自己的异常类层次结构的便捷转换, 该异常类层次结构将 DataAccessException 作为父异常. 这些异常包装了原始异常,因此您永远不会丢失任何可能出错的信息.

除了 JDBC 异常之外,Spring 还可以包装特定于 JPA 和 Hibernate 的异常,将它们转换为一组集中的运行时异常. 这使您可以仅在适当的层中处理大多数不可恢复的持久性异常,而无需在 DAO 中使用恼人的样板 catch-and-throw 块和异常声明. (您仍然可以在任何需要的位置捕获和处理异常. ) 如上所述,JDBC 异常(包括特定于数据库的方言) 也会转换为相同的层次结构, 这意味着您可以在一致的编程模型中使用 JDBC 执行某些操作.

前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类. 如果使用基于拦截器的类, 则应用程序必须关心处理 HibernateExceptionsPersistenceExceptions 本身, 最好分别委托 SessionFactoryUtilsconvertHibernateAccessException(..)convertJpaAccessException() 方法. 这些方法将异常转换为与 org.springframework.dao 异常层次结构中兼容的异常. 由于 PersistenceExceptions 未被检测的异常,它们也可能被抛出(尽管就异常而言牺牲了通用 DAO 抽象) .

下图显示了 Spring 提供的异常层次结构. (请注意,图中详细说明的类层次结构仅显示整个 DataAccessException 层次结构的子集. )

DataAccessException

2.2. 用于配置 DAO 或存储库类的注解

保证数据访问对象(DAO) 或存储库提供异常转换的最佳方法是使用 @Repository 注解. 此注解还允许组件扫描查找支持和配置 DAO 和存储库,而无需为它们提供 XML 配置. 以下示例显示如何使用 @Repository 注解:

Java
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
    // ...
}
1 @Repository 注解.
Kotlin
@Repository (1)
class SomeMovieFinder : MovieFinder {
    // ...
}
1 @Repository 注解.

任何 DAO 或存储库实现都需要访问持久化资源,这具体取决于所使用的持久性技术. 例如,基于 JDBC 的存储库需要访问 JDBC DataSource,而基于 JPA 的存储库需要访问 EntityManager. 完成此操作的最简单方法是使用 @Autowired@Inject@Resource@PersistenceContext 注解之一注入此资源依赖. 以下示例适用于 JPA 存储库:

Java
@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...
}
Kotlin
@Repository
class JpaMovieFinder : MovieFinder {

    @PersistenceContext
    private lateinit var entityManager: EntityManager

    // ...
}

如果您使用经典的 Hibernate API,则可以注入 SessionFactory,如以下示例所示:

Java
@Repository
public class HibernateMovieFinder implements MovieFinder {

    private SessionFactory sessionFactory;

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    // ...
}
Kotlin
@Repository
class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder {
    // ...
}

我们在这里展示的最后一个例子是典型的 JDBC 支持. 您可以将 DataSource 注入到初始化方法中, 您可以使用此 DataSource 创建 JdbcTemplate 和其他数据访问支持类(如 SimpleJdbcCall 等) . 以下示例自动装配 DataSource:

Java
@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void init(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // ...
}
Kotlin
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    // ...
}
有关如何利用注解的详细信息配置应用程序上下文,请参阅每种持久性技术的具体内容.

3. 使用 JDBC 进行数据访问

Spring Framework JDBC 抽象提供的值通过下表中列出的操作序列显示. 该表显示了 Spring 负责的操作以及您负责的操作.

Table 4. Spring JDBC - who does what?
Action Spring You

定义连接参数

X

打开连接

X

指定 SQL 语句

X

声明参数并提供参数的值

X

准备并执行语句

X

通过结果集(如果有) 设置迭代器循环

X

为每次迭代做好工作.

X

处理任务异常

X

处理事务.

X

关闭连接、语句和结果集

X

Spring Framework 负责处理所有可能使 JDBC 成为繁琐 API 的低级细节.

3.1. 选择 JDBC 数据库访问方法

您可以选择多种方法来构成 JDBC 数据库访问的基础. 除了三种类型的 JdbcTemplate 之外, 新的 SimpleJdbcInsertSimpleJdbcCall 方法还优化了数据库元数据, 而 RDBMS Object 样式采用了类似于 JDO Query 设计的面向对象方法. 一旦开始使用这些方法之一,您仍然可以混合和匹配以包含来自不同方法的功能. 所有方法都需要 JDBC 2.0 兼容的驱动程序,而某些高级功能需要 JDBC 3.0 驱动程序.

  • JdbcTemplate 是经典且最流行的 Spring JDBC 方法. 这种 "最低级别" 的方法和所有其他方法都使用了 JdbcTemplate.

  • NamedParameterJdbcTemplate 包装 JdbcTemplate 以提供命名参数而不是传统的JDBC ? 占位符. 当您有多个 SQL 语句参数时,此方法可提供更好的文档和易用性.

  • SimpleJdbcInsertSimpleJdbcCall 优化数据库元数据以限制必要的配置量. 此方法简化了编码,因此您只需提供表或过程的名称,并提供与列名匹配的参数映射. 仅当数据库提供足够的元数据时,这才有效. 如果数据库未提供此元数据,则必须提供参数的显式配置.

  • RDBMS - 对象(包括 MappingSqlQuerySqlUpdateStoredProcedure) 要求您在数据访问层初始化期间创建可重用且线程安全的对象. 此方法以 JDO Query 为模型,其中您定义查询字符串、声明参数和编译查询. 执行此操作后,可以使用各种参数值多次调用 execute(…​), update(…​), 和 findObject(…​) 方法.

3.2. 包层次结构

Spring Framework 的 JDBC 抽象框架由四个不同的包组成:

  • core: org.springframework.jdbc.core 包中包含 JdbcTemplate 类及其各种回调接口,以及各种相关类. 名为 org.springframework.jdbc.core.simple 的子包包含 SimpleJdbcInsertSimpleJdbcCall 类. 另一个名为 org.springframework.jdbc.core.namedparam 的子包包含 NamedParameterJdbcTemplate 类和相关的支持类. 请参阅使用 JDBC 核心类控制基本 JDBC 处理和错误处理, JDBC 批处理操作以及使用 SimpleJdbc 类简化 JDBC 操作.

  • datasource: org.springframework.jdbc.datasource 包中包含一个用于轻松访问 DataSource 的实用程序类 和各种简单的 DataSource 实现,可用于在 Java EE 容器外测试和运行未修改的 JDBC 代码. 名为 org.springfamework.jdbc.datasource.embedded 的子包提供了使用 Java 数据库引擎(如HSQL,H2和Derby) 创建嵌入式数据库的支持. 请参阅控制数据库连接嵌入式数据库支持.

  • object: org.springframework.jdbc.object 包中包含将 RDBMS 查询,更新和存储过程表示为线程安全,可重用对象的类. 请参阅将将 JDBC 操作建模为 Java 对象. 这种方法由 JDO 建模,尽管查询返回的对象自然与数据库断开连接. 这种更高级别的 JDBC 抽象取决于 org.springframework.jdbc.core 包中的低级抽象.

  • support: org.springframework.jdbc.support 包提供 SQLException 转换功能和一些实用程序类. JDBC 处理期间抛出的异常将转换为 org.springframework.dao 包中定义的异常. 这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理. 所有已转换的异常都是未被检测的,这使您可以选择捕获可恢复的异常,同时将其他异常传播给调用方. 请参阅使用 使用 SQLExceptionTranslator.

3.3. 使用 JDBC 核心类控制基本 JDBC 处理和错误处理

本节介绍如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理. 它包括以下主题:

3.3.1. 使用 JdbcTemplate

JdbcTemplate 是 JDBC 核心包中的中心类. 它处理资源的创建和释放,帮助您避免常见错误,例如忘记关闭连接. 它执行核心 JDBC 工作流的基本任务(例如语句创建和执行) ,并根据应用程序提供的 SQL 代码提取结果. JdbcTemplate 类:

  • 执行 SQL 查询

  • 更新语句和存储过程调用

  • ResultSet 实例执行迭代并提取返回的参数值.

  • 捕获 JDBC 异常并将它们转换为 org.springframework.dao 包中定义的通用的、信息量更大的异常层次结构. (请参阅一致的异常层次结构. )

当您为代码使用 JdbcTemplate 时,您只需要实现回调接口,为它们提供明确定义的合同. 给定 JdbcTemplate 类提供的 Connection,PreparedStatementCreator 回调接口创建一个预准备语句,提供 SQL 和任何必要的参数. CallableStatementCreator 接口也是如此,它创建了可调用语句. RowCallbackHandler 接口从 ResultSet 的每一行中提取值.

您可以通过引用 DataSource 的直接实例,在 DAO 实现中使用 JdbcTemplate, 也可以在 Spring IoC 容器中对其进行配置,并将其作为 bean 引用提供给 DAO.

应始终将 DataSource 配置为 Spring IoC 容器中的 bean. 在第一种情况下,bean 可以直接在服务中使用; 在第二种情况下,它被给予准备好的模板.

此类执行的所有 SQL 都记录在模板实例的完全限定类名下的 DEBUG 日志级别 (通常为 JdbcTemplate,但如果使用 JdbcTemplate 类的自定义子类,则可能会有所不同) .

以下部分提供了一些 JdbcTemplate 用法的示例. 这些示例并不是 JdbcTemplate 暴露的所有功能. 具体请参阅 javadoc.

查询 (SELECT)

以下查询获取相关的行数:

Java
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
Kotlin
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!

以下查询使用绑定变量:

Java
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
Kotlin
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
        "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!

以下查询查找 String:

Java
String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        String.class, 1212L);
Kotlin
val lastName = this.jdbcTemplate.queryForObject<String>(
        "select last_name from t_actor where id = ?",
        arrayOf(1212L))!!

以下查询查找并填充单个 domain 对象:

Java
Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        },
        1212L);
Kotlin
val actor = jdbcTemplate.queryForObject(
            "select first_name, last_name from t_actor where id = ?",
            arrayOf(1212L)) { rs, _ ->
        Actor(rs.getString("first_name"), rs.getString("last_name"))
    }

以下查询查找并填充多个 domain 对象:

Java
List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });
Kotlin
val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ ->
        Actor(rs.getString("first_name"), rs.getString("last_name"))

如果最后两个代码片段实际存在于同一个应用程序中,那么删除两个 RowMapper lambda 表达式中重复的代码段并将他们提取到一个字段 ,当需要时通过 DAO 方法引用它. 例如,最好重写前面的代码片段,如下所示:

Java
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", actorRowMapper);
}
Kotlin
val actorMapper = RowMapper<Actor> { rs: ResultSet, rowNum: Int ->
    Actor(rs.getString("first_name"), rs.getString("last_name"))
}

fun findAllActors(): List<Actor> {
    return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper)
}
使用 JdbcTemplate 更新 (INSERT, UPDATE, and DELETE)

您可以使用 update(..) 方法执行插入,更新和删除操作. 参数值通常作为变量参数提供,或者作为对象数组提供.

下面的例子是插入一个新的纪录:

Java
this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");
Kotlin
jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling")

以下例子是更新已存在的纪录:

Java
this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);
Kotlin
jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L)

以下例子是删除一条纪录:

Java
this.jdbcTemplate.update(
        "delete from t_actor where id = ?",
        Long.valueOf(actorId));
Kotlin
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
JdbcTemplate 的其他操作

您可以使用 execute(..) 方法运行任意 SQL. 因此,该方法通常用于 DDL 语句. 它重载了带有回调接口,绑定变量数组等的方法. 以下示例创建一个表:

Java
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
Kotlin
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")

以下示例调用存储过程:

Java
this.jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        Long.valueOf(unionId));
Kotlin
jdbcTemplate.update(
        "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
        unionId.toLong())

稍后 将介绍更复杂的存储过程支持.

JdbcTemplate 最佳实践

一旦配置,JdbcTemplate 类的实例就是线程安全的. 这很重要,因为这意味着您可以配置 JdbcTemplate 的单个实例,然后将此共享引用安全地注入多个 DAO (或存储库) . JdbcTemplate 是有状态的,因为它维护对 DataSource 的引用,但此状态不是会话状态.

使用 JdbcTemplate 类(以及关联的NamedParameterJdbcTemplate class) 类) 时的常见做法是在 Spring 配置文件中配置 DataSource, 然后将共享 DataSource bean 依赖注入到 DAO 类中. JdbcTemplate 是在 DataSource 的 setter 中创建的. 这导致 DAO 类似于以下内容:

Java
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
Kotlin
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}

以下示例显示了相应的 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

显式配置的替代方法是使用组件扫描和注解支持依赖注入. 在这种情况下,您可以使用 @Repository 注解该类(这使其成为组件扫描的候选者) 并使用 @Autowired 注解 DataSource 的 setter 方法. 以下示例显示了如何执行此操作:

Java
@Repository (1)
public class JdbcCorporateEventDao implements CorporateEventDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired (2)
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource); (3)
    }

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 使用 @Repository 注解类
2 使用 @Autowired 注解 DataSource 的 setter 方法
3 使用 DataSource 创建一个新的 JdbcTemplate.
Kotlin
@Repository (1)
class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2)

    private val jdbcTemplate = JdbcTemplate(dataSource) (3)

    // JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 使用 @Repository 注解类
2 使用 @Autowired 注解 DataSource 的 setter 方法
3 使用 DataSource 创建一个新的 JdbcTemplate.

以下示例显示了相关的 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- Scans within the base package of the application for @Component classes to configure as beans -->
    <context:component-scan base-package="org.springframework.docs.test" />

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <context:property-placeholder location="jdbc.properties"/>

</beans>

如果使用 Spring 的 JdbcDaoSupport 类,并且支持 JDBC 的各种 DAO 类从它扩展,继承 JdbcDaoSupport 类, 子类可以获取 setDataSource(..) 方法. 您可以选择是否继承此类. JdbcDaoSupport 类仅为方便起见而提供.

无论您选择使用(或不使用) 上述哪种模板初始化样式,每次要运行 SQL 时,很少需要创建 JdbcTemplate 类的新实例. 配置完成后, JdbcTemplate 实例是线程安全的. 如果您的应用程序访问多个数据库, 您可能需要多个 JdbcTemplate 实例,这需要多个 DataSources, 随后需要多个不同配置的 JdbcTemplate 实例.

3.3.2. 使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate 类通过使用命名参数添加了对编写 JDBC 语句的支持, 而不是仅使用经典占位符( '?') 参数编写 JDBC 语句. NamedParameterJdbcTemplate 类包装了 JdbcTemplate 并委托包装的 JdbcTemplate 来完成其大部分工作. 本节仅描述 NamedParameterJdbcTemplate 类与 JdbcTemplate 本身不同的区域 - 即使用命名参数编写 JDBC 语句. 以下示例显示如何使用 NamedParameterJdbcTemplate:

Java
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Kotlin
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
    val sql = "select count(*) from T_ACTOR where first_name = :first_name"
    val namedParameters = MapSqlParameterSource("first_name", firstName)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

请注意在分配给 sql 变量的值中使用命名参数表示法以及插入 namedParameters 变量(MapSqlParameterSource 类型) 的相应值.

或者,您可以使用基于 Map 的样式将命名参数及其对应值传递给 NamedParameterJdbcTemplate 实例. NamedParameterJdbcOperations 暴露并由 NamedParameterJdbcTemplate 类实现的其余方法遵循类似的模式,此处不再介绍.

以下示例显示了基于 Map 的样式的使用:

Java
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActorsByFirstName(firstName: String): Int {
    val sql = "select count(*) from T_ACTOR where first_name = :first_name"
    val namedParameters = mapOf("first_name" to firstName)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

NamedParameterJdbcTemplate 相关的一个很好的功能(并且存在于同一个 Java 包中) 是 SqlParameterSource 接口. 您已经在之前的一个代码片段(MapSqlParameterSource 类) 中看到了此接口的实现示例. SqlParameterSourceNamedParameterJdbcTemplate 的命名参数值的来源. MapSqlParameterSource 类是一个简单的实现,它是 java.util.Map 的适配器,其中键是参数名称,值是参数值.

另一个 SqlParameterSource 实现是 BeanPropertySqlParameterSource 类. 此类包装任意 JavaBean(即,遵循 JavaBean 约定的类的实例) , 并使用包装的 JavaBean 的属性作为命名参数值的来源.

以下示例展示了典型的 JavaBean:

Java
public class Actor {

    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public Long getId() {
        return this.id;
    }

    // setters omitted...

}
Kotlin
data class Actor(val id: Long, val firstName: String, val lastName: String)

以下示例使用 NamedParameterJdbcTemplate 返回上一示例中显示的类成员的计数:

Java
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

    // notice how the named parameters match the properties of the above 'Actor' class
    String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

    SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Kotlin
// some JDBC-backed DAO class...
private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

fun countOfActors(exampleActor: Actor): Int {
    // notice how the named parameters match the properties of the above 'Actor' class
    val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"
    val namedParameters = BeanPropertySqlParameterSource(exampleActor)
    return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!!
}

请记住,NamedParameterJdbcTemplate 类包装了一个经典的 JdbcTemplate 模板. 如果需要访问包装的 JdbcTemplate 实例 中的功能, 则可以使用 getJdbcOperations() 方法通过 JdbcOperations 接口访问包装的 JdbcTemplate.

有关在应用程序上下文中使用 NamedParameterJdbcTemplate 类的指导,另请参阅 JdbcTemplate 最佳实践.

3.3.3. 使用 SQLExceptionTranslator

SQLExceptionTranslator 是一个由类实现的接口, 可以在 SQLExceptions 和 Spring 自己的 org.springframework.dao.DataAccessException 之间进行转换,这与数据访问策略无关. 此实现可以是通用的(例如,使用 JDBC 的 SQLState 代码) 或专有的(例如,使用 Oracle 错误代码) 以获得更高的精度.

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator 的默认实现. 此实现使用特定 Provicer 代码. 它比 SQLState 实现更精确. 错误代码转换的代码在 SQLErrorCodes 的 JavaBean 类型中. 此类由 SQLErrorCodesFactory 创建和填充,SQLErrorCodesFactory(顾名思义) 是一个工厂, 用于根据名为 sql-error-codes.xml 的配置文件的内容创建 SQLErrorCodes. 此文件使用 Provicer 代码填充,并基于从 DatabaseMetaData 获取的 DatabaseProductName. 使用您正在使用的实际数据库的代码.

SQLErrorCodeSQLExceptionTranslator 按以下顺序应用匹配规则:

  1. 由子类实现的任何自定义转换. 通常,使用提供的具体 SQLErrorCodeSQLExceptionTranslator, 因此该规则不适用. 仅当您实际提供了子类实现时才适用.

  2. SQLExceptionTranslator 接口的任何自定义实现,提供给 SQLErrorCodes 类的 customSqlExceptionTranslator 属性.

  3. 搜索 CustomSQLErrorCodesTranslation 类的实例列表(为 SQLErrorCodes 类的 customTranslations 属性提供) 以查找匹配项.

  4. 应用错误码匹配.

  5. 使用备用转换器. SQLExceptionSubclassTranslator 是默认的备用转换器. 如果此转换不可用,则下一个备用转换器是 SQLStateSQLExceptionTranslator.

默认情况下,SQLErrorCodesFactory 用于定义 Error 代码和自定义异常转换. 从类路径中查找名为 sql-error-codes.xml 的文件, 并根据正在使用的数据库的数据库元数据中的数据库名称找到匹配的 SQLErrorCodes 实例.

您可以继承 SQLErrorCodeSQLExceptionTranslator,如以下示例所示:

Java
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {

    protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
        if (sqlEx.getErrorCode() == -12345) {
            return new DeadlockLoserDataAccessException(task, sqlEx);
        }
        return null;
    }
}
Kotlin
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {

    override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? {
        if (sqlEx.errorCode == -12345) {
                return DeadlockLoserDataAccessException(task, sqlEx)
            }
            return null
    }
}

在前面的示例中,特定错误码(-12345) 将被转换,而其他错误由默认转换器的实现转换. 要使用此自定义转换程序,必须通过方法 setExceptionTranslator 将其传递给 JdbcTemplate, 并且必须将此 JdbcTemplate 用于需要此转换程序的所有数据访问处理. 以下示例显示了如何使用此自定义转换器:

Java
private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {

    // create a JdbcTemplate and set data source
    this.jdbcTemplate = new JdbcTemplate();
    this.jdbcTemplate.setDataSource(dataSource);

    // create a custom translator and set the DataSource for the default translation lookup
    CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
    tr.setDataSource(dataSource);
    this.jdbcTemplate.setExceptionTranslator(tr);

}

public void updateShippingCharge(long orderId, long pct) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate.update("update orders" +
        " set shipping_charge = shipping_charge * ? / 100" +
        " where id = ?", pct, orderId);
}
Kotlin
// create a JdbcTemplate and set data source
private val jdbcTemplate = JdbcTemplate(dataSource).apply {
    // create a custom translator and set the DataSource for the default translation lookup
    exceptionTranslator = CustomSQLErrorCodesTranslator().apply {
        this.dataSource = dataSource
    }
}

fun updateShippingCharge(orderId: Long, pct: Long) {
    // use the prepared JdbcTemplate for this update
    this.jdbcTemplate!!.update("update orders" +
            " set shipping_charge = shipping_charge * ? / 100" +
            " where id = ?", pct, orderId)
}

自定义转换器传递一个数据源,以便在 sql-error-codes.xml 中查找错误码.

3.3.4. 执行语句

运行 SQL 语句只需要很少的代码. 您需要一个 DataSource 和一个 JdbcTemplate,包括 JdbcTemplate 提供的便捷方法. 以下示例显示了为创建新表的最小但功能齐全的类所需要包含的内容:

Java
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAStatement {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void doExecute() {
        this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAStatement(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun doExecute() {
        jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
    }
}

3.3.5. 执行查询

某些查询方法返回单个值. 要从一行检索计数或特定值,请使用 queryForObject(..). 后者将返回的 JDBC Type 转换为作为参数传入的 Java 类. 如果类型转换无效,则抛出 InvalidDataAccessApiUsageException. 以下示例包含两个查询方法,一个用于 int,另一个用于查询 String:

Java
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class RunAQuery {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int getCount() {
        return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
    }

    public String getName() {
        return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class RunAQuery(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    val count: Int
        get() = jdbcTemplate.queryForObject("select count(*) from mytable")!!

    val name: String?
        get() = jdbcTemplate.queryForObject("select name from mytable")
}

除了单个结果查询方法之外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目. 最通用的方法是 queryForList(..), 它返回一个 List,其中每个元素都是一个包含每列的一个条目的 Map,使用列名作为键. 如果向前面的示例添加方法以获取所有行的列表,则可能如下所示:

Java
private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

public List<Map<String, Object>> getList() {
    return this.jdbcTemplate.queryForList("select * from mytable");
}
Kotlin
private val jdbcTemplate = JdbcTemplate(dataSource)

fun getList(): List<Map<String, Any>> {
    return jdbcTemplate.queryForList("select * from mytable")
}

返回的列表将类似于以下内容:

[{name=Bob, id=1}, {name=Mary, id=2}]

3.3.6. 更新数据库

以下示例更新某个主键的列:

Java
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;

public class ExecuteAnUpdate {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public void setName(int id, String name) {
        this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
    }
}
Kotlin
import javax.sql.DataSource
import org.springframework.jdbc.core.JdbcTemplate

class ExecuteAnUpdate(dataSource: DataSource) {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun setName(id: Int, name: String) {
        jdbcTemplate.update("update mytable set name = ? where id = ?", name, id)
    }
}

在前面的示例中,SQL 语句具有参数的占位符. 您可以将参数值作为可变参数传递,或者作为对象数组传递. 因此,您应该在原始包装类中显式包装原语,或者您应该使用自动装箱.

3.3.7. 检索自动生成的密钥

update() 方便方法支持检索数据库生成的主键. 此支持是 JDBC 3.0 标准的一部分. 有关详细信息,请参阅规范的第 13.6 章. 该方法将 PreparedStatementCreator 作为其第一个参数,这是指定所需 insert 语句的方式. 另一个参数是 KeyHolder, 它包含从更新成功返回时生成的密钥. 没有标准的单一方法来创建适当的 PreparedStatement (这解释了为什么方法签名就是这样) . 以下示例适用于 Oracle,但可能无法在其他平台上运行:

Java
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";

KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
    PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
    ps.setString(1, name);
    return ps;
}, keyHolder);

// keyHolder.getKey() now contains the generated key
Kotlin
val INSERT_SQL = "insert into my_test (name) values(?)"
val name = "Rob"

val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({
    it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) }
}, keyHolder)

// keyHolder.getKey() now contains the generated key

3.4. 控制数据库连接

本节包括:

3.4.1. 使用 DataSource

Spring 通过 DataSource 获取与数据库的连接. DataSource 是 JDBC 规范的一部分,是一个通用的连接工厂. 它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题. 作为开发人员,您无需了解有关如何连接到数据库的详细信息. 这是设置数据源的管理员的责任. 您最有可能在开发和测试代码时填充这两个角色,但您不必知道如何配置生产数据源.

使用 Spring 的 JDBC 层时,可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现配置自己的数据源. 传统的实现是 Apache Commons DBCP 和 C3P0. 对于现代 JDBC 连接池,请考虑使用 HikariCP.

您应该仅将 DriverManagerDataSourceSimpleDriverDataSource 类用于测试目的,因为它不提供连接池,并且在发出多个连接请求时性能很差.

以下部分使用 Spring 的 DriverManagerDataSource 实现. 稍后将介绍其他 DataSource.

配置 DriverManagerDataSource:

  1. 通常获取 JDBC 连接时,获取与 DriverManagerDataSource 的连接.

  2. 指定 JDBC 驱动程序的完全限定类名,以便 DriverManager 可以加载驱动程序类.

  3. 提供不同 JDBC 驱动程序之间的 URL. (有关正确的值,请参阅驱动程序的文档. )

  4. 提供连接到数据库的用户名和密码.

以下示例显示了如何在 Java 中配置 DriverManagerDataSource

Java
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
Kotlin
val dataSource = DriverManagerDataSource().apply {
    setDriverClassName("org.hsqldb.jdbcDriver")
    url = "jdbc:hsqldb:hsql://localhost:"
    username = "sa"
    password = ""
}

以下示例显示了相关的 XML 配置:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

接下来的两个示例显示了 DBCP 和 C3P0 的基本连接和配置. 要了解有助于控制连接池功能的更多选项,请参阅相应连接池实现的产品文档.

以下示例展示了 DBCP 的配置:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

以下示例展示了 C3P0 的配置:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="${jdbc.driverClassName}"/>
    <property name="jdbcUrl" value="${jdbc.url}"/>
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

<context:property-placeholder location="jdbc.properties"/>

3.4.2. 使用 DataSourceUtils

DataSourceUtils 类是一个方便且功能强大的辅助类,它提供 static 方法可以从 JNDI 获取连接并关闭连接. 它支持线程绑定连接,例如,DataSourceTransactionManager.

3.4.3. 实现 SmartDataSource

SmartDataSource 接口应该由可以提供与关系型数据库的连接的类实现. 它继承了 DataSource 接口,让使用它的类查询是否应该在给定操作后关闭连接. 当您知道需要重用连接时,此用法很有效.

3.4.4. 继承 AbstractDataSource

AbstractDataSource 是 Spring 的 DataSource 实现的 abstract 基类. 它实现了所有 DataSource 共有的代码. 如果编写自己的 DataSource 实现,则应继承 AbstractDataSource 类.

3.4.5. 使用 SingleConnectionDataSource

SingleConnectionDataSource 类是 SmartDataSource 接口的一个实现,它包装了每次使用后未关闭的单个 Connection. 这不是多线程的.

如果任何客户端代码在连接池时调用 close(如使用持久性工具时) ,则应将 suppressClose 属性设置为 true. 此设置将返回一个包装了物理连接并且禁止关闭的代理. 请注意,您不能再将其强制转换为本地 Oracle Connection 或类似对象.

SingleConnectionDataSource 主要是一个测试类. 例如,它可以在简单的 JNDI 环境中轻松测试应用程序服务器外部的代码. 与 DriverManagerDataSource 相比,它始终重用相同的连接,避免过度创建物理连接.

3.4.6. 使用 DriverManagerDataSource

DriverManagerDataSource 类是标准 DataSource 接口的实现, 它通过 bean 属性配置普通 JDBC 驱动程序,并且每次都返回一个新的 Connection.

此实现对于 Java EE 容器外部的测试和独立环境非常有用,可以作为 Spring IoC 容器中的 DataSource bean, 也可以与简单的 JNDI 环境结合使用. 连接池调用 Connection.close() 关闭连接, 因此任何支持 DataSource 的持久性代码都应该有效. 但是,即使在测试环境中,使用 JavaBean 样式的连接池(例如 commons-dbcp) 也非常容易, 因此使用这样的连接池几乎总是优先于 DriverManagerDataSource.

3.4.7. 使用 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy 是目标 DataSource 的代理. 代理包装目标 DataSource 以添加对 Spring 管理的事务的感知. 在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource.

除非必须调用已经存在的代码并传递标准 JDBC DataSource 接口实现,否则很少需要使用此类. 在这种情况下,您仍然可以使用此代码,同时让此代码参与 Spring 管理事务. 通常最好使用更高级别的资源抽象化管理,编写自己的新代码,例如 JdbcTemplateDataSourceUtils.

更多详细资料,请参阅 TransactionAwareDataSourceProxy javadoc

3.4.8. 使用 DataSourceTransactionManager

DataSourceTransactionManager 类是单个 JDBC 数据源的 PlatformTransactionManager 实现. 它将 JDBC 连接从指定的数据源绑定到当前正在执行的线程,可能允许每个数据源连接一个线程.

需要应用程序代码来通过 DataSourceUtils.getConnection(DataSource) 而不是 Java EE 的标准 DataSource.getConnection 来获取 JDBC 连接. 它会抛出未经检查的 org.springframework.dao 异常,而不是受检查的 SQLExceptions. 所有框架类(例如 JdbcTemplate) 都隐式使用此策略. 如果未与此事务管理器一起使用,则查找策略的行为与常见策略完全相同. 因此,它可以在任何情况下使用.

DataSourceTransactionManager 类支持自定义隔离级别和超时,这些隔离级别和超时将作为适当的 JDBC 语句查询超时应用. 要支持后者,应用程序代码必须使用 JdbcTemplate 或为每个创建的语句调用 DataSourceUtils.applyTransactionTimeout(..) 方法.

您可以在单资源情况下使用此实现而不是 JtaTransactionManager,因为它不需要容器支持 JTA. 如果您坚持所需的连接查找模式,则在两者之间切换只是配置问题. JTA 不支持自定义隔离级别.

3.5. JDBC 批处理操作

如果批量多次调用同一个预准备语句,则大多数 JDBC 驱动程序都可以提高性能. 通过将更新分组到批次中,可以限制程序到数据库的往返次数.

3.5.1. 使用 JdbcTemplate 进行基础的批处理操作

您可以通过实现特殊接口 BatchPreparedStatementSetter 的两个方法来完成 code>JdbcTemplate 批处理, 并将该实现作为 batchUpdate 方法调用中的第二个参数传递. 您可以使用 getBatchSize 方法提供当前批次的大小. 您可以使用 setValues 方法设置预准备语句的参数值. 此方法您在 getBatchSize 调用中指定的次数. 以下示例根据列表中的条目更新 t_actor 表,并将整个列表用作批处理:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
                .withFunctionName("get_actor_name");
    }

    public String getActorName(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        String name = funcGetActorName.executeFunction(String.class, in);
        return name;
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                object: BatchPreparedStatementSetter {
                    override fun setValues(ps: PreparedStatement, i: Int) {
                        ps.setString(1, actors[i].firstName)
                        ps.setString(2, actors[i].lastName)
                        ps.setLong(3, actors[i].id)
                    }

                    override fun getBatchSize() = actors.size
                })
    }

    // ... additional methods
}

如果您处理更新流或从文件读取,则可能具有首选的批次大小,但最后一批可能没有该数量的条目. 在这种情况下,您可以使用 InterruptibleBatchPreparedStatementSetter 接口,该接口允许您在输入源耗尽时中断批处理. isBatchExhausted 方法允许您发出批处理结束的信号.

3.5.2. 使用对象列表进行批处理操作

JdbcTemplateNamedParameterJdbcTemplate 都提供了另一种提供批量更新的方法. 您可以将调用中的所有参数值作为列表提供,而不是实现特殊的批处理接口. 框架循环遍历这些值并使用内部预处理的 setter 语句. API 会有所不同,具体取决于您是否使用命名参数. 对于命名参数,您提供了一个 SqlParameterSource 数组,该批处理的每个成员都有一个条目. 您可以使用 SqlParameterSourceUtils.createBatch 方便方法来创建此数组, 传入一个 bean 对象数组(使用与参数对应的 getter 方法) ,String 关键字的 Map 实例(包含相应参数作为值) ,或者两者混合使用.

一下示例展示使用命名参数的批量更新:

Java
public class JdbcActorDao implements ActorDao {

    private NamedParameterTemplate namedParameterJdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }

    public int[] batchUpdate(List<Actor> actors) {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }

        // ... additional methods
}

对于使用经典的 SQL 语句 ? 在占位符中,传入包含具有更新值的对象数组的列表. 此对象数组必须在 SQL 语句中为每个占位符分配一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同.

以下示例与前面的示例相同,只是它使用经典JDBC ? 占位符:

Java
public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[] batchUpdate(final List<Actor> actors) {
        List<Object[]> batch = new ArrayList<Object[]>();
        for (Actor actor : actors) {
            Object[] values = new Object[] {
                    actor.getFirstName(), actor.getLastName(), actor.getId()};
            batch.add(values);
        }
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): IntArray {
        val batch = mutableListOf<Array<Any>>()
        for (actor in actors) {
            batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
        }
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?", batch)
    }

    // ... additional methods
}

我们之前描述的所有批处理更新方法都返回一个 int 数组,其中包含每个批处理条目的受影响行数. JDBC 驱动程序报告此计数. 如果计数不可用,则 JDBC 驱动程序返回值 -2.

在这种情况下,通过在底层 PreparedStatement 上自动设置值,需要从给定的 Java 类型扩展每个值的相应 JDBC 类型. 虽然这通常很有效,但是存在潜在的问题(例如,使用包含 Map 的 null 值) . 默认情况下,Spring 会在这种情况下调用 ParameterMetaData.getParameterType,这对于 JDBC 驱动程序来说可能开销很大. 您应该使用最新的驱动程序版本并考虑将 spring.jdbc.getParameterType.ignore 属性设置为 true (作为 JVM 系统属性或类路径根目录中的 SpringProperties 文件) , 如果遇到性能问题 - 例如 ,如 Oracle 12c JBoss and PostgreSQL 所述.

或者您可以考虑通过 BatchPreparedStatementSetter(如前所示) ; 或者通过给予基于 List <Object[]> 的调用的显式类型数组; 或者通过在自定义 MapSqlParameterSource 的实例上 调用 registerSqlType; 或者通过从 Java 声明的属性类型扩展 SQL 类型- BeanPropertySqlParameterSource(即使对于空值) 来显式指定相应的 JDBC 类型.

3.5.3. 多批次批处理操作

前面的批量更新示例处理的批量非常大,您希望将它们分成几个较小的批次. 您可以通过多次调用 batchUpdate 方法,使用前面提到的方法执行此操作,但现在有一种更方便的方法. 除了 SQL 语句之外,此方法还包含一个包含参数的 Collection 对象,为每个批处理生成更新的数量, 以及一个 ParameterizedPreparedStatementSetter,用于设置预准备语句的参数值. 框架循环提供的值,并将更新调用分解为指定大小的批处理.

以下示例显示批处理更新,批处理大小为 100:

Java
public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    public int[][] batchUpdate(final Collection<Actor> actors) {
        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                actors,
                100,
                (PreparedStatement ps, Actor actor) -> {
                    ps.setString(1, actor.getFirstName());
                    ps.setString(2, actor.getLastName());
                    ps.setLong(3, actor.getId().longValue());
                });
        return updateCounts;
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource)

    fun batchUpdate(actors: List<Actor>): Array<IntArray> {
        return jdbcTemplate.batchUpdate(
                    "update t_actor set first_name = ?, last_name = ? where id = ?",
                    actors, 100) { ps, argument ->
            ps.setString(1, argument.firstName)
            ps.setString(2, argument.lastName)
            ps.setLong(3, argument.id)
        }
    }

    // ... additional methods
}

此调用的批处理更新方法返回一个 int 数组的数组,其中包含每个批处理的数组条目,和每次更新的受影响行数的数组. 顶级数组的长度表示执行的批次数,第二级数组的长度表示该批次中的更新数. 每个批次中的更新数量应该是由整个批处理提供的批次大小(最后一个批次可能更少) ,具体取决于提供的更新对象总数. 每个更新语句的更新计数是由 JDBC 驱动程序报告的. 如果计数不可用,则 JDBC 驱动程序返回值 -2.

3.6. 使用 SimpleJdbc 类简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall 类可通过 JDBC 驱动程序获取的数据库元数据来提供简化的配置. 这意味着您可以减少预先配置,但如果您希望提供代码中的所有详细信息,则可以覆盖或关闭元数据处理.

3.6.1. 使用 SimpleJdbcInsert 插入数据

我们首先使用最少量的配置选项查看 SimpleJdbcInsert 类. 您应该在数据访问层的初始化方法中实例化 SimpleJdbcInsert. 对于此示例,初始化方法是 setDataSource 方法. 您不需要继承 SimpleJdbcInsert 类的子类. 相反,您可以使用 withTableName 方法创建新实例并设置表名. 此类的配置方法遵循返回 SimpleJdbcInsert 实例的 fluid 样式,该样式允许您链接所有配置方法. 以下示例仅使用一种配置方法(稍后我们将显示多个方法的示例) :

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(3);
        parameters.put("id", actor.getId());
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        insertActor.execute(parameters);
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")

    fun add(actor: Actor) {
        val parameters = mutableMapOf<String, Any>()
        parameters["id"] = actor.id
        parameters["first_name"] = actor.firstName
        parameters["last_name"] = actor.lastName
        insertActor.execute(parameters)
    }

    // ... additional methods
}

这里使用的 execute 方法将普通的 java.util.Map 作为唯一参数. 这里要注意的重要事项是,用于 Map 的键必须与表的列名匹配,如数据库中所定义. 这是因为我们读取了元数据来构造实际的 insert 语句.

3.6.2. 使用 SimpleJdbcInsert 检索自动生成的密钥

下一个示例使用与前面示例相同的插入,但是,它不是传入 id,而是获取自动生成的键并将其设置在新的 Actor 对象上. 在创建 SimpleJdbcInsert 时,除了指定表名外,它还使用 usingGeneratedKeyColumns 方法指定生成的键列的名称. 以下清单显示了它的工作原理:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor").usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = mapOf(
                "first_name" to actor.firstName,
                "last_name" to actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters);
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

使用第二种方法运行插入时的主要区别在于,您不用将 id 添加到 Map,并调用 executeAndReturnKey 方法. 这将返回一个 java.lang.Number 对象,您可以使用该对象创建域类中使用的数字类型的实例. 您不能依赖所有数据库来返回特定的 Java 类. java.lang.Number 是您可以依赖的基类. 如果您有多个自动生成的列或生成的值是非数字的,则可以使用从 executeAndReturnKeyHolder 方法返回的 KeyHolder.

3.6.3. 为 SimpleJdbcInsert 指定列

您可以通过使用 usingColumns 方法指定列名列表来限制插入的列,如以下示例所示:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingColumns("first_name", "last_name")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<String, Object>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingColumns("first_name", "last_name")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = mapOf(
                "first_name" to actor.firstName,
                "last_name" to actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters);
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

插入的执行与您依赖元数据确定要使用的列相同.

3.6.4. 使用 SqlParameterSource 提供参数值

使用 Map 提供参数值工作是正常的,但它不是最方便使用的类. Spring 提供了几个可以使用的 SqlParameterSource 接口实现. 第一个是 BeanPropertySqlParameterSource,如果你有一个包含你的值的 JavaBean 兼容类,这是一个非常方便的类. 它使用相应的 getter 方法来提取参数值. 以下示例显示如何使用 BeanPropertySqlParameterSource:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = BeanPropertySqlParameterSource(actor)
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

另一个选项是类似于 MapMapSqlParameterSource,但提供了一个可以被链接的更方便的 addValue 方法. 以下示例显示了如何使用它:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        SqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("first_name", actor.getFirstName())
                .addValue("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val insertActor = SimpleJdbcInsert(dataSource)
            .withTableName("t_actor")
            .usingGeneratedKeyColumns("id")

    fun add(actor: Actor): Actor {
        val parameters = MapSqlParameterSource()
                    .addValue("first_name", actor.firstName)
                    .addValue("last_name", actor.lastName)
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }

    // ... additional methods
}

如您所见,配置是相同的. 只有更改执行代码才能使用这些替代输入类.

3.6.5. 使用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall 类使用数据库中的元数据来查找 inout 参数的名称,这样您就不必显式声明它们. 如果您愿意这样做,或者如果您有没有自动映射到 Java 类的参数(例如 ARRAYSTRUCT) ,则可以声明参数. 第一个示例显示了一个简单的存储过程,该过程仅从 MySQL 数据库返回 VARCHARDATE 格式的标量值. 示例存储过程读取指定的 actor entry,并以 out 参数的形式返回 first_namelast_namebirth_date 列. 以下清单显示了第一个示例:

CREATE PROCEDURE read_actor (
    IN in_id INTEGER,
    OUT out_first_name VARCHAR(100),
    OUT out_last_name VARCHAR(100),
    OUT out_birth_date DATE)
BEGIN
    SELECT first_name, last_name, birth_date
    INTO out_first_name, out_last_name, out_birth_date
    FROM t_actor where id = in_id;
END;

in_id 参数包含您正在查找的 actorid. out 参数返回从表中读取的数据.

您可以用类似于声明 SimpleJdbcInsert 的方式声明 SimpleJdbcCall. 您应该在数据访问层的初始化方法中实例化和配置类. 与 StoredProcedure 类相比,您无需创建子类,也无需声明可在数据库元数据中查找的参数. 以下 SimpleJdbcCall 配置示例使用前面的存储过程(除了 DataSource 之外,唯一的配置选项是存储过程的名称) :

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.procReadActor = new SimpleJdbcCall(dataSource)
                .withProcedureName("read_actor");
    }

    public Actor readActor(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        Map out = procReadActor.execute(in);
        Actor actor = new Actor();
        actor.setId(id);
        actor.setFirstName((String) out.get("out_first_name"));
        actor.setLastName((String) out.get("out_last_name"));
        actor.setBirthDate((Date) out.get("out_birth_date"));
        return actor;
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val procReadActor = SimpleJdbcCall(dataSource)
            .withProcedureName("read_actor")


    fun readActor(id: Long): Actor {
        val source = MapSqlParameterSource().addValue("in_id", id)
        val output = procReadActor.execute(source)
        return Actor(
                id,
                output["out_first_name"] as String,
                output["out_last_name"] as String,
                output["out_birth_date"] as Date)
    }

        // ... additional methods
}

为执行调用所写的代码而创建一个包含 IN 参数的 SqlParameterSource. 您必须为输入值提供的名称与存储过程中声明的参数名称的名称相匹配. 该案例不必匹配,因为您使用元数据来确定应如何在存储过程中引用数据库对象. 存储过程的源中指定的内容不一定是它存储在数据库中的方式. 某些数据库将名称转换为全部大写,而其他数据库使用小写或使用指定的大小写.

execute 方法接受 IN 参数并返回一个 Map,该 Map 包含由存储过程中指定的名称输入的任何 out 参数. 在这种情况下,它们是 out_first_nameout_last_nameout_birth_date.

execute 方法的最后一部分创建一个 Actor 实例,用于返回检索到的数据. 同样,在存储过程中声明它们时,使用 out 参数的名称很重要. 此外,结果映射中存储的 out 参数名称中的情况与数据库中 out 参数名称的情况相匹配,这可能因数据库而异. 为了使代码更具可移植性,您应该进行不区分大小写的查找或指示 Spring 使用 LinkedCaseInsensitiveMap. 要执行后者,您可以创建自己的 JdbcTemplate 并将 setResultsMapCaseInsensitive 属性设置为 true. 然后,您可以将此自定义 JdbcTemplate 实例传递给 SimpleJdbcCall 的构造函数. 以下示例显示了此配置:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_actor");
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
        isResultsMapCaseInsensitive = true
    }).withProcedureName("read_actor")

    // ... additional methods
}

通过执行此操作,可以避免在用于返回的 out 参数名称的情况下发生冲突.

3.6.6. 显式声明用于 SimpleJdbcCall 的参数

在本章的前面部分,我们描述了如何从元数据中推导出参数,但如果您愿意,可以明确声明它们. 您可以通过使用 declareParameters 方法创建和配置 SimpleJdbcCall 来实现, 该方法将可变数量的 SqlParameter 对象作为输入. 有关如何定义 SqlParameter 的详细信息,请参阅下一节.

如果您使用的数据库不是 Spring 支持的数据库,则必须使用显式声明. 目前,Spring 支持以下数据库的存储过程调用的元数据查找: Apache Derby,DB2,MySQL,Microsoft SQL Server,Oracle 和 Sybase. 我们还支持 MySQL,Microsoft SQL Server 和 Oracle 的存储函数的元数据查找.

您可以选择显式声明一个部分或全部参数. 在未明确声明参数的情况下,仍会使用参数元数据. 要绕过对潜在参数的元数据查找的所有处理并仅使用声明的参数, 可以将方法调用 withoutProcedureColumnMetaDataAccess 作为声明的一部分. 假设您为数据库函数声明了两个或多个不同的调用方法. 在这种情况下,您调用 useInParameterNames 来指定要包含给定方法的 IN 参数名称列表.

以下示例显示了完全声明的存储过程调用,并使用前面示例中的信息:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_actor")
                .withoutProcedureColumnMetaDataAccess()
                .useInParameterNames("in_id")
                .declareParameters(
                        new SqlParameter("in_id", Types.NUMERIC),
                        new SqlOutParameter("out_first_name", Types.VARCHAR),
                        new SqlOutParameter("out_last_name", Types.VARCHAR),
                        new SqlOutParameter("out_birth_date", Types.DATE)
                );
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

        private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
            isResultsMapCaseInsensitive = true
        }).withProcedureName("read_actor")
                .withoutProcedureColumnMetaDataAccess()
                .useInParameterNames("in_id")
                .declareParameters(
                        SqlParameter("in_id", Types.NUMERIC),
                        SqlOutParameter("out_first_name", Types.VARCHAR),
                        SqlOutParameter("out_last_name", Types.VARCHAR),
                        SqlOutParameter("out_birth_date", Types.DATE)
    )

        // ... additional methods
}

两个示例的执行和结束结果是相同的. 第二个示例明确指定所有详细信息,而不是依赖于元数据.

3.6.7. 如何定义 SqlParameters

要为 SimpleJdbc 类以及 RDBMS 操作类定义参数(在将将 JDBC 操作建模为 Java 对象中) ,您可以使用 SqlParameter 或其某一子类. 为此,通常在构造函数中指定参数名称和 SQL 类型. SQL 类型是使用 java.sql.Types 常量指定的. 在本章前面,我们看到了类似于以下内容的声明:

Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
Kotlin
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

SqlParameter 的第一行声明了一个 IN 参数. 您可以通过使用 SqlQuery 及其子类(在 了解 SqlQuery 中介绍) 为存储过程调用和查询使用 IN 参数.

第二行(使用 SqlOutParameter) 声明要在存储过程调用中使用的 out 参数. InOut 参数还有一个 SqlInOutParameter(为过程提供 IN 值并且还返回值的参数) .

只有声明为 SqlParameterSqlInOutParameter 的参数才可用于提供输入值. 这与 StoredProcedure 类不同,后者(为了向后兼容性) 允许为声明为 SqlOutParameter 的参数提供输入值.

对于 IN 参数,除了名称和 SQL 类型之外,还可以指定数字数据的比例或自定义数据库类型的类型名称. 对于 out 参数,您可以提供 RowMapper 来处理从 REF 游标返回的行的映射. 另一个选项是指定一个 SqlReturnType,它提供了定义返回值的自定义处理的机会.

3.6.8. 使用 SimpleJdbcCall 调用存储函数

除了提供函数名称而不是过程名称之外,您可以使用与调用存储过程几乎相同的方式调用存储函数. 您可以使用 withFunctionName 方法作为配置的一部分,以指示您要对函数进行调用,并生成函数调用的相应字符串. 专有的执行调用(executeFunction) 用于执行函数,它将函数返回值作为指定类型的对象返回,这意味着您不必从结果映射中检索返回值. 类似的便捷方法(名为 executeObject) 也可用于只有一个 out 参数的存储过程. 以下示例(对于 MySQL) 基于名为 get_actor_name 的存储函数,该函数返回 actor 的全名:

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
    DECLARE out_name VARCHAR(200);
    SELECT concat(first_name, ' ', last_name)
        INTO out_name
        FROM t_actor where id = in_id;
    RETURN out_name;
END;

为了调用此函数,我们再次在初始化方法中创建 SimpleJdbcCall,如以下示例所示:

Java
public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcCall funcGetActorName;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
                .withFunctionName("get_actor_name");
    }

    public String getActorName(Long id) {
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("in_id", id);
        String name = funcGetActorName.executeFunction(String.class, in);
        return name;
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

    private val jdbcTemplate = JdbcTemplate(dataSource).apply {
        isResultsMapCaseInsensitive = true
    }
    private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
            .withFunctionName("get_actor_name")

    fun getActorName(id: Long): String {
        val source = MapSqlParameterSource().addValue("in_id", id)
        return funcGetActorName.executeFunction(String::class.java, source)
    }

    // ... additional methods
}

使用的 executeFunction 方法返回一个 String,该 String 包含函数调用的返回值.

3.6.9. 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标

调用返回结果集的存储过程或函数有点棘手. 某些数据库在 JDBC 结果处理期间返回结果集,而其他数据库则需要显式注册的特定类型的 out 参数. 这两种方法都需要额外的处理来循环结果集并处理返回的行. 使用 SimpleJdbcCall,您可以使用 returningResultSet 方法并声明 RowMapper 实现用于特定参数. 如果在结果处理期间返回结果集,则不会定义任何名称,因此返回的结果必须与声明 RowMapper 实现的顺序相匹配. 指定的名称仍用于将处理的结果列表存储在从 execute 语句返回的结果映射中.

下一个示例(对于 MySQL) 使用不带 IN 参数的存储过程,并返回 t_actor 表中的所有行:

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

要调用此过程,可以声明 RowMapper. 因为要映射到的类遵循 JavaBean 规则,所以可以使用通过传入必需的类来映射到 newInstance 方法而创建的 BeanPropertyRowMapper. 以下示例显示了如何执行此操作:

Java
public class JdbcActorDao implements ActorDao {

    private SimpleJdbcCall procReadAllActors;

    public void setDataSource(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.setResultsMapCaseInsensitive(true);
        this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
                .withProcedureName("read_all_actors")
                .returningResultSet("actors",
                BeanPropertyRowMapper.newInstance(Actor.class));
    }

    public List getActorsList() {
        Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
        return (List) m.get("actors");
    }

    // ... additional methods
}
Kotlin
class JdbcActorDao(dataSource: DataSource) : ActorDao {

        private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
            isResultsMapCaseInsensitive = true
        }).withProcedureName("read_all_actors")
                .returningResultSet("actors",
                        BeanPropertyRowMapper.newInstance(Actor::class.java))

    fun getActorsList(): List<Actor> {
        val m = procReadAllActors.execute(mapOf<String, Any>())
        return m["actors"] as List<Actor>
    }

    // ... additional methods
}

调用 execute 传入一个空 Map,因为此调用不接受任何参数. 然后从结果映射中检索 actor 列表并返回给调用者.

3.7. 将 JDBC 操作建模为 Java 对象

org.springframework.jdbc.object 包中包含的类允许您以面向对象的方式访问数据库. 例如,您可以执行查询并将结果作为包含业务对象的列表返回,其中关系列数据映射到业务对象的属性. 您还可以运行存储过程并运行 update,delete 和 insert 语句.

许多 Spring 开发人员认为下面描述的各种 RDBMS 操作类(StoredProcedure 类除外) 通常可以用直接的 JdbcTemplate 调用替换. 通常,直接编写在 JdbcTemplate 上调用 DAO 方法(而不是将查询封装为完整的类) 更简单.

但是,如果您从使用 RDBMS 操作类获得可测量的值,则应继续使用这些类.

3.7.1. 了解 SqlQuery

SqlQuery 是一个可重用的、线程安全的类,它封装了一个 SQL 查询. 子类必须实现 newRowMapper(..) 方法以提供 RowMapper 实例,该实例可以通过迭代在查询执行期间创建的 ResultSet 获得每行创建一个对象. SqlQuery 类很少直接使用,因为 MappingSqlQuery 子类为将行映射到 Java 类提供了更方便的实现. 继承 SqlQuery 的其他实现是 MappingSqlQueryWithParametersUpdatableSqlQuery.

3.7.2. 使用 MappingSqlQuery

MappingSqlQuery 是一个可重用的查询,其中具体的子类必须实现抽象的 mapRow(..) 方法,将提供的 ResultSet 的每一行转换为指定类型的对象. 以下示例显示了一个自定义查询,该查询将 t_actor 关系中的数据映射到 Actor 类的实例:

Java
public class ActorMappingQuery extends MappingSqlQuery<Actor> {

    public ActorMappingQuery(DataSource ds) {
        super(ds, "select id, first_name, last_name from t_actor where id = ?");
        declareParameter(new SqlParameter("id", Types.INTEGER));
        compile();
    }

    @Override
    protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
        Actor actor = new Actor();
        actor.setId(rs.getLong("id"));
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}
Kotlin
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {

    init {
        declareParameter(SqlParameter("id", Types.INTEGER))
        compile()
    }

    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
            rs.getLong("id"),
            rs.getString("first_name"),
            rs.getString("last_name")
    )
}

该类扩展了使用 Actor 类型参数化的 MappingSqlQuery. 此自定义查询的构造函数将 DataSource 作为唯一参数. 在此构造函数中,您可以使用 DataSource 和应执行的 SQL 调用超类上的构造函数来检索此查询的行. 此 SQL 用于创建 PreparedStatement,因此它可能包含在执行期间传递的任何参数的占位符. 您必须使用传入 SqlParameter 的 declareParameter 方法声明每个参数. SqlParameter 带有名称,以及 java.sql.Types 中定义的 JDBC 类型. 定义所有参数后,可以调用 compile() 方法,以便可以准备并稍后运行该语句. 这个类在编译后是线程安全的,因此,只要在初始化 DAO 时创建这些实例,它们就可以作为实例变量保存并重用. 以下示例显示如何定义此类:

Java
private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
    this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Customer getCustomer(Long id) {
    return actorMappingQuery.findObject(id);
}
Kotlin
private val actorMappingQuery = ActorMappingQuery(dataSource)

fun getCustomer(id: Long) = actorMappingQuery.findObject(id)

前面示例中的方法使用传入的唯一参数 id 检索 Customer. 由于我们只想返回一个对象,因此我们使用 id 作为参数调用 findObject 方法. 如果我们改为返回一个对象列表并获取其他参数的查询,我们将使用一个 execute 方法,该方法将传入的参数值数组作为可变参数. 以下示例显示了这样一种方法:

Java
public List<Actor> searchForActors(int age, String namePattern) {
    List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
    return actors;
}
Kotlin
fun searchForActors(age: Int, namePattern: String) =
            actorSearchMappingQuery.execute(age, namePattern)

3.7.3. 使用 SqlUpdate

SqlUpdate 类封装了 SQL 更新. 与查询一样,更新对象是可重用的,并且与所有 RdbmsOperation 类一样,更新可以具有参数并在 SQL 中定义. 此类提供了许多类似于查询对象的 execute(..) 方法的 update(..) 方法. SqlUpdate 类是具体的. 它可以是子类 - 例如,添加自定义更新方法. 但是,您不必为 SqlUpdate 类创建子类,因为可以通过设置 SQL 和声明参数来轻松地对其进行参数化. 以下示例创建名为 execute 的自定义更新方法:

Java
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

    public UpdateCreditRating(DataSource ds) {
        setDataSource(ds);
        setSql("update customer set credit_rating = ? where id = ?");
        declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
        declareParameter(new SqlParameter("id", Types.NUMERIC));
        compile();
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    public int execute(int id, int rating) {
        return update(rating, id);
    }
}
Kotlin
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate

class UpdateCreditRating(ds: DataSource) : SqlUpdate() {

    init {
        setDataSource(ds)
        sql = "update customer set credit_rating = ? where id = ?"
        declareParameter(SqlParameter("creditRating", Types.NUMERIC))
        declareParameter(SqlParameter("id", Types.NUMERIC))
        compile()
    }

    /**
     * @param id for the Customer to be updated
     * @param rating the new value for credit rating
     * @return number of rows updated
     */
    fun execute(id: Int, rating: Int): Int {
        return update(rating, id)
    }
}

3.7.4. 使用 StoredProcedure

StoredProcedure 类是 RDBMS 存储过程的对象抽象的超类. 这个类是 abstract 的.

继承的 sql 属性是 RDBMS 中存储过程的名称.

要为 StoredProcedure 类定义参数,可以使用 SqlParameter 或其某一子类. 您必须在构造函数中指定参数名称和 SQL 类型,如以下代码段所示:

Java
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
Kotlin
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

使用 java.sql.Types 常量指定 SQL 类型.

第一行(使用 SqlParameter) 声明一个 IN 参数. 您可以将IN参数用于存储过程调用和使用 SqlQuery 及其子类的查询(在 了解 SqlQuery 中介绍) .

第二行(使用 SqlOutParameter) 声明要在存储过程调用中使用的 out 参数. InOut 参数还有一个 SqlInOutParameter(为存储过程提供一个 in 值和返回值的参数) .

对于 in 参数,除了名称和 SQL 类型之外,还可以为自定义数据库类型指定数字数据的比例或类型名称. 对于 out 参数,您可以提供 RowMapper 来处理从 REF 游标返回的行的映射. 另一个选项是指定一个 SqlReturnType,它允许您定义返回值的自定义处理.

简单 DAO 的下一个示例使用 StoredProcedure 来调用函数(sysdate()) ,该函数随任何 Oracle 数据库一起提供. 要使用存储过程功能,您必须创建一个继承 StoredProcedure 的类. 在此示例中,StoredProcedure 类是内部类. 但是,如果需要重用 StoredProcedure,可以将其声明为顶级类. 此示例没有输入参数,但通过使用 SqlOutParameter 类将输出参数声明为日期类型. execute() 方法运行该过程并从结果 Map 中提取返回的日期. 结果 Map 通过使用参数名称作为键,为每个声明的输出参数(在本例中只有一个) 提供了一个条目. 以下清单显示了我们的自定义 StoredProcedure 类:

Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

    private GetSysdateProcedure getSysdate;

    @Autowired
    public void init(DataSource dataSource) {
        this.getSysdate = new GetSysdateProcedure(dataSource);
    }

    public Date getSysdate() {
        return getSysdate.execute();
    }

    private class GetSysdateProcedure extends StoredProcedure {

        private static final String SQL = "sysdate";

        public GetSysdateProcedure(DataSource dataSource) {
            setDataSource(dataSource);
            setFunction(true);
            setSql(SQL);
            declareParameter(new SqlOutParameter("date", Types.DATE));
            compile();
        }

        public Date execute() {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            Map<String, Object> results = execute(new HashMap<String, Object>());
            Date sysdate = (Date) results.get("date");
            return sysdate;
        }
    }

}
Kotlin
import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class StoredProcedureDao(dataSource: DataSource) {

    private val SQL = "sysdate"

    private val getSysdate = GetSysdateProcedure(dataSource)

    val sysdate: Date
        get() = getSysdate.execute()

    private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {

        init {
            setDataSource(dataSource)
            isFunction = true
            sql = SQL
            declareParameter(SqlOutParameter("date", Types.DATE))
            compile()
        }

        fun execute(): Date {
            // the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
            val results = execute(mutableMapOf<String, Any>())
            return results["date"] as Date
        }
    }
}

以下 StoredProcedure 示例有两个输出参数(在本例中为 Oracle REF 游标) :

Java
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "AllTitlesAndGenres";

    public TitlesAndGenresStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
        compile();
    }

    public Map<String, Object> execute() {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(new HashMap<String, Object>());
    }
}
Kotlin
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

    companion object {
        private const val SPROC_NAME = "AllTitlesAndGenres"
    }

    init {
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
        compile()
    }

    fun execute(): Map<String, Any> {
        // again, this sproc has no input parameters, so an empty Map is supplied
        return super.execute(HashMap<String, Any>())
    }
}

请注意在 TitlesAndGenresStoredProcedure 构造函数中使用的 declareParameter(..) 方法的重载是如何传递 RowMapper 实现实例的. 这是重用现有功能的一种非常方便和强大的方法. 接下来的两个示例提供了两个 RowMapper 实现的代码.

TitleMapper 类将 ResultSet 映射到提供的 ResultSet 中每一行的 Title 对象,如下所示:

Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

    public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
        Title title = new Title();
        title.setId(rs.getLong("id"));
        title.setName(rs.getString("name"));
        return title;
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper

class TitleMapper : RowMapper<Title> {

    override fun mapRow(rs: ResultSet, rowNum: Int) =
            Title(rs.getLong("id"), rs.getString("name"))
}

GenreMapper 类将 ResultSet 映射到提供的 ResultSet 中每一行的 Genre 对象,如下所示:

Java
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;

public final class GenreMapper implements RowMapper<Genre> {

    public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Genre(rs.getString("name"));
    }
}
Kotlin
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper

class GenreMapper : RowMapper<Genre> {

    override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
        return Genre(rs.getString("name"))
    }
}

要将参数传递给在 RDBMS 的定义中具有一个或多个输入参数的存储过程, 您可以编写一个强类型的 execute(..) 方法,该方法将委托给超类中的无类型 execute(Map) 方法,如以下例子所示:

Java
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

    private static final String SPROC_NAME = "TitlesAfterDate";
    private static final String CUTOFF_DATE_PARAM = "cutoffDate";

    public TitlesAfterDateStoredProcedure(DataSource dataSource) {
        super(dataSource, SPROC_NAME);
        declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
        declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
        compile();
    }

    public Map<String, Object> execute(Date cutoffDate) {
        Map<String, Object> inputs = new HashMap<String, Object>();
        inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
        return super.execute(inputs);
    }
}
Kotlin
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

    companion object {
        private const val SPROC_NAME = "TitlesAfterDate"
        private const val CUTOFF_DATE_PARAM = "cutoffDate"
    }

    init {
        declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        compile()
    }

    fun execute(cutoffDate: Date) = super.execute(
            mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}

3.8. 参数和数据值处理的常见问题

Spring Framework 的 JDBC 支持提供的不同方法中存在参数和数据值的常见问题. 本节介绍如何解决这些问题.

3.8.1. 为参数提供 SQL 类型信息

通常,Spring 根据传入的参数类型确定参数的 SQL 类型. 可以显式提供设置参数值时要使用的 SQL 类型. 有时需要正确设置 NULL 值.

您可以通过多种方式提供 SQL 类型信息:

  • JdbcTemplate 的许多更新和查询方法都采用 int 数组形式的附加参数. 此数组用于通过使用 java.sql.Types 类中的常量值来指示相应参数的SQL类型. 为每个参数提供一个条目.

  • 您可以使用 SqlParameterValue 类来包装需要此附加信息的参数值. 为此,请为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值. 您还可以为数值提供可选的 scale 参数

  • 对于使用命名参数的方法,可以使用 SqlParameterSource 类,BeanPropertySqlParameterSourceMapSqlParameterSource. 它们都具有为任何命名参数值注册 SQL 类型的方法.

3.8.2. 处理 BLOB 和 CLOB 对象

您可以在数据库中存储图片,其他二进制数据和大块文本. 这些大对象称为二进制数据的 BLOB(二进制大对象) 和字符数据的 CLOB(字符大对象) . 在 Spring 中,您可以直接使用 JdbcTemplate 处理这些大对象,也可以在使用 RDBMS 对象和 JdbcTemplate 类提供的更高抽象时处理这些大对象. 所有这些方法都使用 LobHandler 接口的实现来实际的管理 LOB(大对象) 数据. LobHandler 通过 getLobCreator 方法提供对 LobCreator 类的访问,该方法用于创建要插入的新 LOB 对象.

LobCreatorLobHandler 为 LOB 输入和输出提供以下支持:

  • BLOB

    • byte[]: getBlobAsBytessetBlobAsBytes

    • InputStream: getBlobAsBinaryStreamsetBlobAsBinaryStream

  • CLOB

    • String: getClobAsStringsetClobAsString

    • InputStream: getClobAsAsciiStreamsetClobAsAsciiStream

    • Reader: getClobAsCharacterStreamsetClobAsCharacterStream

下一个示例显示如何创建和插入 BLOB 对象. 稍后我们将展示如何从数据库中读取它.

此示例使用 JdbcTemplateAbstractLobCreatingPreparedStatementCallback 的实现. 它实现了一个方法: setValues. 此方法提供了一个 LobCreator,我们用它来设置 SQL insert 语句中 LOB 列的值.

对于此示例,我们假设有一个变量 lobHandler,它已经设置为 DefaultLobHandler 的一个实例. 您通常通过依赖注入设置此值.

以下示例展示了如何创建并插入一个 BLOB 对象:

Java
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);

jdbcTemplate.execute(
    "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
    new AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
        protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
            ps.setLong(1, 1L);
            lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());  (2)
            lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());  (3)
        }
    }
);

blobIs.close();
clobReader.close();
1 在本例中,传入的 lobHandler 是一个普通的 DefaultLobHandler.
2 使用 setClobAsCharacterStream 方法传入 CLOB 的内容.
3 使用 setBlobAsBinaryStream 方法传入 BLOB 的内容.
Kotlin
val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)

jdbcTemplate.execute(
        "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
        object: AbstractLobCreatingPreparedStatementCallback(lobHandler) {  (1)
            override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
                ps.setLong(1, 1L)
                lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt())  (2)
                lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt())  (3)
            }
        }
)
blobIs.close()
clobReader.close()
1 在本例中,传入的 lobHandler 是一个普通的 DefaultLobHandler.
2 使用 setClobAsCharacterStream 方法传入 CLOB 的内容.
3 使用 setBlobAsBinaryStream 方法传入 BLOB 的内容.

如果从 DefaultLobHandler.getLobCreator() 返回的 LobCreator 上调用 setBlobAsBinaryStream, setClobAsAsciiStreamsetClobAsCharacterStream 方法,则可以选择为 contentLength 参数指定负值. 如果指定的内容长度为负,则 DefaultLobHandler 使用 set-stream 方法的 JDBC 4.0 变体而不使用 length 参数. 否则,它将指定的长度传递给驱动程序.

请参阅用于验证它是否支持在不提供内容长度的情况下流式传输 LOB 的 JDBC 驱动程序的文档.

现在是时候从数据库中读取 LOB 数据了. 同样,您使用具有相同实例变量 lobHandlerJdbcTemplate 和对 DefaultLobHandler 的引用. 以下示例显示了如何执行此操作:

Java
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
    new RowMapper<Map<String, Object>>() {
        public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
            Map<String, Object> results = new HashMap<String, Object>();
            String clobText = lobHandler.getClobAsString(rs, "a_clob");  (1)
            results.put("CLOB", clobText);
            byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");  (2)
            results.put("BLOB", blobBytes);
            return results;
        }
    });
1 使用 getClobAsString 方法检索 CLOB 的内容.
2 使用 getBlobAsBytes 方法检索 BLOB 的内容.
Kotlin
val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
    val clobText = lobHandler.getClobAsString(rs, "a_clob")  (1)
    val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob")  (2)
    mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
1 使用 getClobAsString 方法检索 CLOB 的内容.
2 使用 getBlobAsBytes 方法检索 BLOB 的内容.

3.8.3. 传入 Lists 值到 IN 子句

SQL 标准允许基于包含列表变量值的表达式来选择行. 一个典型的例子是 select * from T_ACTOR where id in (1, 2, 3). JDBC 标准对预准备语句不直接支持此列表变量. 您不能声明可变数量的占位符. 您需要准备好所需占位符数量的多种变体,或者一旦知道需要多少占位符, 就需要动态生成 SQL 字符串. NamedParameterJdbcTemplateJdbcTemplate 中提供的命名参数支持采用后一种方法. 您可以将值作为原始对象的 java.util.List 传递. 此列表用于插入所需的占位符,并在语句执行期间传入值.

传递许多值时要小心. JDBC 标准不保证您可以为 in 列表表达式使用 100 个以上的值. 各种数据库超过此数量,但它们通常对允许的值有多少硬性限制. 例如,Oracle 的限制为 1000.

除了值列表中的原始值之外,您还可以创建对象数组的 java.util.List. 此列表可以支持为 in 子句定义的多个表达式, 例如 select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2,'Harrop')). 当然,这要求您的数据库支持此语法.

3.8.4. 处理存储过程调用的复杂类型

调用存储过程时,有时可以使用特定于数据库的复杂类型. 为了容纳这些类型,Spring 提供了一个 SqlReturnType,用于从存储过程调用中返回时处理这些类型, 和一个 SqlTypeValue,当这些类型作为参数出入存储过程中时处理他们.

SqlReturnType 接口有一个必须实现的方法(名为 getTypeValue) . 此接口用作 SqlOutParameter 声明的一部分. 以下示例显示返回用户声明的类型为 ITEM_TYPE 的 Oracle STRUCT 对象的值:

Java
public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        // ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            (CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
                STRUCT struct = (STRUCT) cs.getObject(colIndx);
                Object[] attr = struct.getAttributes();
                TestItem item = new TestItem();
                item.setId(((Number) attr[0]).longValue());
                item.setDescription((String) attr[1]);
                item.setExpirationDate((java.util.Date) attr[2]);
                return item;
            }));
        // ...
    }
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        // ...
        declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
            val struct = cs.getObject(colIndx) as STRUCT
            val attr = struct.getAttributes()
            TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
        })
        // ...
    }
}

您可以使用 SqlTypeValue 将 Java 对象(例如 TestItem) 的值传递给存储过程. SqlTypeValue 接口有一个必须实现的单例方法(名为 createTypeValue) . 传入 active 连接,您可以使用它来创建特定于数据库的对象, 例如 ArrayDescriptor 实例或 ArrayDescriptor 实例. 以下示例创建 StructDescriptor 实例:

Java
final TestItem testItem = new TestItem(123L, "A test item",
        new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
        Struct item = new STRUCT(itemDescriptor, conn,
        new Object[] {
            testItem.getId(),
            testItem.getDescription(),
            new java.sql.Date(testItem.getExpirationDate().getTime())
        });
        return item;
    }
};
Kotlin
val (id, description, expirationDate) = TestItem(123L, "A test item",
        SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))

val value = object : AbstractSqlTypeValue() {
    override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
        val itemDescriptor = StructDescriptor(typeName, conn)
        return STRUCT(itemDescriptor, conn,
                arrayOf(id, description, java.sql.Date(expirationDate.time)))
    }
}

您现在可以将此 SqlTypeValue 添加到为存储过程调用的 execute 方法的包含输入参数的 Map.

SqlTypeValue 的另一个用途是将值数组传递给 Oracle 存储过程. 在这种情况下,必须使用 Oracle 自己的内部 ARRAY 类,您可以使用 SqlTypeValue 创建 Oracle ARRAY 的实例, 并使用 Java ARRAY 中的值填充它,如以下示例所示:

Java
final Long[] ids = new Long[] {1L, 2L};

SqlTypeValue value = new AbstractSqlTypeValue() {
    protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
        ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
        ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
        return idArray;
    }
};
Kotlin
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {

    init {
        val ids = arrayOf(1L, 2L)
        val value = object : AbstractSqlTypeValue() {
            override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
                val arrayDescriptor = ArrayDescriptor(typeName, conn)
                return ARRAY(arrayDescriptor, conn, ids)
            }
        }
    }
}

3.9. 嵌入式数据库支持

org.springframework.jdbc.datasource.embedded 包提供对嵌入式 Java 数据库引擎的支持. 本地提供对http://www.hsqldb.org[HSQL], H2, 和 Derby 的支持. 您还可以使用可扩展 API 来插入新的嵌入式数据库类型和 DataSource 实现.

3.9.1. 为什么使用嵌入式数据库?

嵌入式数据库在项目的开发阶段非常有用,因为它具有轻量级特性. 优点包括易于配置,快速启动时间,可测试性以及在开发过程中快速发展 SQL 的能力.

3.9.2. 使用 Spring XML 创建嵌入式数据库

如果你在 ApplicationContext 中想暴露一个嵌入式数据库的实例作为 bean,那么你可以在 spring-jdbc 命名空间中使用 embedded-database 标签.

<jdbc:embedded-database id="dataSource" generate-name="true">
    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

上述配置创建一个嵌入式 HSQL 数据库,该数据库使用类路径根目录中的 schema.sqltest-data.sql 来填充数据. 此外,作为最佳实践,将为嵌入式数据库分配唯一生成的名称. 嵌入式数据库作为 javax.sql.DataSource 类型的 bean 可用于 Spring 容器, 然后可以根据需要将其注入数据访问对象.

3.9.3. 以编程方式创建嵌入式数据库

EmbeddedDatabaseBuilder 类提供了一个流式的 API,用于以编程方式构建嵌入式数据库. 当您需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此方法,如以下示例所示:

Java
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build();

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()
Kotlin
val db = EmbeddedDatabaseBuilder()
        .generateUniqueName(true)
        .setType(H2)
        .setScriptEncoding("UTF-8")
        .ignoreFailedDrops(true)
        .addScript("schema.sql")
        .addScripts("user_data.sql", "country_data.sql")
        .build()

// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)

db.shutdown()

为了进一步了解其所支持的所有可选项,请参阅 javadoc for EmbeddedDatabaseBuilder Java 文档

你也可以使用 EmbeddedDatabaseBuilder 通过 Java 配置创建一个嵌入式数据库,如下所示:

Java
@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build();
    }
}
Kotlin
@Configuration
class DataSourceConfig {

    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("schema.sql")
                .addScripts("user_data.sql", "country_data.sql")
                .build()
    }
}

3.9.4. 选择嵌入式数据库类型

本节介绍如何选择 Spring 支持的三个嵌入式数据库中的某一个. 它包括以下主题:

使用HSQL

Spring 支持 HSQL 1.8.0 及更高版本. 如果未明确指定类型,HSQL 是默认的嵌入式数据库. 要显式指定 HSQL,请将 embedded-database 标记的 type 属性设置为 HSQL. 如果使用构建器 API,请使用 EmbeddedDatabaseType.HSQL 调用 setType(EmbeddedDatabaseType) 方法.

使用 H2

Spring 支持 H2 数据库. 要启用 H2,请将 embedded-database 标记的 type 属性设置为 H2. 如果使用构建器 API,请使用 EmbeddedDatabaseType.H2 调用 setType(EmbeddedDatabaseType) 方法.

使用 Derby

Spring 支持 Apache Derby 10.5 及更高版本. 要启用 Derby,请将 embedded-database 标记的 type 属性设置为 DERBY. 如果使用构建器API,请使用 EmbeddedDatabaseType.DERBY 调用 setType(EmbeddedDatabaseType) 方法.

3.9.5. 使用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种轻量级的方法来测试数据访. 下一个示例是使用嵌入式数据库的数据访问集成测试模板. 当嵌入式数据库不需要跨测试类重用时,使用这样的模板可以用于一次性. 但是,如果您希望创建在测试套件中共享的嵌入式数据库, 请考虑使用 Spring TestContext Framework并将嵌入式数据库配置为 Spring ApplicationContext 中的 bean, 在使用 Spring 使用 Spring XML 创建嵌入式数据库以编程方式创建嵌入式数据库. 以下清单显示了测试模板:

Java
public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @BeforeEach
    public void setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = new EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build();
    }

    @Test
    public void testDataAccess() {
        JdbcTemplate template = new JdbcTemplate(db);
        template.query( /* ... */ );
    }

    @AfterEach
    public void tearDown() {
        db.shutdown();
    }

}
Kotlin
class DataAccessIntegrationTestTemplate {

    private lateinit var db: EmbeddedDatabase

    @BeforeEach
    fun setUp() {
        // creates an HSQL in-memory database populated from default scripts
        // classpath:schema.sql and classpath:data.sql
        db = EmbeddedDatabaseBuilder()
                .generateUniqueName(true)
                .addDefaultScripts()
                .build()
    }

    @Test
    fun testDataAccess() {
        val template = JdbcTemplate(db)
        template.query( /* ... */)
    }

    @AfterEach
    fun tearDown() {
        db.shutdown()
    }
}

3.9.6. 为嵌入式数据库生成唯一名称

如果测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到嵌入式数据库的错误. 如果 XML 配置文件或 @Configuration 类负责创建嵌入式数据库,并且相应的配置随后在同一测试套件中的多个测试场景中重复使用(即在同一 JVM 进程中) , 那么这一错误很容易发生. 例如 ,针对嵌入式数据库的集成测试,其 ApplicationContext 配置仅与配置文件处于 active 状态的 bean 的定义有所不同.

导致此类错误的根本原因是,如果没有另外指定嵌入式数据库名称,Spring 的 EmbeddedDatabaseFactory (由 <jdbc:embedded-database> XML 命名空间元素和 Java 配置的 EmbeddedDatabaseBuilder 在内部使用) 将嵌入数据库的名称设置为 testdb. 对于 <jdbc:embedded-database> 的情况,通常会为嵌入式数据库分配一个等于 bean 的 id 的名称(通常类似于 dataSource) . 因此,随后尝试创建嵌入式数据库时不会产生新的数据库. 相反,重用相同的 JDBC 连接 URL,并且试图创建新的嵌入式数据库实际上是指向从相同配置创建的现有嵌入式数据库.

为了解决这个常见问题,Spring Framework 4.2 支持为嵌入式数据库生成唯一名称. 要启用生成的名称,请使用以下选项之一.

  • EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()

  • EmbeddedDatabaseBuilder.generateUniqueName()

  • <jdbc:embedded-database generate-name="true" …​ >

3.9.7. 扩展嵌入式数据库的支持

您可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持:

  • 实现 EmbeddedDatabaseConfigurer 以支持新的嵌入式数据库的类型.

  • 实现 DataSourceFactory 以支持新的 DataSource 实现,例如用于管理嵌入式数据库连接的连接池.

我们鼓励您在 GitHub Issues 上为 Spring 社区贡献扩展.

3.10. 初始化 DataSource

org.springframework.jdbc.datasource.init 包提供对初始化现有 DataSource 的支持. 嵌入式数据库支持提供了一个用于为应用程序创建和初始化 DataSource 的选项. 但是,您有时可能需要初始化在某个服务器上运行的实例.

3.10.1. 使用 Spring XML 初始化数据库

如果要初始化数据库并且提供对 DataSource bean 的引用,可以在 spring-jdbc 命名空间中使用 initialize-database 标记:

<jdbc:initialize-database data-source="dataSource">
    <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
    <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>

上面的示例针对数据库运行两个指定的脚本. 第一个脚本创建一个 schem,第二个脚本使用测试数据集填充表. 脚本位置也可以是带有通配符的模式,这些通配符用于 Spring 中的资源(例如 classpath*:/com/foo/**/sql/*-data.sql) . 如果使用一定的规则,脚本将按其 URL 或文件名的词法顺序运行.

数据库初始化程序的默认行为是无条件运行提供的脚本. 这可能并不总是您想要的 - 例如,如果您针对已经包含测试数据的数据库运行脚本. 首先通过创建表然后插入数据的公共模式(如前所示) ,减少了意外删除数据的可能性. 如果表已经存在,则第一步失败.

但是,为了更好地控制现有数据的创建和删除,XML 命名空间提供了一些其他选项. 第一个是打开和关闭初始化的标志. 您可以根据环境设置它(例如从系统属性或环境 bean 中提取布尔值) . 以下示例从系统属性获取值:

<jdbc:initialize-database data-source="dataSource"
    enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
    <jdbc:script location="..."/>
</jdbc:initialize-database>
1 Get the value for enabled from a system property called INITIALIZE_DATABASE.

控制现有数据发生情况的第二个选择是更容忍失败. 为此,您可以控制初始化程序忽略它从脚本执行的 SQL 中的某些错误的能力,如以下示例所示:

<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS">
    <jdbc:script location="..."/>
</jdbc:initialize-database>

在前面的示例中,我们说我们希望脚本有时会针对空数据库运行,并且脚本中有一些 DROP 语句因此会失败. 因此失败的 SQL DROP 语句将被忽略,但其他失败将导致异常. 如果您的 SQL 语法不支持 DROP …​ IF EXISTS 或类似) , 但您希望在重新创建之前无条件地删除所有测试数据,这将非常有用. 在这种情况下,第一个脚本通常是一组 DROP 语句,后跟一组 CREATE 语句.

ignore-failures 选项可以被设置为 NONE(默认值) 、DROPS (忽略删除失败) 或者 ALL (忽略所有的失败) .

每条语句应该由 ; 分隔,如果 ; 字符根本不存在与脚本中,则每条语句应该新起一行. 你可以通过脚本控制全局或脚本,如下例所示:

<jdbc:initialize-database data-source="dataSource" separator="@@"> (1)
    <jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2)
    <jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/>
    <jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/>
</jdbc:initialize-database>
1 将脚本分隔符设置为 @@.
2 db-schema.sql 的分隔符设置为 ;.

在此示例中,两个 test-data 脚本使用 @@ 作为语句分隔符,并且只有 db-schema.sql 使用 ;. 此配置指定默认分隔符为 @@ 并覆盖 db-schema 脚本的默认分隔符.

如果您需要比从 XML 命名空间获得的更多控制,则可以直接使用 DataSourceInitializer 并将其定义为应用程序中的组件.

初始化依赖于数据库的其他组件

应用程序大多数类(那些在 Spring 上下文启动之后才使用数据库的应用程序) 可以使用数据库初始化程序而不用进一步的复杂化初始数据库. 如果您的应用程序不是其中之一,则可能需要阅读本节的其余部分.

数据库初始化程序依赖于 DataSource 实例并运行其初始化回调中提供的脚本(类似于 XML bean 定义中的 init-method, 组件中的 @PostConstruct 方法或实现 InitializingBean 的组件中的 afterPropertiesSet() 方法) ) . 如果其他 bean 依赖于相同的数据源并在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化. 一个常见的例子是缓存,它在应用程序启动时急切地初始化并从数据库加载数据.

要解决此问题,您有两个选择: 将缓存初始化策略更改为稍后执行或确保首先初始化数据库初始化程序.

如果应用程序在您的控制范围内而不是其他方式,则更改缓存初始化策略可能很容易. 关于如何实现这一点的一些建议如下:

  • 在首次使用时使缓存初始化懒加载,这可以缩短应用程序启动时间.

  • 拥有缓存或初始化缓存的单独组件实现 LifecycleSmartLifecycle. 当应用程序上下文启动时,您可以通过设置其 autoStartup 标志自动启动 SmartLifecycle, 并且可以通过在封闭上下文中调用 ConfigurableApplicationContext.start() 来手动启动 Lifecycle.

  • 使用 Spring ApplicationEvent 或类似的自定义监听器机制来触发缓存初始化. ContextRefreshedEvent 总是在上下文准备好使用时发布(在所有 bean 初始化之后) ,因此这通常是一个有用的钩子(这是 SmartLifecycle 默认工作的方式) .

确保首先初始化数据库初始化程序也很容易. 关于如何实现这一点的一些建议如下:

  • 依赖于 Spring BeanFactory 的默认行为,即 bean 按注册顺序初始化. 您可以通过采用 XML 配置中的一组 <import/> 元素的通用实践来轻松安排初始化顺序, 这些元素可以对应用程序模块进行排序,并确保首先列出数据库和数据库初始化.

  • DataSource 和使用它的业务组件分开并通过将它们放在单独的 ApplicationContext 实例中来控制它们的启动顺序 (例如,父上下文包含 DataSource,子上下文包含业务组件) . 这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用.

4. 使用 R2DBC 进行数据访问

R2DBC ("Reactive Relational Database Connectivity") 是一个由社区驱动的规范,它使用响应式模式对 SQL 数据库的访问进行标准化。

4.1. 包的层次结构

Spring Framework 的 R2DBC 抽象框架由两个不同的包组成:

4.2. 使用 R2DBC 的核心类来控制基本的 R2DBC 处理和错误处理

本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。它包括以下主题:

4.2.1. 使用 DatabaseClient

DatabaseClient 是 R2DBC core 包下的核心类. 他处理资源的创建和释放,这有助于避免常见的错误发生. 例如:忘记了关闭连接,它执行 R2DBC 工作流的基本任务(例如语句创建和执行),让应用程序代码提供 SQL 并提取结果。DatabaseClient 类:

  • 运行 SQL 查询

  • 更新语句和存储过程调用

  • Result 实例遍历

  • 捕获 R2DBC 异常并将它们转换为在 org.springframework.dao 包中定义的通用的、具有更多信息的异常层次结构。(请参见 一致的异常层次结构。)

client 有一个使用响应式类型的流式 API 进行声明性组合.

当您在代码中使用 DatabaseClient 时,您只需要实现 java.util.function 接口,并按照约定为它们提供一个明确的定义。给 DatabaseClient 类提供的 Connection, Function 回调将创建一个 Publisher。对于提取 Row 结果的映射函数也是如此。

您可以通过使用 ConnectionFactory 直接实例化 DatabaseClient 然后在 DAO 实现中使用,或者您可以在 Spring IoC 容器中配置它,并将它作为 bean 提供给 DAO。

创建 DatabaseClient 对象最简单的方法是通过静态工厂方法,如下所示:

Java
DatabaseClient client = DatabaseClient.create(connectionFactory);
Kotlin
val client = DatabaseClient.create(connectionFactory)
ConnectionFactory 应该始终配置为 Spring IoC 容器中的 bean.

The preceding method creates a DatabaseClient with default settings. 上述方法使用默认设置创建 DatabaseClient

您还可以使用 DatabaseClient.builder() 创建一个 Builder 实例. 然后通过以下方法自定义 DatabaseClient:

  • ….bindMarkers(…): 提供一个特定的 BindMarkersFactory 以将指定参数配置为数据库绑定标记转换.

  • ….executeFunction(…): 设置 ExecuteFunction 如何执行 Statement 对象.

  • ….namedParameters(false): 禁用命名参数扩展。默认启用.

语句由 ConnectionFactory 中的 BindMarkersFactoryResolver 来解析,通常是通过检查 ConnectionFactoryMetadata.

+ 你可以通过注册一个实现了 org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider 的类来让 Spring 自动发现你的 BindMarkersFactory 通过 META-INF/spring.factories. BindMarkersFactoryResolver 使用 Spring 提供的 SpringFactoriesLoader 从类路径中发现绑定标记提供程序实现.

+

目前支持的数据库:

  • H2

  • MariaDB

  • Microsoft SQL Server

  • MySQL

  • Postgres

这个类发出的所有 SQL 都在 DEBUG 级别上记录在与客户端实例(通常是 DefaultDatabaseClient)的完全限定类名对应的类别下。此外,每次执行都在响应序列中注册一个检查点,以辅助调试。

下面几节提供一些使用 DatabaseClient 的示例。这些示例并不是 DatabaseClient 的所有功能的详尽列表。请参阅相关的 javadoc.

执行语句

DatabaseClient 提供了运行语句的基本功能。下面的例子展示了创建新表所需的最小但功能完整的代码:

Java
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .then();
Kotlin
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
        .await()

DatabaseClient 是为简便、流畅的使用而设计的。它在执行规范的每个阶段都公开中间方法、延续方法和终端方法。上面的示例使用 then() 返回一个完成 Publisher,它在查询(或者查询多个,如果 SQL 查询包含多条语句)完成时完成.

execute(…) 接受SQL查询字符串或查询 Supplier<String>, 将实际的查询创建推迟到执行时.
Querying (SELECT)

SQL 查询可以通过 Row 对象或受影响行的数量返回值。根据你的查询,DatabaseClient 可以返回更新的行数或行本身.

下面的查询将返回 idname 行:

Java
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
        .fetch().first();
Kotlin
val first = client.sql("SELECT id, name FROM person")
        .fetch().awaitSingle()

下面的查询使用绑定变量:

Java
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().first();
Kotlin
val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitSingle()

你可能已经注意到了上面的 fetch() 方法. fetch() 是一个延迟操作符,允许您指定要使用多少数据

调用 first() 将返回结果中的第一行,并丢弃其余行。您可以使用以下操作符使用数据:

  • first() 返回整个结果的第一行. 他的 Kotlin Coroutine 变量为 awaitSingle() 用于非空返回值;如果值是可选的,则名为 awaitSingleOrNull().

  • one() 只返回一个结果,如果结果包含更多行则失败。使用 Kotlin Coroutines 可以使用 awaitOne() 返回一个值,如果值可能为 null,则为 `awaitOneOrNull.

  • all() 返回结果所有行. 当使用 Kotlin Coroutines, 使用 flow().

  • rowsUpdated() 返回受影响的行数 (INSERT/UPDATE/DELETE count). 他的 Kotlin Coroutine 变量为 awaitRowsUpdated().

在不指定进一步映射细节的情况下,查询结果将表达为 Map,其键是不区分大小写的列名,值为列的值。

您可以通过提供一个 Function<Row, T> 来控制结果映射,每个 Row 将调用该函数,以便它可以返回任意值( singular 值、集合和 Map 以及对象)。

下面的例子提取 name 列:

Java
Flux<String> names = client.sql("SELECT name FROM person")
        .map(row -> row.get("name", String.class))
        .all();
Kotlin
val names = client.sql("SELECT name FROM person")
        .map{ row: Row -> row.get("name", String.class) }
        .flow()
关于 null?

关系型数据库结果可以包含 null 值。Reactive Streams 则禁止发出 null 值。此规范要求在 extractor 函数中进行正确的处理 null 值。、 虽然可以从 Row 中获取 null 值,但不能发出 null 值。必须将任何 null 值包装在对象中(例如, Optional for singular values),以确保 extractor 函数永远不会直接返回 null 值.

DatabaseClient 更新 (INSERT, UPDATE, and DELETE)

修改语句的唯一区别是,这些语句通常不返回表格数据,因此使用 rowsUpdated() 来表示结果。

下面的例子显示了一个 UPDATE 语句,它返回更新的行数:

Java
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().rowsUpdated();
Kotlin
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
        .bind("fn", "Joe")
        .fetch().awaitRowsUpdated()
将值绑定到查询

一个典型的应用程序需要根据某些输入选择或更新行参数化 SQL 语句。这些语句通常是受 WHERE 子句约束的 SELECT 语句,或者接受输入参数的 INSERTUPDATE 语句。如果参数没有正确转义,参数化语句将会有 SQL 注入的风险。DatabaseClient 利用 R2DBC 的 bind API 来消除查询参数的 SQL 注入风险。可以使用 execute(…) 操作符提供参数化 SQL 语句,并将参数绑定到实际语句。然后,您的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行语句。

参数绑定支持两种绑定策略:

  • 通过索引, 索引从 0 开始.

  • 通过Name(名称), 使用占位符名称.

下面的示例展示了查询的参数绑定:

db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
    .bind("id", "joe")
    .bind("name", "Joe")
    .bind("age", 34);
R2DBC 本地绑定标记

R2DBC 使用依赖于实际数据库供应商的数据库本机绑定标记。例如,Postgres 使用索引标记,如 $1$2$n。另一个例子是 SQL Server,它使用带有 @ 前缀的命名绑定标记。

这与 JDBC 不同,JDBC 需要 ? 绑定标记。在 JDBC 中,实际的驱动程序将翻译 ? 并将标记绑定到数据库本地标记,作为其语句执行的一部分。

Spring Framework 的 R2DBC 支持允许您使用本机绑定标记或使用 :name 语法的已命名绑定标记。

命名参数支持利用 BindMarkersFactory 实例在执行查询时将命名参数扩展为本机绑定标记,这使您在不同的数据库供应商之间具有一定程度的查询可移植性.

query-preprocessor 将名为 Collection 的参数展开到一系列绑定标记中,以消除基于参数数量动态创建查询的需要。扩展嵌套对象数组以允许使用(例如)选择列表。

考虑以下查询:

SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50))

可以对前面的查询进行参数化并按如下方式运行:

Java
List<Object[]> tuples = new ArrayList<>();
tuples.add(new Object[] {"John", 35});
tuples.add(new Object[] {"Ann",  50});

client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples);
Kotlin
val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))

client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
    .bind("tuples", tuples)
选择列表的使用取决于数据库供应商.

下面的例子展示了一个使用 IN 谓词的更简单的方式:

Java
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("ages", Arrays.asList(35, 50));
Kotlin
val tuples: MutableList<Array<Any>> = ArrayList()
tuples.add(arrayOf("John", 35))
tuples.add(arrayOf("Ann", 50))

client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
    .bind("tuples", arrayOf(35, 50))
R2DBC 本身不支持类似于 collection 的值。尽管如此,展开上面的例子中的 List, 在 Spring 的 R2DBC 命名参数支持中依然可用,例如上面所示的 IN 子句中的使用。然而,插入或更新数组类型的列(例如在 Postgres 中)需要底层 R2DBC 驱动程序支持的数组类型:通常是 Java 数组,例如用 String[] 来更新 text[] 列。不要将 Collection<String> 或类似的参数作为数组参数传递.
Statement Filters

有时,您需要在实际 Statement 运行之前对其选项进行微调。通过 DatabaseClient 注册一个 Statement 过滤器(StatementFilterFunction),以便在语句执行时拦截和修改语句,如下例所示:

Java
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
    .bind("name", …)
    .bind("state", …);
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
            .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) }
            .bind("name", …)
            .bind("state", …)

DatabaseClient 还有一个更简便的 filter(…) 重载方法,接收 Function<Statement, Statement> 参数:

Java
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"));

client.sql("SELECT id, name, state FROM table")
    .filter(statement -> s.fetchSize(25));
Kotlin
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }

client.sql("SELECT id, name, state FROM table")
    .filter { statement -> s.fetchSize(25) }

StatementFilterFunction 的实现可以过滤 StatementResult 对象.

DatabaseClient 最佳实践

DatabaseClient 类的实例是线程安全的。这很重要,因为这意味着您可以将 DatabaseClient 配置为单例,然后可以安全地将这个 bean 的引用注入多个 dao(或存储库) 中。DatabaseClient 是有状态的,因为它维护对 ConnectionFactory 的引用,但这种状态不是 conversational 状态。

使用 DatabaseClient 类的一个常见做法是在 Spring 配置文件中配置一个 ConnectionFactory,然后依赖注入共享的 ConnectionFactory bean 到 DAO 类中。DatabaseClient 是在 ConnectionFactorysetter 中创建的。如下 dao:

Java
public class R2dbcCorporateEventDao implements CorporateEventDao {

    private DatabaseClient databaseClient;

    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory);
    }

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
Kotlin
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao {

    private val databaseClient = DatabaseClient.create(connectionFactory)

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}

显式配置的另一种选择是使用组件扫描和注解支持的依赖注入。在这种情况下,您可以使用 @Component 注解类(这使它成为组件扫描的候选对象),并使用 @Autowired 注解 ConnectionFactory setter 方法。下面的例子展示了如何做到这一点:

Java
@Component (1)
public class R2dbcCorporateEventDao implements CorporateEventDao {

    private DatabaseClient databaseClient;

    @Autowired (2)
    public void setConnectionFactory(ConnectionFactory connectionFactory) {
        this.databaseClient = DatabaseClient.create(connectionFactory); (3)
    }

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 使用 @Component 注解类.
2 在 setter 方法上使用 @Autowired 注解注解 ConnectionFactory.
3 使用 ConnectionFactory 创建一个新的 DatabaseClient.
Kotlin
@Component (1)
class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2)

    private val databaseClient = DatabaseClient(connectionFactory) (3)

    // R2DBC-backed implementations of the methods on the CorporateEventDao follow...
}
1 使用 @Component 注解类.
2 在 setter 方法上使用 @Autowired 注解注解 ConnectionFactory.
3 使用 ConnectionFactory 创建一个新的 DatabaseClient.

无论您选择使用(或不使用)上述的几种方式,在每次希望运行 SQL 时都很少需要创建 DatabaseClient 类的新实例。一旦配置好,DatabaseClient 实例就是线程安全的。如果您的应用程序访问多个数据库,您可能需要多个 DatabaseClient 实例,这需要多个 ConnectionFactory,以及随后的多个不同配置的 DatabaseClient 实例.

4.3. 获取自动生成的键

在具有自增或标识符的表中插入数据时,INSERT 语句可能会自动为其生成键。想要对生成的列名进行完全控制,只需注册 StatementFilterFunction,该函数请求所需列的生成键.

Java
Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter(statement -> s.returnGeneratedValues("id"))
        .map(row -> row.get("id", Integer.class))
        .first();

// generatedId emits the generated key once the INSERT statement has finished
Kotlin
val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
    .filter { statement -> s.returnGeneratedValues("id") }
        .map { row -> row.get("id", Integer.class) }
        .awaitOne()

// generatedId emits the generated key once the INSERT statement has finished

4.4. 控制数据库连接

本节包含:

4.4.1. 使用 ConnectionFactory

Spring 通过 ConnectionFactory 获得到数据库的 R2DBC 连接。ConnectionFactory 是 R2DBC 规范的一部分,是驱动程序的常见入口点。它允许容器或框架隐藏应用程序代码连接池和事务管理问题。作为开发人员,您不需要知道如何连接到数据库的细节。这是设置 ConnectionFactory 的管理员的责任。您很可能在开发和测试代码时同时担任这两种角色,但是您不必知道生产数据源是如何配置的。

当您使用 Spring 的 R2DBC 时,您可以使用第三方提供的连接池实现配置自己的连接池。一个流行的实现是 R2DBC Pool (r2dbc-pool)。Spring 发行版中的实现仅用于测试目的。

配置 ConnectionFactory:

  1. 获取 ConnectionFactory 连接,就像通常获取 R2DBC ConnectionFactory 一样。

  2. 提供一个 R2DBC URL(请参阅驱动程序的文档以获得正确的值).

下面的示例演示如何配置 ConnectionFactory:

Java
ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
Kotlin
val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");

4.4.2. 使用 ConnectionFactoryUtils

ConnectionFactoryUtils 类是一个简便而强大的工具类,它提供了从 ConnectionFactory 获取连接并关闭连接(如果需要)的静态方法。

它支持与订阅者的 Context 绑定连接,例如 R2dbcTransactionManager.

4.4.3. 使用 SingleConnectionFactory

SingleConnectionFactory 类是 DelegatingConnectionFactory 接口的一个实现,它封装了一个在每次使用后没有关闭的 Connection

如果任何客户端代码在使用连接池时调用 close(如使用持久性工具时),则应该将 suppressClose 属性设置为 true。此设置返回一个包装物理连接的 close-suppressing proxy 。注意,您不能再将其强制转换为本机 Connection 或类似对象。

SingleConnectionFactory 主要是一个测试类,如果您的 R2DBC 驱动程序允许这样做的话,它可以用于特定的需求,比如管道。与池化的 ConnectionFactory 相比,它始终重用相同的连接,避免过多地创建物理连接.

4.4.4. 使用 TransactionAwareConnectionFactoryProxy

TransactionAwareConnectionFactoryProxyConnectionFactory 的代理。代理包装 ConnectionFactory,以添加 spring 管理事务的感知。

如果您使用的 R2DBC 客户端没有与 Spring 的 R2DBC 支持集成,则需要使用这个类。在这种情况下,您仍然可以使用这个客户端,同时让这个客户端参与到 Spring 的事务。为了进行资源管理,最好将 R2DBC 客户端与 ConnectionFactoryUtils 的访问适当集成在一起.

有关更多详细信息,请参阅 TransactionAwareConnectionFactoryProxy.

4.4.5. 使用 R2dbcTransactionManager

R2dbcTransactionManager 类是针对单个 R2DBC 数据源的 ReactiveTransactionManager 实现。它将一个 R2DBC 连接从指定的连接工厂绑定到 subscriber Context,可能允许每个连接工厂都有一个订阅者连接。

应用程序代码需要通过 ConnectionFactoryUtils.getConnection(ConnectionFactory) 来检索 R2DBC 连接,而不是 R2DBC 的标准 ConnectionFactory.create()

所有框架类(如 DatabaseClient)都隐式地使用这个策略。如果不与该事务管理器一起使用,则查找策略的行为与普通策略完全相同。因此,它可以在任何情况下使用。

R2dbcTransactionManager 类支持连接的自定义隔离级别.

5. 对象关系映射(ORM) 数据访问

本节介绍使用对象关系映射(ORM) 时的数据访问.

5.1. 介绍 ORM 与 Spring

Spring Framework 支持与 Java Persistence API(JPA) 的集成,并支持本地 Hibernate 用于资源管理、数据访问对象(DAO) 实现和事务策略. 例如,对于 Hibernate 有一些一流的支持,它具有几个方便的 IoC 功能,可以解决许多典型的 Hibernate 集成问题. 您可以通过依赖注入为 ORM(对象关系) 映射工具配置所有支持的功能. 他们可以参与 Spring 的资源和事务管理,并且遵守 Spring 的通用事务和 DAO 异常层次结构. 推荐的集成方式是针对普通的 Hibernate 或 JPA API 编写 DAO 代码.

在创建数据访问应用程序时,Spring 会为您选择的 ORM 层添加重要的增强功能. 您可以根据需要利用尽可能多的集成支持,并且您应该将此集成工作与内部构建类似基础架构的成本和风险进行比较. 您可以像使用库一样使用大部分 ORM 支持,无论技术如何,因为所有内容都设计为一组可重用的 JavaBean. Spring IoC 容器中的 ORM 有助于配置和部署. 因此,本节中的大多数示例都显示了 Spring 容器内的配置.

使用 Spring Framework 创建 ORM DAO 的好处包括:

  • Easier testing. Spring 的 IoC 方法可以轻松交换 Hibernate SessionFactory 实例、 JDBC DataSource 实例、事务管理器和映射对象实现(如果需要) 的实现和配置位置. 这反过来使得单独测试每个与持久性相关的代码变得更加容易.

  • Common data access exceptions. Spring 可以从 ORM 工具中包装异常,将它们从专有(可能已检查) 的异常转换为公共运行时的 DataAccessException 层次结构. 此功能允许您处理大多数不可恢复的持久性异常,仅在适当的层中处理,而不会产生令人讨厌的样板捕获、抛出和异常声明. 您仍然可以根据需要捕获和处理异常. 请记住,JDBC 异常(包括特定于 DB 的方言) 也会转换为相同的层次结构, 这意味着您可以在相同的编程模型中使用 JDBC 执行某些操作.

  • General resource management. Spring 应用程序上下文可以处理 Hibernate SessionFactory 实例、JPA EntityManagerFactory 实例、 JDBC DataSource 实例和其他相关资源的位置和配置. 这使得这些值易于管理和更改. Spring 提供高效、简单和安全的持久化资源处理. 例如,使用 Hibernate 的相关代码通常需要使用相同的 Hibernate Session 来确保效率和正确的事务处理. Spring 通过 Hibernate SessionFactory 暴露当前 Session,可以轻松、透明地创建 Session 并将其绑定到当前线程. 因此,Spring 解决了许多典型 Hibernate 使用的慢性问题,适用于任何本地或 JTA 事务环境.

  • Integrated transaction management. 您可以通过 @Transactional 注解或通过在 XML 配置文件中显式配置事务 AOP 通知, 使用声明式的、面向切面编程(AOP) 样式的方法拦截器包装 ORM 代码. 在这两种情况下,都会为您处理事务语义和异常处理(回滚等) . 如资源和事务管理中所述,您还可以交换各种事务管理器,而不会影响与 ORM 相关的代码. 例如,例如,您可以在本地事务和 JTA 之间进行交换,并在两种方案中使用完全相同的 services(例如声明式事务) . 此外,与 JDBC 相关的代码可以与您用于执行 ORM 的代码进行完全事务集成. 这对数据访问和流但不适合 ORM(例如批处理和 BLOB 流) , 但仍需要与 ORM 操作共享公共事务.

有关更全面的 ORM 支持,包括对 MongoDB 等替代数据库技术的支持,您可能需要查看项目的 Spring Data . 如果您是 JPA 用户,则 Getting Started Accessing Data with JPA 上的入门使用 JPA 访问数据指南提供了很好的介绍.

5.2. 通用 ORM 集成注意事项

本节重点介绍适用于所有 ORM 技术的注意事项. Hibernate 部分提供了更多详细信息,并在具体上下文中显示了这些功能和配置.

Spring 的 ORM 集成的主要目标是清晰的应用程序分层(使用任何数据访问和事务技术) 以及应用程序对象的松散耦合 - 不再需要对数据访问或事务策略的业务服务依赖性、不再需要硬编码的资源查找、更难以替换的单例、没有更多的定制服务注册表. 它的目标是使用一种简单而一致的方法来连接应用程序对象,使它们保持可重用性并尽可能避免容器依赖. 所有独立的数据访问功能都可以在它们自己的方法中使用,但与 Spring 的应用程序上下文概念有很好地集成, 提供基于 XML 的配置和不需要 Spring 感知的普通 JavaBean 实例的交叉引用. 在典型的 Spring 应用程序中,许多重要的对象是 JavaBeans: 数据访问模板、数据访问对象、事务管理器、 使用数据访问对象和事务管理器的业务 services、Web 视图解析器、使用业务 services 的 Web 控制器等等.

5.2.1. 资源与事务管理器

典型的业务应用程序混杂着重复的资源管理代码. 许多项目试图发明自己的解决方案,有时为了方便编程而牺牲正确的故障处理. Spring 提出了适当资源处理的简单解决方案,即在 JDBC 的情况下通过模板化 IoC 并为 ORM 技术应用 AOP 拦截器.

基础结构提供适当的资源处理以及将特定 API 异常适当转换为未经检查的基础异常层次结构. Spring 引入了 DAO 异常层次结构,适用于任何数据访问策略. 对于直接 JDBC,上一节中提到的 JdbcTemplate 类提供了连接处理 和 SQLExceptionDataAccessException 层次结构的正确转换, 包括将特定于数据库的 SQL 错误代码转换为有意义的异常类. 对于 ORM 技术,请参阅下一节,了解如何获得相同的异常转换优势.

在事务管理方面,JdbcTemplate 类通过各自的 Spring 事务管理器挂钩 Spring 事务支持并支持 JTA 和 JDBC 事务. 对于支持的 ORM 技术,Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持. 有关事务支持的详细信息,请参阅 事务管理 .

5.2.2. 转换异常

在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的异常类. DAO 会抛出 HibernateExceptionPersistenceException 的子类,具体取决于使用那种技术. 这些异常都是运行时异常, 不必声明或捕获. 您可能还必须处理 IllegalArgumentExceptionIllegalStateException. 这意味着调用者通常只能将异常视为致命的,除非他们想要依赖持久性技术的异常结构. 如果不将调用者与实现策略联系起来,则无法捕获特定原因(例如乐观锁定失败) . 对于强烈基于 ORM 或不需要任何特殊异常处理(或两者) 的应用程序,这种权衡可能是可以接受的. 但是,Spring 允许通过 @Repository 注解透明地应用异常转换. 以下示例(一个基于 Java 配置,一个基于 XML 配置) 显示如何执行此操作:

Java
@Repository
public class ProductDaoImpl implements ProductDao {

    // class body here...

}
Kotlin
@Repository
class ProductDaoImpl : ProductDao {

    // class body here...

}
<beans>

    <!-- Exception translation bean post processor -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

后置处理器自动查找所有异常转换器(PersistenceExceptionTranslator 接口的实现) , 并标记所有 @Repository 注解的 bean,以便发现的转换器可以拦截并对抛出的异常应用适当的转换.

总之,您可以基于普通持久性技术的 API 和注解实现 DAO, 同时仍然可以从 Spring 管理的事务、依赖注入和 Spring 的自定义异常层次结构的透明异常转换(如果需要) 中受益.

5.3. Hibernate

我们首先在 Spring 环境中介绍 Hibernate 5 ,然后使用它来演示 Spring 对集成 ORM 映射器的方法. 本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体. 大多数这些模式可以直接转换为所有其他支持的 ORM 工具. 然后,本章后面的部分将介绍其他 ORM 技术并展示简要示例.

从 Spring Framework 5.3 开始,Spring 需要 Hibernate ORM 5.2+ 用于 Spring 的 HibernateJpaVendorAdapter 以及本地的 Hibernate SessionFactory 设置.强烈建议将 Hibernate ORM 5.4 与新启动的应用程序一起使用. 为了与 HibernateJpaVendorAdapter 一起使用, Hibernate Search 需要升级到 5.11.6.

5.3.1. 在 Spring 容器中设置 SessionFactory

为避免将应用程序对象绑定到硬编码资源查找,您可以将资源(例如 JDBC DataSource 或 Hibernate SessionFactory) 定义为 Spring 容器中的 bean. 需要访问资源的应用程序对象通过 bean 引用接收对此类预定义实例的引用,如 下一节中的 DAO定 义所示.

以下摘自 XML 应用程序上下文定义显示了如何在其上设置 JDBC DataSource 和 Hibernate SessionFactory:

<beans>

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
        <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
    </bean>

    <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="myDataSource"/>
        <property name="mappingResources">
            <list>
                <value>product.hbm.xml</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <value>
                hibernate.dialect=org.hibernate.dialect.HSQLDialect
            </value>
        </property>
    </bean>

</beans>

从本地 Jakarta Commons DBCP BasicDataSource 切换到位于JNDI的 DataSource(通常由应用程序服务器管理) 只是配置问题,如下例所示:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

您还可以使用 Spring 的 JndiObjectFactoryBean<jee:jndi-lookup> 来访问位于 JNDI 的 SessionFactory,以检索并暴露它. 但是,这通常在 EJB 上下文之外并不常见.

Spring 还提供了 LocalSessionFactoryBuilder 变体,与 @Bean 样式配置和程序设置无缝集成(不涉及 FactoryBean) .

LocalSessionFactoryBeanLocalSessionFactoryBuilder 都支持后台引导, Hibernate 初始化与给定引导执行程序(例如 SimpleAsyncTaskExecutor) 上的应用程序引导线程并行运行. 在 LocalSessionFactoryBean 上,可以通过 bootstrapExecutor 属性获得引导程序. 在编程式的 LocalSessionFactoryBuilder 上,有一个重载的 buildSessionFactory 方法,它接受一个 bootstrap executor 参数.

从 Spring Framework 5.1 开始,这样的本地 Hibernate 设置还可以在本地 Hibernate 访问时暴露 JPA EntityManagerFactory 以进行标准 JPA 交互. 有关详细信息,请参阅JPA 的本地 Hibernate 设置.

5.3.2. 基于普通的 Hibernate API 实现 DAO

Hibernate 有一个称为上下文会话的功能,其中 Hibernate 本身管理每个事务的一个当前 Session. 这大致相当于 Spring 每次事务同步一个 Hibernate Session. 相应的 DAO 实现类似于以下示例,它是基于普通的 Hibernate API:

Java
public class ProductDaoImpl implements ProductDao {

    private SessionFactory sessionFactory;

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Collection loadProductsByCategory(String category) {
        return this.sessionFactory.getCurrentSession()
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list();
    }
}
Kotlin
class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao {

    fun loadProductsByCategory(category: String): Collection<*> {
        return sessionFactory.currentSession
                .createQuery("from test.Product product where product.category=?")
                .setParameter(0, category)
                .list()
    }
}

除了将 SessionFactory 保存在实例变量中之外,此样式类似于 Hibernate 参考文档和示例的样式. 我们强烈建议在 Hibernate 的 CaveatEmptor 示例应用程序中使用老式 static HibernateUtil 类进行基于实例的设置. (一般情况下,除非绝对必要,否则不要将任何资源保留在 static 变量中. )

前面的 DAO 示例遵循依赖注入模式. 它非常适合 Spring IoC 容器,就像在 Spring 的 HibernateTemplate 中编码一样. 您还可以在普通 Java 中设置这样的 DAO(例如,在单元测试中) . 为此,请实例化它并使用所需的工厂引用调用 setSessionFactory(..). 作为 Spring bean 定义,DAO 将类似于以下内容:

<beans>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

</beans>

这种 DAO 风格的主要优点是它仅依赖于 Hibernate API. 不需要导入任何 Spring 类. 这从非侵入性的角度来看很吸引人,并且可能对 Hibernate 开发人员来说更自然.

但是,DAO 抛出普通的 HibernateException(未经检查,因此不必声明或捕获) , 这意味着调用者通常只能将异常视为致命的,除非他们想要依赖 Hibernate 自己的异常层次结构. 如果不将调用者与实现策略联系起来,则无法捕获特定原因(例如乐观锁定失败) . 对于基于 Hibernate 的强大应用程序,不需要任何特殊异常处理或两者兼而有之,这种权衡可能是可以接受的.

幸运的是,Spring 的 LocalSessionFactoryBean 支持任何 Spring 事务策略的 Hibernate 的 SessionFactory.getCurrentSession() 方法, 返回当前 Spring 管理的事务 Session,即使使用 HibernateTransactionManager 也是如此. 该方法的标准行为仍然是返回与正在进行的 JTA 事务关联的当前 Session(如果有) . 无论您使用的是 Spring 的 JtaTransactionManager、EJB容器管理事务(CMT) 还是 JTA,此行为都适用.

总之,您可以基于普通的 Hibernate API 实现 DAO,同时仍然可以参与 Spring 管理的事务.

5.3.3. 声明式事务划分

我们建议您使用 Spring 的声明式事务支持,它允许您使用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 的调用. 您可以使用 Java 注解或 XML 在 Spring 容器中配置此事务拦截器. 这种声明式事务功能使您可以保持业务服务不受重复的事务划分代码的影响,并专注于添加业务逻辑,这是您的应用程序的真正价值.

在继续之前,我们强烈建议您阅读 声名式事务管理(如果尚未这样做) .

您可以使用 @Transactional 注解来注解服务层,并指示 Spring 容器查找这些注解并为这些带注解的方法提供事务语义. 以下示例显示了如何执行此操作:

Java
public class ProductServiceImpl implements ProductService {

    private ProductDao productDao;

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    @Transactional
    public void increasePriceOfAllProductsInCategory(final String category) {
        List productsToChange = this.productDao.loadProductsByCategory(category);
        // ...
    }

    @Transactional(readOnly = true)
    public List<Product> findAllProducts() {
        return this.productDao.findAllProducts();
    }
}
Kotlin
class ProductServiceImpl(private val productDao: ProductDao) : ProductService {

    @Transactional
    fun increasePriceOfAllProductsInCategory(category: String) {
        val productsToChange = productDao.loadProductsByCategory(category)
        // ...
    }

    @Transactional(readOnly = true)
    fun findAllProducts() = productDao.findAllProducts()
}

在容器中,您需要设置 PlatformTransactionManager 实现(作为bean) 和 <tx:annotation-driven/> 条目,在运行时选择 @Transactional 处理. 以下示例显示了如何执行此操作:

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

    <!-- SessionFactory, DataSource, etc. omitted -->

    <bean id="transactionManager"
            class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <tx:annotation-driven/>

    <bean id="myProductService" class="product.SimpleProductService">
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>

5.3.4. 编程式事务划分

除了跨越任意数量操作的低级数据访问服务之外,您还可以在更高级别的应用程序中划分事务. 对周围业务服务的实现也没有限制. 它只需要一个 Spring PlatformTransactionManager. 同样,后者可以来自任何地方,但最好通过 setTransactionManager(..) 方法作为 bean 引用. 此外,productDAO 应该由 setProductDao(..) 方法设置. 以下一对片段在 Spring 应用程序上下文中显示事务管理器和业务服务定义,以及业务方法实现的示例:

<beans>

    <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

    <bean id="myProductService" class="product.ProductServiceImpl">
        <property name="transactionManager" ref="myTxManager"/>
        <property name="productDao" ref="myProductDao"/>
    </bean>

</beans>
Java
public class ProductServiceImpl implements ProductService {

    private TransactionTemplate transactionTemplate;
    private ProductDao productDao;

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void setProductDao(ProductDao productDao) {
        this.productDao = productDao;
    }

    public void increasePriceOfAllProductsInCategory(final String category) {
        this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            public void doInTransactionWithoutResult(TransactionStatus status) {
                List productsToChange = this.productDao.loadProductsByCategory(category);
                // do the price increase...
            }
        });
    }
}
Kotlin
class ProductServiceImpl(transactionManager: PlatformTransactionManager,
                        private val productDao: ProductDao) : ProductService {

    private val transactionTemplate = TransactionTemplate(transactionManager)

    fun increasePriceOfAllProductsInCategory(category: String) {
        transactionTemplate.execute {
            val productsToChange = productDao.loadProductsByCategory(category)
            // do the price increase...
        }
    }
}

Spring 的 TransactionInterceptor 允许使用回调代码抛出任何已检查的应用程序异常,而 TransactionTemplate 仅限于回调中未经检查的异常. 如果未经检查的应用程序异常或者事务被应用程序标记为仅回滚(通过设置 TransactionStatus) ,TransactionTemplate 将触发回滚. 默认情况下,TransactionInterceptor 的行为方式相同,但每个方法允许可配置的回滚策略.

5.3.5. 事务管理策略

TransactionTemplateTransactionInterceptor 都将实际的事务处理委托给一个 PlatformTransactionManager 实例 (可以是一个 HibernateTransactionManager(用于单个 Hibernate SessionFactory) , 通过使用引擎下的 ThreadLocal Session) 或者一个 JtaTransactionManager(委托给容器的JTA子系统) 来实现 Hibernate 应用. 您甚至可以使用自定义的 PlatformTransactionManager 实现. 从本地 Hibernate 事务管理切换到 JTA(例如,当应用程序的某些部署时面临分布式事务要求) 只是配置问题. 您可以使用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器. 事务划分和数据访问代码都可以无变化地工作,因为它们使用通用事务管理 APIs.

对于跨多个 Hibernate 会话工厂的分布式事务,您可以将 JtaTransactionManager 作为事务策略与多个 LocalSessionFactoryBean 定义相结合. 然后,每个 DAO 都会将一个特定的 SessionFactory 引用传递到其相应的 bean 属性中. 如果所有基础 JDBC 数据源都是事务性容器, 则业务服务可以跨越任意数量的 DAO 和任意数量的会话工厂划分事务,而无需特别考虑,只要它使用 JtaTransactionManager 作为策略.

HibernateTransactionManagerJtaTransactionManager 都允许使用 Hibernate 进行正确的 JVM 级缓存处理, 无需特定于容器的事务管理器查找或 JCA 连接器(如果不使用 EJB 来启动事务) .

HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出为特定 DataSource 的普通 JDBC 访问代码. 如果您只访问一个数据库,则此功能允许在没有 JTA 的情况下完全混合 Hibernate 和 JDBC 数据访问的高级事务划分. 如果您通过 LocalSessionFactoryBean 类的 dataSource 属性设置了传入的 SessionFactoryDataSource, HibernateTransactionManager 会自动将 Hibernate 事务暴露为 JDBC 事务. 或者,您可以通过 HibernateTransactionManager 类的 dataSource 属性显式指定应该为其暴露事务的 DataSource.

5.3.6. 比较容器管理和本地定义的资源

您可以在容器管理的 JNDI SessionFactory 和本地定义的 JNDI SessionFactory 之间切换,而无需更改单行应用程序代码. 是将资源定义保留在容器中还是本地保存在应用程序中主要取决于您使用的事务策略. 与 Spring 定义的本地 SessionFactory 相比,手动注册的 JNDI SessionFactory 不提供任何好处. 通过 Hibernate 的 JCA 连接器部署 SessionFactory 提供了参与 Java EE 服务器管理基础结构的附加价值,但不会增加实际价值.

Spring 的事务支持不限于容器. 使用除 JTA 之外的任何策略进行配置时,事务支持也可以在独立或测试环境中运行. 特别是在单数据库事务的典型情况下,Spring 的单资源本地事务支持是 JTA 的轻量级和强大的替代方案. 当您使用本地 EJB 无状态会话 bean 来驱动事务时,您既依赖于 EJB 容器又依赖于 JTA,即使您只访问单个数据库并且仅使用无状态会话 bean 来通过容器管理的事务提供声明式事务. 以编程方式直接使用 JTA 还需要 Java EE 环境. JTA 不仅包含对 JTA 本身的容器依赖性,还有对 JNDI DataSource 实例的依赖性. 对于非 Spring,JTA 驱动的 Hibernate 事务,您必须使用 Hibernate JCA 连接器或额外的 Hibernate 事务代码,并将 TransactionManagerLookup 配置为正确的 JVM 级缓存.

Spring 驱动的事务也可以与本地定义的 Hibernate SessionFactory 一样工作,就像它们使用本地 JDBC DataSource 一样, 前提是它们访问单个数据库. 因此,当您具有分布式事务要求时,只需使用 Spring 的 JTA 事务策略. JCA 连接器需要特定于容器的部署步骤,并且首先需要 JCA 支持. 与使用本地资源定义和Spring驱动的事务部署简单 Web 应用程序相比,此配置需要更多工作. 此外,如果您使用的是不提供 JCA 的 WebLogic Express,则通常需要容器的 Enterprise Edition. 具有跨越单个数据库的本地资源和事务的 Spring 应用程序可在任何 Java EE Web 容器(无 JTA,JCA 或 EJB) 中工作,例如 Tomcat,Resin 甚至普通 Jetty. 此外,您可以轻松地在桌面应用程序或测试套件中重用这样的中间层.

考虑到所有事情,如果您不使用 EJB,请坚持使用本地 SessionFactory 设置和 Spring 的 HibernateTransactionManagerJtaTransactionManager. 您可以获得所有好处,包括正确的事务性 JVM 级缓存和分布式事务,而不会给容器部署带来不便. 只有在与 EJB 结合使用时才会添加值,以通过 JCA 连接器对 Hibernate SessionFactory 进行 JNDI 注册.

5.3.7. Hibernate 的虚假应用程序服务器警告

在一些具有非常严格的 XADataSource 实现的 JTA 环境中(当前只有一些 WebLogic Server 和 WebSphere 版本) , 当配置 Hibernate 而不考虑该环境的 JTA 事务管理时,虚假警告或异常可以显示在应用程序服务器日志中. 这些警告或异常表明正在访问的连接不再有效或 JDBC 访问不再有效,可能是因为事务不再处于 active 状态. 例如,以下是 WebLogic 的实际异常:

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

另一个常见的问题是 JTA 事务后的连接泄漏, 并且 Hibernate 会话 (以及潜在的 JDBC 连接) 无法正确关闭.

您可以通过让 Hibernate 知道它同步的 JTA PlatformTransactionManager 实例(以及Spring) 来解决此警告. 您有两种选择:

  • 将您的 Spring JtaTransactionManager bean 传递到您的 Hibernate 设置中. 最简单的方法是对 LocalSessionFactoryBean bean 的 jtaTransactionManager 属性进行 bean 引用 (请参见 Hibernate Transaction 设置) . 然后, Spring 将相应的 JTA 策略提供给 Hibernate.

  • 您还可以在 LocalSessionFactoryBean 的 "hibernateProperties" 中显式配置与 Hibernate 的 JTA 相关的属性, 尤其是 "hibernate.transaction.coordinator_class", "hibernate.connection.handling_mode" 以及可能的 "hibernate.transaction.jta.platform" (请参见 Hibernate 的手册) . 有关这些属性的详细信息)

本节的其余部分描述了在 Hibernate 不了解 JTA PlatformTransactionManager 的情况下发生的事件的顺序.

当 Hibernate 配置为不知道 JTA 事务管理 时,JTA 事务提交时会发生以下事件:

  • JTA 事务提交.

  • Spring 的 JtaTransactionManager 与 JTA 事务同步,因此 JTA 事务管理器通过 afterCompletion 回调调用它.

  • 在其他 active 中,这种同步可以触发 Spring 对 Hibernate 的回调,通过 Hibernate 的 afterTransactionCompletion 方法回调(用于清除 Hibernate 缓存) , 然后在 Hibernate 会话上进行显式的 close() 调用,这会导致 Hibernate 尝试调用 close() 关闭 JDBC 连接.

  • 在某些环境中,此 Connection.close() 调用会触发警告或错误,因为应用程序服务器不再认为 Connection 可用,因为事务已经提交.

当 Hibernate 配置为知道 JTA 事务管理 时,JTA 事务提交时会发生以下事件:

  • JTA 事务已准备好提交.

  • Spring 的 JtaTransactionManager 与 JTA 事务同步,因此JTA事务管理器通过 beforeCompletion 回调方法调用该事务.

  • Spring 知道 Hibernate 本身与 JTA 事务同步,其行为与前一个场景不同. 特别是它与 Hibernate 的事务性资源管理.

  • JTA 事务提交.

  • Hibernate 与 JTA 事务同步,因此事务由 JTA 事务管理器通过 afterCompletion 回调调用,并可以正确清除其缓存.

5.4. JPA

Spring JPA 在 org.springframework.orm.jpa 包下提供,以类似于与 Hibernate 集成的方式提供对 Java 持久化 API 的全面支持,同时了解底层实现以提供其他功能.

5.4.1. Spring 环境中 JPA 设置的三个选项

Spring JPA 支持提供了三种设置 JPA EntityManagerFactory 的方法,该方法由应用程序用于获取实体管理器.

使用 LocalEntityManagerFactoryBean

您只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试.

LocalEntityManagerFactoryBean 创建一个适用于简单部署环境的 EntityManagerFactory, 其中应用程序仅使用 JPA 进行数据访问. 工厂 bean 使用JPA PersistenceProvider 自动检测机制(根据 JPA 的 Java SE 引导) , 并且在大多数情况下,要求您仅指定持久性单元名称. 以下 XML 示例配置了这样一个 bean:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="myPersistenceUnit"/>
    </bean>
</beans>

这种形式的 JPA 部署是最简单和最受限的. 您不能引用现有的 JDBC DataSource bean定义,也不存在对全局事务的支持. 此外,持久化类的织入(字节码转换) 是特定于提供者的,通常需要在启动时指定特定的 JVM 代理. 此选项仅适用于为其设计 JPA 规范的独立应用程序和测试环境.

从 JNDI 获取EntityManagerFactory

部署到 Java EE 服务器时,可以使用此选项. 检查服务器的文档,了解如何将自定义 JPA 提供程序部署到服务器中,从而允许使用与服务器默认提供程序不同的提供程序.

从 JNDI 获取 EntityManagerFactory(例如在 Java EE 环境中) ,需要更改 XML 配置,如以下示例所示:

<beans>
    <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>

此操作假定由标准 Java EE 引导. Java EE 服务器自动检测 Java EE 部署描述符中的持久性单元 (实际上是应用程序 jar 中的 META-INF/persistence.xml 文件) 和 persistence-unit-ref 条目 (例如,web.xml) 并定义这些持久性单元命名的上下文位置环境.

在这种情况下,整个持久化单元的部署,包括持久化类的织入(字节码转换) ,都是由 Java EE 服务器决定. JDBC DataSource 是通过 META-INF/persistence.xml 文件中的 JNDI 位置定义的. EntityManager 事务与服务器的 JTA 子系统集成在一起. Spring 仅使用获取的 EntityManagerFactory,通过依赖注入将其传递给应用程序对象,并管理持久化单元的事务(通常通过 JtaTransactionManager) .

如果在同一个应用程序中使用多个持久性单元, 则此类 JNDI 检索的持久化单元的 bean 名称应与应用程序用于引用它们的持久化单元名称匹配 (例如,在 @PersistenceUnit@PersistenceContext 注解中) .

使用 LocalContainerEntityManagerFactoryBean

您可以在基于 Spring 的应用程序环境中将此选项用于完整的 JPA 功能. 这包括 Web 容器,如 Tomcat、独立应用程序以及具有复杂持久性要求的集成测试.

如果你想专门配置一个 Hibernate 设置,一个直接的选择是设置一个本地 Hibernate LocalSessionFactoryBean 而不是一个普通的 JPA LocalContainerEntityManagerFactoryBean, 让它与 JPA 访问代码以及本地 Hibernate 访问代码交互. 有关详细信息,请参阅 本地 JPAHibernate 设置.

LocalContainerEntityManagerFactoryBean 完全控制 EntityManagerFactory 配置,适用于需要细粒度自定义的环境. LocalContainerEntityManagerFactoryBean 基于 persistence.xml 文件, 提供的 dataSourceLookup 策略和指定的 loadTimeWeaver 创建 PersistenceUnitInfo 实例. 因此,可以使用 JNDI 之外的自定义数据源并控制织入过程. 以下示例显示了 LocalContainerEntityManagerFactoryBean 的典型 bean 定义:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="someDataSource"/>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
        </property>
    </bean>
</beans>

以下示例显示了典型的 persistence.xml 文件:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
    <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
        <mapping-file>META-INF/orm.xml</mapping-file>
        <exclude-unlisted-classes/>
    </persistence-unit>
</persistence>
<exclude-unlisted-classes/> 快捷方式表示不应扫描带注解的实体类. 显式的 true 值(<exclude-unlisted-classes>true</exclude-unlisted-classes/>) 也意味着没有扫描. <exclude-unlisted-classes>false</exclude-unlisted-classes/> 会触发扫描. 但是,如果要进行实体类扫描,我们建议省略 exclude-unlisted-classes 元素.

使用 LocalContainerEntityManagerFactoryBean 是最强大的 JPA 设置选项,允许在应用程序中进行灵活的本地配置. 它支持指向现有JDBC DataSource 的链接,支持本地和全局事务等. 但是,它还对运行时环境施加了要求,例如,如果持久化提供程序需要字节码转换,则可以使用具有编织功能的类加载器.

此选项可能与 Java EE 服务器的内置 JPA 功能冲突. 在完整的 Java EE 环境中,请考虑从 JNDI 获取 EntityManagerFactory. 或者,在 LocalContainerEntityManagerFactoryBean 定义上指定自定义 persistenceXmlLocation(例如,META-INF/persistence.xml) , 并在应用程序 jar 文件中仅包含具有该名称的描述符. 由于 Java EE 服务器仅查找默认的 META-INF/persistence.xml 文件, 因此它会忽略此类自定义持久化单元,因此可以避免与 Spring 驱动的 JPA 设置发生冲突. (例如,这适用于 Resin 3.1. )

何时需要加载时织入?

并非所有 JPA 提供程序都需要 JVM 代理. Hibernate 就是一个没有的例子. 如果您的提供程序不需要代理或您有其他替代方法,例如通过自定义编译器或 Ant 任务在构建时应用增强功能,则不应使用加载时编织器.

LoadTimeWeaver 接口是 Spring 提供的类,它允许以特定方式插入 JPA ClassTransformer 实例,具体取决于环境是 Web 容器还是应用程序服务器. 通过 agent 挂钩 ClassTransformer 通常效率不高. 代理程序可以对整个虚拟机进行操作,并检查所加载的每个类,这在生产服务器环境中通常是不推荐的.

Spring 为各种环境提供了许多 LoadTimeWeaver 实现,允许 ClassTransformer 实例仅应用于每个类加载器而不是每个 VM.

有关 LoadTimeWeave r实现及其设置的更多信息,请参阅 AOP 章节中的 Spring 配置 , 这些实现可以是通用的,也可以是针对各种平台(例如 Tomcat,WebLogic,GlassFish,Resin 和 JBoss) 定制的.

Spring配置 中所述,您可以使用 context:load-time-weaver XML 元素的 @EnableLoadTimeWeaving 注解配置上下文范围的 LoadTimeWeaver. 所有 JPA LocalContainerEntityManagerFactoryBean 实例都会自动选择这样的全局编织器. 以下示例显示了设置加载时织入器的首选方法,提供对平台(WebLogic,GlassFish,Tomcat,Resin,JBoss 或 VM 代理) 的自动检测以及将织入器自动传播到所有 weaver-aware bean:

<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    ...
</bean>

但是,如果需要,您可以通过 loadTimeWeaver 属性手动指定专用的 weaver,如以下示例所示:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
    </property>
</bean>

无论如何配置 LTW,通过使用此技术,依赖于检测的 JPA 应用程序可以在目标平台(例如,Tomcat) 中运行,而无需代理. 当托管应用程序依赖于不同的 JPA 实现时,这一点尤为重要,因为 JPA 转换器仅应用于类加载器级别,因此彼此隔离.

处理多个持久化单元

对于依赖于多个持久性单元位置的应用程序(例如,存储在类路径中的各种 JARS 中) , Spring 提供 PersistenceUnitManager 作为中央存储库并避免持久化单元发现过程,这可能很昂贵. 默认实现允许指定多个位置. 解析这些位置,然后通过持久化单元名称检索这些位置. (默认情况下,会在类路径中搜索 META-INF/persistence.xml 文件. ) 以下示例配置多个位置:

<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager">
    <property name="persistenceXmlLocations">
        <list>
            <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value>
            <value>classpath:/my/package/**/custom-persistence.xml</value>
            <value>classpath*:META-INF/persistence.xml</value>
        </list>
    </property>
    <property name="dataSources">
        <map>
            <entry key="localDataSource" value-ref="local-db"/>
            <entry key="remoteDataSource" value-ref="remote-db"/>
        </map>
    </property>
    <!-- if no datasource is specified, use this one -->
    <property name="defaultDataSource" ref="remoteDataSource"/>
</bean>

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceUnitManager" ref="pum"/>
    <property name="persistenceUnitName" value="myCustomUnit"/>
</bean>

默认实现允许自定义 PersistenceUnitInfo 实例(在它们被提供给 JPA 提供者之前) ,以声明方式(通过其属性,影响所有托管单元) 或以编程方式(通过 PersistenceUnitPostProcessor,允许持久化单元选择) 进行定义. 如果未指定 PersistenceUnitManager,则由 LocalContainerEntityManagerFactoryBean 在内部创建并使用一个 PersistenceUnitInfo 实例.

后台引导

LocalContainerEntityManagerFactoryBean 通过 bootstrapExecutor 属性支持后台引导,如以下示例所示:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="bootstrapExecutor">
        <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
    </property>
</bean>

实际的 JPA 提供程序引导传递给指定的执行程序,然后并行运行到应用程序引导程序线程. 暴露的 EntityManagerFactory 代理可以注入其他应用程序组件,甚至能够响应 EntityManagerFactoryInfo 配置检查. 但是,一旦其他组件访问了实际的 JPA 提供程序(例如,调用 createEntityManager) ,这些调用将阻塞,直到后台引导完成. 特别是,当您使用 Spring Data JPA 时,请确保为其存储库设置延迟引导.

5.4.2. 基于 JPA 实现 DAO: EntityManagerFactoryEntityManager

虽然 EntityManagerFactory 实例是线程安全的,但 EntityManager 实例不是安全的. 注入的 JPA EntityManager 的行为类似于从应用程序服务器的 JNDI 环境获取的 EntityManager,正如 JPA 规范所定义. 它将所有的调用委托给当前事务式的 EntityManager(如果有) . 否则,它会回退到每个操作新创建的 EntityManager,实际上使其使用线程安全.

通过使用注入的 EntityManagerFactoryEntityManager,可以在没有任何 Spring 依赖性的情况下针对普通 JPA 编写代码. 如果启用了 PersistenceAnnotationBeanPostProcessor,Spring 可以在字段和方法级别理解 @PersistenceUnit@PersistenceContext 注解. 以下示例显示了使用 @PersistenceUnit 注解的普通JPA DAO实现:

Java
public class ProductDaoImpl implements ProductDao {

    private EntityManagerFactory emf;

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.emf = emf;
    }

    public Collection loadProductsByCategory(String category) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Query query = em.createQuery("from Product as p where p.category = ?1");
            query.setParameter(1, category);
            return query.getResultList();
        }
        finally {
            if (em != null) {
                em.close();
            }
        }
    }
}
Kotlin
class ProductDaoImpl : ProductDao {

    private lateinit var emf: EntityManagerFactory

    @PersistenceUnit
    fun setEntityManagerFactory(emf: EntityManagerFactory) {
        this.emf = emf
    }

    fun loadProductsByCategory(category: String): Collection<*> {
        val em = this.emf.createEntityManager()
        val query = em.createQuery("from Product as p where p.category = ?1");
        query.setParameter(1, category);
        return query.resultList;
    }
}

前面的 DAO 不依赖于 Spring,仍然适合 Spring 应用程序上下文. 此外,DAO 利用注解来要求注入默认的 EntityManagerFactory,如下面的示例 bean 定义所示:

<beans>

    <!-- bean post-processor for JPA annotations -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

作为显式定义 PersistenceAnnotationBeanPostProcessor 的替代方法,请考虑在应用程序上下文配置中使用 Spring 的 context:annotation-config XML 元素. 这样做会自动注册所有 Spring 标准的后处理器以进行基于注解的配置,包括 CommonAnnotationBeanPostProcessor 等.

请考虑以下示例:

<beans>

    <!-- post-processors for all standard config annotations -->
    <context:annotation-config/>

    <bean id="myProductDao" class="product.ProductDaoImpl"/>

</beans>

这种 DAO 的主要问题是它总是通过工厂创建一个新的 EntityManager. 您可以通过请求事务式的 EntityManager (也称为 “shared EntityManager”,因为它是实际事务性 EntityManager 的共享,线程安全代理) 而不是工厂来避免这种情况. 以下示例显示了如何执行此操作:

Java
public class ProductDaoImpl implements ProductDao {

    @PersistenceContext
    private EntityManager em;

    public Collection loadProductsByCategory(String category) {
        Query query = em.createQuery("from Product as p where p.category = :category");
        query.setParameter("category", category);
        return query.getResultList();
    }
}
Kotlin
class ProductDaoImpl : ProductDao {

    @PersistenceContext
    private lateinit var em: EntityManager

    fun loadProductsByCategory(category: String): Collection<*> {
        val query = em.createQuery("from Product as p where p.category = :category")
        query.setParameter("category", category)
        return query.resultList
    }
}

@PersistenceContext 注解有一个名为 type 的可选属性,默认为 PersistenceContextType.TRANSACTION. 您可以使用此默认值来接收共享的 EntityManager 代理. 相反的,PersistenceContextType.EXTENDED 是一个完全不同的事情. 这导致了一个所谓的扩展 EntityManager,它不是线程安全的,因此不能在并发访问的组件中使用,例如 Spring 管理的单例 bean. 扩展的 EntityManager 实例仅应用于有状态组件,例如,驻留在会话中,EntityManager 的生命周期不依赖于当前事务,而是完全取决于应用程序.

方法和字段级的注入

您可以在类中的字段或方法上应用指示依赖注入的注解(例如 @PersistenceUnit@PersistenceContext) - 因此表达式为 "方法级注入" 和 "字段级注入". 字段级注解简洁且易于使用,而方法级注解允许进一步处理注入的依赖. 在这两种情况下,成员可见性(公共,受保护或私有) 都无关紧要.

什么是类级别注解?

在 Java EE 平台上,它们用于依赖的声明,而不用于资源注入.

注入的 EntityManager 是 Spring 管理的(了解正在进行的事务) . 即使新的 DAO 实现使用 EntityManager 的方法级注入而不是 EntityManagerFactory,但由于注解的使用,在应用程序上下文的 XML 文件中不需要进行任何更改.

这种 DAO 风格的主要优点是它仅依赖于 Java 持久化 API. 不需要导入任何 Spring 类. 此外,当理解 JPA 注解时,注入由 Spring 容器自动应用. 从非侵入性的角度来看,这很有吸引力,并且对于 JPA 开发人员来说感觉更自然.

5.4.3. Spring 驱动的 JPA 事务

我们强烈建议您阅读 声名式事务管理 (如果您还没有这样做) ,以获得有关 Spring 声明式事务支持的更详细的介绍. ,

JPA 的推荐策略是通过 JPA 的本地事务支持进行本地事务. Spring 的 JpaTransactionManager 针对任何常规 JDBC 连接池(无 XA 要求) 提供了许多本地 JDBC 事务(例如特定于事务的隔离级别和资源级只读优化) 的功能.

Spring JPA 还允许配置的 JpaTransactionManager 将 JPA 事务暴露给访问相同 DataSource 的 JDBC 访问代码, 前提是已注册的 JpaDialect 支持检索基础 JDBC Connection. Spring 为 EclipseLink 和 Hibernate JPA 实现提供了方言. 有关 JpaDialect 机制的详细信息,请参阅 下一节.

NOTE: 作为一种直接替代方案,Spring 的本地 HibernateTransactionManager 能够与 JPA 访问代码交互,适应多种特定的 Hibernate 细节并提供 JDBC 交互. 这与 LocalSessionFactoryBean 设置结合使用特别有意义. 有关详细信息,请参阅 JPA 交互的本地 Hibernate 设置 .

5.4.4. 了解 JpaDialectJpaVendorAdapter

作为一项高级功能,JpaTransactionManagerAbstractEntityManagerFactoryBean 的子类允许将自定义 JpaDialect 传递给 JpaDialect bean 属性. JpaDialect 实现可以启用 Spring 支持的以下高级功能,通常以特定于供应商的方式:

  • 应用特定的事务语义(例如自定义隔离级别或事务超时)

  • 检索事务 JDBC Connection(用于暴露基于JDBC的DAO)

  • PersistenceExceptions 到 Spring DataAccessExceptions 的高级转换

这对于特殊事务语义和异常的高级转换特别有用. 默认实现(DefaultJpaDialect) 不提供任何特殊功能,如果需要前面列出的功能,则必须指定相应的方言.

作为一个更广泛的提供者适配工具,主要用于 Spring 的全功能 LocalContainerEntityManagerFactoryBean 设置, JpaVendorAdapterJpaDialect 的功能与其他特定于提供者的默认设置相结合. 指定 HibernateJpaVendorAdapterEclipseLinkJpaVendorAdapter 是分别为 Hibernate或EclipseLink 自动配置 EntityManagerFactory 设置的最便捷方式. 请注意,这些提供程序适配器主要设计用于 Spring 驱动的事务管理(即,与 JpaTransactionManager 一起使用) .

有关其操作的更多详细信息以及如何在 Spring 的 JPA 支持中使用它们,请参阅 JpaDialectJpaVendorAdapter javadoc.

5.4.5. 使用 JTA 事务管理设置 JPA

作为 JpaTransactionManager 的替代方案,Spring 还允许通过 JTA 进行多资源事务协调,无论是在 Java EE 环境中还是在独立的事务协调器(如 Atomikos) 上. 除了选择 Spring 的 JtaTransactionManager 而不是 JpaTransactionManager 之外,您还需要采取以下几个步骤:

  • 底层 JDBC 连接池需要具备 XA 功能,并与事务协调器集成. 这在 Java EE 环境中通常很简单,通过 JNDI 暴露不同类型的 DataSource. 有关详细信息,请参阅应用程序服器文档. 类似地,独立的事务协调器通常带有特殊的 XA 集成的 DataSource 实现. 再次检查其文档.

  • 需要为 JTA 配置 JPA EntityManagerFactory 设置. 这是特定于提供者的, 通常通过在 LocalContainerEntityManagerFactoryBean 上指定为 jpaProperties 的特殊属性. 在 Hibernate 的情况下,这些属性甚至是特定于版本的. 有关详细信息,请参阅 Hibernate 文档.

  • Spring 的 HibernateJpaVendorAdapter 强制执行某些面向 Spring 的默认设置,例如连接释放模式 on-close, 它与 Hibernate 在 Hibernate 5.0 中的默认设置相匹配,但在 5.1+ 中则不再匹配. 对于 JTA 设置,确保声明您的持久性单元事务类型为 "JTA". 或者,将 Hibernate 5.2 的 hibernate.connection.handling_mode 属性 设置为 DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT 以恢复 Hibernate 自己的默认值. 有关 WebLogic 的相关说明,请参阅 Hibernate 的虚假应用程序服务器警告 .

  • 或者,考虑从应用程序服务器本身获取 EntityManagerFactory(即,通过 JNDI 查找而不是本地声明的 LocalContainerEntityManagerFactoryBean) . 服务器提供的 EntityManagerFactory 可能需要在服务器配置中使用特殊的定义(使部署不那么可移植) ,而是由服务器的 JTA 环境设置.

5.4.6. 用于 JPA 交互的本地 Hibernate 设置和本地 Hibernate 事务

本地 LocalSessionFactoryBean 设置与 HibernateTransactionManager 结合使用, 可以与 @PersistenceContext 和其他 JPA 访问代码进行交互. Hibernate SessionFactory 现在在本地实现 JPA 的 EntityManagerFactory 接口,而 Hibernate Session 句柄本身就是 JPA EntityManager. Spring 的 JPA 支持工具自动检测本地 Hibernate 会话.

因此,在许多场景中,这种本地 Hibernate 设置可以替代标准 JPA LocalContainerEntityManagerFactoryBeanJpaTransactionManager 组合, 允许在同一本地事务中与 @PersistenceContext EntityManager 旁边的 SessionFactory.getCurrentSession()(以及 HibernateTemplate) 进行交互. 这样的设置还提供了更强大的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导程序契约的约束.

在这种情况下,您不需要 HibernateJpaVendorAdapter 配置,因为 Spring 的本地 Hibernate 设置提供了更多功能 (例如,自定义 Hibernate Integrator 设置,Hibernate 5.3 bean 容器集成以及对只读事务的更强优化) . 最后但同样重要的是,您还可以通过 LocalSessionFactoryBuilder 表达本地 Hibernate 设置,与 @Bean 样式配置无缝集成(不涉及 FactoryBean) .

LocalSessionFactoryBeanLocalSessionFactoryBuilder 支持后台引导, 就像 JPA LocalContainerEntityManagerFactoryBean 一样. 有关简介,请参阅 后台引导.

LocalSessionFactoryBean 上,可以通过 bootstrapExecutor 属性获得. 在编程式的 LocalSessionFactoryBuilder 上,重载的 buildSessionFactory 方法采用 bootstrap executor 参数来获得后台引导.

6. 使用 Object-XML Mappers 编组 XML

6.1. 简介

本章介绍 Spring 的 Object-XML Mapping 支持. Object-XML Mapping(简称O-X映射) 是将 XML 文档转换为对象或从对象转换 XML 文档的行为. 此转换过程也称为 XML 编组或 XML 序列化. 本章可互换使用这些术语.

在 O-X 映射领域,编组器负责将对象(图形) 序列化为 XML. 以类似的方式,解组器将 XML 反序列化为对象图. 此 XML 可以采用 DOM 文档、输入或输出流或 SAX 处理程序的形式

使用 Spring 满足 O/X 映射需求的一些好处是:

6.1.1. 易于配置

Spring 的 bean 工厂可以轻松配置编组器,而无需构建 JAXB 上下文、JiBX 绑定工厂等. 您可以像应用程序上下文中的任何其他 bean 一样配置编组器. 此外,基于 XML 命名空间的配置可用于许多编组器,使配置更简单.

6.1.2. 一致的接口

Spring 的 O-X 映射通过两个全局接口进行操作: MarshallerUnmarshaller. 这些抽象允许您相对轻松地切换 O-X 映射框架,对执行编组的类几乎不需要更改. 这种方法的另一个好处是可以使用混合匹配方法进行 XML 编组(例如,使用 JAXB 执行某些编组操作,而使用 Castor 执行某些编组操作) 以非侵入方式执行,使您可以使用每种技术的优势.

6.1.3. 一致的异常层次

Spring 提供从底层 O-X 映射工具的异常到其自己的异常层次结构的转换,其中 XmlMappingException 作为根异常. 这些运行时异常包装原始异常,因此不会丢失任何信息.

6.2. MarshallerUnmarshaller

简介 中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象. 本节介绍用于此目的的两个 Spring 接口.

6.2.1. 了解 Marshaller

Spring 抽象了 org.springframework.oxm.Marshaller 接口后面的所有编组操作,其主要方法如下:

public interface Marshaller {

    /**
     * Marshal the object graph with the given root into the provided Result.
     */
    void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}

Marshaller 接口有一个主方法,它将给定对象封送到给定的 javax.xml.transform.Result. 结果是一个标记接口,它基本上代表 XML 输出抽象. 具体实现包装各种 XML 表示,如下表所示:

Result implementation Wraps XML representation

DOMResult

org.w3c.dom.Node

SAXResult

org.xml.sax.ContentHandler

StreamResult

java.io.File, java.io.OutputStream, or java.io.Writer

尽管 marshal() 方法接受普通对象作为其第一个参数,但大多数 Marshaller 实现都无法处理任意对象. 相反,对象类必须映射到映射文件中,使用注解标记,向编组器注册或具有公共基类. 请参阅本章后面的章节,以确定 O-X 技术如何管理它.

6.2.2. 了解 Unmarshaller

Marshaller 类似,我们有 org.springframework.oxm.Unmarshaller 接口,如下所示:

public interface Unmarshaller {

    /**
     * Unmarshal the given provided Source into an object graph.
     */
    Object unmarshal(Source source) throws XmlMappingException, IOException;
}

此接口还有一个方法,它从给定的 javax.xml.transform.Source(XML输入抽象) 中读取并返回读取的对象. 与 Result 一样,Source 是一个标记接口,具有三个具体实现. 每个包装一个不同的 XML 表示,如下表所示:

Source implementation Wraps XML representation

DOMSource

org.w3c.dom.Node

SAXSource

org.xml.sax.InputSource, and org.xml.sax.XMLReader

StreamSource

java.io.File, java.io.InputStream, or java.io.Reader

即使有两个独立的编组接口(MarshallerUnmarshaller) ,Spring-WS 中的所有实现都在一个类中实现. 这意味着您可以连接一个编组器类并引用它,并将其作为编组器和 applicationContext.xml 中的解组器.

6.2.3. 了解 XmlMappingException

Spring 将基础 O-X 映射工具中的异常转换为其自己的异常层次结构,并将 XmlMappingException 作为根异常. 这些运行时异常包装原始异常,因此不会丢失任何信息.

此外,MarshallingFailureExceptionUnmarshallingFailureException 提供了编组和解组操作之间的区别, 即使底层的 O-X 映射工具不这样做.

O-X Mapping 异常层次结构如下图所示:

oxm exceptions

6.3. 使用 MarshallerUnmarshaller

您可以在各种情况下使用 Spring 的 OXM. 在下面的示例中,我们使用它将 Spring 托管应用程序的设置编组为 XML 文件. 在以下示例中,我们使用简单的 JavaBean 来表示设置:

Java
public class Settings {

    private boolean fooEnabled;

    public boolean isFooEnabled() {
        return fooEnabled;
    }

    public void setFooEnabled(boolean fooEnabled) {
        this.fooEnabled = fooEnabled;
    }
}
Kotlin
class Settings {
    var isFooEnabled: Boolean = false
}

应用程序类使用此 bean 来存储其设置. 除了主方法之外,该类还有两个方法: saveSettings() 将设置 bean 保存到名为 settings.xml 的文件中,loadSettings() 再次加载这些设置. 以下 main() 方法构造 Spring 应用程序上下文并调用这两个方法:

Java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.oxm.Marshaller;
import org.springframework.oxm.Unmarshaller;

public class Application {

    private static final String FILE_NAME = "settings.xml";
    private Settings settings = new Settings();
    private Marshaller marshaller;
    private Unmarshaller unmarshaller;

    public void setMarshaller(Marshaller marshaller) {
        this.marshaller = marshaller;
    }

    public void setUnmarshaller(Unmarshaller unmarshaller) {
        this.unmarshaller = unmarshaller;
    }

    public void saveSettings() throws IOException {
        try (FileOutputStream os = new FileOutputStream(FILE_NAME)) {
            this.marshaller.marshal(settings, new StreamResult(os));
        }
    }

    public void loadSettings() throws IOException {
        try (FileInputStream is = new FileInputStream(FILE_NAME)) {
            this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
        }
    }

    public static void main(String[] args) throws IOException {
        ApplicationContext appContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Application application = (Application) appContext.getBean("application");
        application.saveSettings();
        application.loadSettings();
    }
}
Kotlin
class Application {

    lateinit var marshaller: Marshaller

    lateinit var unmarshaller: Unmarshaller

    fun saveSettings() {
        FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) }
    }

    fun loadSettings() {
        FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings }
    }
}

private const val FILE_NAME = "settings.xml"

fun main(args: Array<String>) {
    val appContext = ClassPathXmlApplicationContext("applicationContext.xml")
    val application = appContext.getBean("application") as Application
    application.saveSettings()
    application.loadSettings()
}

Application 需要设置 marshallerunmarshaller 属性. 我们可以使用以下 applicationContext.xml 来实现:

<beans>
    <bean id="application" class="Application">
        <property name="marshaller" ref="xstreamMarshaller" />
        <property name="unmarshaller" ref="xstreamMarshaller" />
    </bean>
    <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</beans>

此应用程序上下文使用 Castor,但我们可以使用本章后面介绍的任何其他 marshaller 实例. 请注意,默认情况下,Castor 不需要任何进一步的配置,因此 bean 定义相当简单. 另请注意,CastorMarshaller 实现了 MarshallerUnmarshaller, 因此我们可以在应用程序的 MarshallerUnmarshaller 属性中引用 castorMarshaller bean.

此示例应用程序生成以下 settings.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>

6.4. XML 配置命名空间

您可以使用 OXM 命名空间中的标记更简洁地配置编组器. 要使这些标记可用,必须首先在 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:oxm="http://www.springframework.org/schema/oxm" (1)
xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2)
1 引用 the oxm schema.
2 指定 the oxm schema 位置.

目前,此模式使以下元素可用:

每个标签都在其各自的编组部分中进行了解释. 但是,作为示例,JAXB2 的编组器的配置可能类似于以下内容:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

6.5. JAXB

JAXB 绑定编译器将 W3C XML Schema 转换为一个或多个 Java 类、jaxb.properties 文件以及可能的某些资源文件. JAXB 还提供了一种从带有注解的 Java 类生成模式的方法.

Spring 支持 JAXB 2.0 API 作为 XML 编组策略,遵循 MarshallerUnmarshaller 中描述的 MarshallerUnmarshaller 接口. 相应的集成类驻留在 org.springframework.oxm.jaxb 包中.

6.5.1. 使用 Jaxb2Marshaller

Jaxb2Marshaller 类实现了 Spring 的 MarshallerUnmarshaller 接口. 它需要一个上下文路径来运行. 您可以通过设置 contextPath 属性来设置上下文路径. 上下文路径是包含模式扩展类的冒号分隔的 Java 包名称列表. 它还提供了 classesToBeBound 属性,允许您设置编组器支持的类数组. 通过为 bean 指定一个或多个模式资源来执行模式验证,如以下示例所示:

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>org.springframework.oxm.jaxb.Flight</value>
                <value>org.springframework.oxm.jaxb.Flights</value>
            </list>
        </property>
        <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
    </bean>

    ...

</beans>
XML 配置命名空间

jaxb2-marshaller 元素配置 org.springframework.oxm.jaxb.Jaxb2Marshaller,如以下示例所示:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

或者,你可以使用 class-to-be-bound 子元素提供要绑定到编组器的类列表:

<oxm:jaxb2-marshaller id="marshaller">
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/>
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/>
    ...
</oxm:jaxb2-marshaller>

下表描述了可用的属性:

Attribute Description Required

id

The ID of the marshaller

No

contextPath

The JAXB Context path

No

6.6. JiBX

JiBX 框架提供了类似于 Hibernate 为 ORM 提供的解决方案: 绑定的定义定义了 Java 对象如何转换为 XML 或从 XML 转换的规则. 在准备绑定和编译类之后,JiBX 绑定编译器增强了类文件,并添加了代码来处理从 XML 转换类的实例或将类示例转换为 XML 文件.

有关 JiBX 的更多信息,请参见 JiBX网站. Spring 集成类驻留在 org.springframework.oxm.jibx 包中.

6.6.1. 使用 JibxMarshaller

JibxMarshaller 类同时实现了 MarshallerUnmarshaller 接口. 要进行操作,需要编组的班级名称,您可以使用 targetClass 属性设置. (可选) 您可以通过设置 bindingName 属性. 在下面的示例中,我们绑定 Flights 类:

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">org.springframework.oxm.jibx.Flights</property>
    </bean>
    ...
</beans>

JibxMarshaller 配置为单个类. 如果要编组多个类,则必须使用不同的 targetClass 属性值配置多个 JibxMarshaller 实例.

XML 配置命名空间

jibx-marshaller 标签配置 org.springframework.oxm.jibx.JibxMarshaller,如下例所示:

<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>

下表描述了可用的属性 :

Attribute Description Required

id

The ID of the marshaller

No

target-class

The target class for this marshaller

Yes

bindingName

The binding name used by this marshaller

No

6.7. XStream

XStream 是一个简单的库,用于将对象序列化为 XML 并再次返回. 它不需要任何映射并生成干净的 XML.

关于 XStream 的更多信息,请参阅 XStream网站. Spring 集成的类驻留在 org.springframework.oxm.xstream 包中.

6.7.1. 使用 XStreamMarshaller

XStreamMarshaller 不需要任何配置,可以直接在应用程序上下文中进行配置. 要进一步自定义 XML,您可以设置别名映射,该映射由映射到类的字符串别名组成,如以下示例所示:

<beans>
    <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
        <property name="aliases">
            <props>
                <prop key="Flight">org.springframework.oxm.xstream.Flight</prop>
            </props>
        </property>
    </bean>
    ...
</beans>

默认情况下,XStream 允许任意类被解组,这可能导致不安全的 Java 序列化效果. 因此,我们不建议使用 XStreamMarshaller 从外部源(即 Web) 解组 XML,因为这可能导致安全漏洞.

如果选择使用 XStreamMarshaller 从外部源解组XML, 请在 XStreamMarshaller 上设置 supportedClasses 属性,如以下示例所示:

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
    <property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/>
    ...
</bean>

这样做可确保只有已注册的类才有资格进行解组.

此外,您可以注册 自定义转换器, 以确保只能对您支持的类进行解组. 除了明确支持应支持的域类的转换器之外,您可能还希望将 CatchAllConverter 添加为列表中的最后一个转换器. 因此,不会调用具有较低优先级和可能的安全漏洞的默认 XStream 转换器.

请注意,XStream 是 XML 序列化库,而不是数据绑定库. 因此,它具有有限的命名空间支持. 因此,它非常不适合在 Web 服务中使用.

7. 附录

7.1. XML Schemas

附录的这一部分列出了用于数据访问的 XML schema,包括以下内容:

7.1.1. tx Schema

tx 标签处理 Spring 对事务的全面支持中所有这些 bean 的配置. 这些标签在标题为事务管理的章节中介绍. .

我们强烈建议您查看 Spring 发行版附带的 spring-tx.xsd 文件. 此文件包含 Spring 的事务配置的 XML schema,并涵盖 tx 命名空间中的所有各种元素,包括属性默认值和类似信息. 该文件以内联方式记录,因此,为了遵守 DRY(不要重复自己) 原则,此处不再重复这些信息.

为了完整性,要使用 tx schema 中的元素,您需要在 Spring XML 配置文件的顶部包含以下代码. 以下代码段中的文本引用了正确的 schema,以便您可以使用 tx 命名空间中的标记:

<?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" (1)
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd (2)
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->

</beans>
1 声明 tx 命名空间的使用.
2 指定位置(使用其他 schema 位置) .
通常,当您使用 tx 命名空间中的元素时,您还使用了 aop 命名空间中的元素(因为 Spring 中的声明式事务支持是通过使用 AOP 实现的) . 前面的 XML 片段包含引用 aop schema 所需的相关行,以便您可以使用 aop 命名空间中的元素.

7.1.2. jdbc Schema

jdbc 元素使您可以快速配置嵌入式数据库 或初始化现有数据源. 这些元素分别记录在嵌入式数据库支持初始化数据源中.

要使用 jdbc Schema 中的元素,需要在 Spring XML 配置文件的顶部包含以下代码. 以下代码段中的文本引用了正确的 Schema,以便您可以使用 jdbc 命名空间中的元素:

<?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:jdbc="http://www.springframework.org/schema/jdbc" (1)
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2)

    <!-- bean definitions here -->

</beans>
1 声明 jdbc 的命名空间.
2 指定位置(使用其他 Schema 位置) .