数据访问
1. 事务管理
全面的事务支持是使用 Spring 框架最具吸引力的原因之一。Spring 框架为事务管理提供了一致的抽象,从而带来以下优势:
以下部分描述了Spring框架的事务特性和技术:
-
Spring Framework 事务支持模型的优势 描述了为什么你会选择使用 Spring Framework 的事务抽象,而不是 EJB 容器管理事务(CMT),或者通过专有 API(例如 Hibernate)来驱动本地事务。
-
理解 Spring Framework 的事务抽象 概述了核心类,并描述了如何从各种来源配置和获取
DataSource实例。 -
与事务同步资源 描述了应用程序代码如何确保资源被正确地创建、重用和清理。
-
声明式事务管理 描述了对声明式事务管理的支持。
-
编程管理事务涵盖了程序化(即显式编码的)事务管理支持。
-
交易绑定事件 描述了您如何在事务中使用应用程序事件。
1.1. Spring 框架事务支持模型的优势
传统上,Java EE 开发人员在事务管理方面只有两种选择:全局事务或本地事务,这两种方式都存在明显的局限性。接下来的两节将分别回顾全局事务和本地事务管理,随后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。
1.1.1. 全局事务
全局事务允许您操作多个事务性资源,通常包括关系型数据库和消息队列。应用服务器通过 JTA(Java Transaction API)来管理全局事务,而 JTA 是一个繁琐的 API(部分原因在于其异常模型)。此外,JTA 的 UserTransaction 通常需要从 JNDI 获取,这意味着您必须使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的潜在重用性,因为 JTA 通常仅在应用服务器环境中可用。
过去,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是一种声明式事务管理形式(区别于编程式事务管理)。EJB CMT 消除了与事务相关的 JNDI 查找需求,尽管使用 EJB 本身仍需要使用 JNDI。它消除了大部分(但并非全部)编写 Java 代码来控制事务的需求。然而,其显著缺点在于 CMT 与 JTA 和应用服务器环境紧密绑定。此外,只有在选择使用 EJB 实现业务逻辑(或至少通过一个事务性的 EJB 外观层)时,CMT 才可用。鉴于 EJB 整体上存在诸多弊端,这一方案并不具吸引力,尤其是在当前已有极具竞争力的声明式事务管理替代方案的情况下。
1.1.2. 本地事务
本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更易于使用,但有一个明显的缺点:它们无法跨多个事务性资源工作。例如,使用 JDBC 连接管理事务的代码无法在全局 JTA 事务中运行。由于应用服务器不参与事务管理,因此无法帮助确保跨多个资源的一致性。(值得注意的是,大多数应用程序仅使用单一事务资源。)另一个缺点是,本地事务会对编程模型造成侵入性影响。
1.1.3. Spring Framework 的一致编程模型
Spring 解决了全局事务和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。 你只需编写一次代码,即可在不同环境中受益于不同的事务管理策略。Spring 框架同时提供了声明式和编程式事务管理。 大多数用户更倾向于声明式事务管理,我们在大多数情况下也推荐使用这种方式。
使用编程式事务管理时,开发人员直接使用 Spring Framework 的事务抽象层,该抽象层可以在任何底层事务基础设施之上运行。 而在更推荐的声明式模型中,开发人员通常只需编写很少甚至无需编写与事务管理相关的代码,因此不依赖于 Spring Framework 的事务 API 或任何其他事务 API。
1.2. 理解 Spring 框架的事务抽象
Spring 事务抽象的关键在于事务策略(transaction strategy)的概念。事务策略由 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;
}
interface PlatformTransactionManager : TransactionManager {
@Throws(TransactionException::class)
fun getTransaction(definition: TransactionDefinition): TransactionStatus
@Throws(TransactionException::class)
fun commit(status: TransactionStatus)
@Throws(TransactionException::class)
fun rollback(status: TransactionStatus)
}
这主要是一个服务提供者接口(SPI),尽管你也可以在应用程序代码中以编程方式使用它。由于PlatformTransactionManager是一个接口,因此可以根据需要轻松地对其进行模拟(mock)或桩(stub)处理。它并不绑定到某种查找策略(例如 JNDI)。PlatformTransactionManager的实现如同 Spring 框架 IoC 容器中的任何其他对象(或 bean)一样进行定义。仅此一项优势就使得 Spring 框架的事务抽象非常有价值,即使你在使用 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;
}
interface ReactiveTransactionManager : TransactionManager {
@Throws(TransactionException::class)
fun getReactiveTransaction(definition: TransactionDefinition): Mono<ReactiveTransaction>
@Throws(TransactionException::class)
fun commit(status: ReactiveTransaction): Mono<Void>
@Throws(TransactionException::class)
fun rollback(status: ReactiveTransaction): Mono<Void>
}
响应式事务管理器主要是一种服务提供者接口(SPI),
尽管您也可以从应用程序代码中以编程方式使用它。
由于ReactiveTransactionManager是一个接口,因此可以根据需要轻松地对其进行模拟(mock)或桩(stub)。
The TransactionDefinition 接口规定了:
-
传播行为(Propagation):通常,事务范围内的所有代码都在该事务中运行。然而,当一个带有事务的方法在已有事务上下文存在的情况下被调用时,你可以指定其行为。例如,代码可以继续在现有事务中运行(这是常见情况),也可以挂起现有事务并创建一个新事务。Spring 提供了所有 EJB CMT 中常见的事务传播选项。有关 Spring 中事务传播语义的详细说明,请参阅事务传播。
-
隔离:此事务与其他事务工作的隔离程度。例如,此事务是否能够看到其他事务的未提交写入?
-
超时:此事务在自动回滚之前运行的最长时间,由底层事务基础设施进行控制。
-
读只状态:当您的代码仅读取而不修改数据时,可以使用只读事务。在某些情况下,例如使用Hibernate时,只读事务可以是一种有用的优化手段。
这些设置体现了标准的事务概念。如有必要,请参考讨论事务隔离级别及其他核心事务概念的相关资料。 理解这些概念对于使用 Spring 框架或任何事务管理解决方案都至关重要。
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();
}
interface TransactionStatus : TransactionExecution, SavepointManager, Flushable {
override fun isNewTransaction(): Boolean
fun hasSavepoint(): Boolean
override fun setRollbackOnly()
override fun isRollbackOnly(): Boolean
fun flush()
override fun isCompleted(): Boolean
}
无论你在 Spring 中选择声明式还是编程式事务管理,定义正确的 TransactionManager 实现都至关重要。
你通常通过依赖注入来定义该实现。
TransactionManager 的实现通常需要了解其运行环境:JDBC、JTA、Hibernate 等。以下示例展示了如何定义一个本地的 PlatformTransactionManager 实现(在本例中,使用的是纯 JDBC)。
您可以定义一个JDBC 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>
The related PlatformTransactionManager bean definition then has a reference to the
DataSource definition. 它应该类似于以下示例:
<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>
The JtaTransactionManager 不需要知道关于 DataSource(或其他任何特定资源)的信息,因为它使用了容器的全局事务管理基础设施。
前面的 dataSource bean 定义使用了 <jndi-lookup/> 标签,该标签来自 jee 命名空间。更多信息请参见
JEE Schema(JEE 模式)。 |
| 如果你使用 JTA,无论你采用何种数据访问技术(无论是 JDBC、Hibernate JPA,还是任何其他受支持的技术),你的事务管理器定义都应保持一致。这是因为 JTA 事务是全局事务,可以纳入任何事务性资源。 |
在所有 Spring 事务配置中,应用程序代码无需更改。您只需修改配置即可改变事务的管理方式,即使这种更改意味着从本地事务切换到全局事务,或反之亦然。
1.2.1. Hibernate 事务设置
您也可以轻松使用 Hibernate 本地事务,如下例所示。
在这种情况下,您需要定义一个 Hibernate LocalSessionFactoryBean,您的应用程序代码可以使用它来获取 Hibernate Session 实例。
The DataSource bean定义类似于前面示例中所示的本地JDBC示例,
因此在以下示例中不予展示。
如果 DataSource(由任何非JTA事务管理器使用)是通过 JNDI 查找并在 Java EE 容器中管理的,那么它应该是非事务性的,因为 Spring 框架(而不是 Java EE 容器)管理事务。 |
在此情况下,txManager bean 的类型为 HibernateTransactionManager。与 DataSourceTransactionManager 需要一个对 DataSource 的引用类似,HibernateTransactionManager 也需要一个对 SessionFactory 的引用。以下示例声明了 sessionFactory 和 txManager 这两个 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 的事务协调器(transaction coordinator)以及可能的连接释放模式(connection release mode)配置,使其感知到 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 关联,等等),现在应该已经清楚了。本节将介绍应用程序代码(直接或间接地通过使用持久化 API,如 JDBC、Hibernate 或 JPA)如何确保这些资源被正确地创建、复用和清理。本节还将讨论如何通过相应的 TransactionManager(可选地)触发事务同步。
1.3.1. 高级同步方法
首选的方法是使用 Spring 基于模板的最高层级持久化集成 API,或者在使用原生 ORM API 时,配合支持事务感知的工厂 Bean 或代理来管理原生资源工厂。这些支持事务感知的解决方案在内部处理资源的创建与重用、清理、资源的可选事务同步以及异常映射。因此,用户的数据访问代码无需处理这些任务,而可以专注于纯粹的非样板式持久化逻辑。通常情况下,对于 JDBC 访问,您可以使用原生 ORM API,也可以采用基于模板的方式,例如使用 JdbcTemplate。这些解决方案将在本参考文档的后续章节中详细说明。
1.3.2. 低级同步方法
诸如 DataSourceUtils(用于 JDBC)、EntityManagerFactoryUtils(用于 JPA)、
SessionFactoryUtils(用于 Hibernate)等类存在于较低的层次。当你希望应用程序代码直接处理原生持久化 API 的资源类型时,
可以使用这些类来确保获取到由 Spring 框架正确管理的实例,
(可选地)同步事务,并将过程中发生的异常正确地映射到一致的 API。
例如,在 JDBC 的情况下,您可以使用 Spring 的 getConnection() 类,而不是传统的 JDBC 方法直接调用 DataSource 上的 org.springframework.jdbc.datasource.DataSourceUtils 方法,如下所示:
Connection conn = DataSourceUtils.getConnection(dataSource);
如果现有事务已经有一个连接与其同步(关联),则返回该连接实例。否则,该方法调用会触发创建一个新的连接,该连接(可选地)与任何现有事务同步,并可在同一事务中被后续重复使用。如前所述,任何 SQLException 都会被封装为 Spring 框架的 CannotGetJdbcConnectionException 异常,这是 Spring 框架中非受检 DataAccessException 异常体系的一部分。这种方法比直接从 SQLException 中获取的信息更加丰富,并确保了在不同数据库之间、甚至不同持久化技术之间的可移植性。
此方法在不使用 Spring 事务管理的情况下也可以工作(事务同步是可选的),因此无论您是否使用 Spring 进行事务管理,都可以使用它。
当然,一旦你使用了 Spring 的 JDBC 支持、JPA 支持或 Hibernate 支持,通常就不再愿意直接使用 DataSourceUtils 或其他辅助类,因为你更倾向于通过 Spring 提供的抽象层进行开发,而不是直接使用底层的相关 API。例如,如果你使用 Spring 的 JdbcTemplate 或 jdbc.object 包来简化 JDBC 的使用,那么正确的连接获取会在幕后自动完成,你无需编写任何特殊代码。
1.3.3. TransactionAwareDataSourceProxy
在最底层存在 TransactionAwareDataSourceProxy 类。这是一个目标 DataSource 的代理,它包装了目标 DataSource,以增加对 Spring 管理的事务的感知能力。在这方面,它类似于 Java EE 服务器所提供的事务型 JNDI DataSource。
你几乎永远都不需要或不应该使用这个类,除非必须调用现有代码,并向其传递一个标准 JDBC DataSource 接口的实现。在这种情况下,该代码可能是可用的,但同时又参与了 Spring 管理的事务。你可以通过使用前面提到的更高层次的抽象来编写你的新代码。
1.4. 声明式事务管理
| 大多数 Spring Framework 用户选择声明式事务管理。该选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理念。 |
Spring 框架的声明式事务管理是通过 Spring 面向切面编程(AOP)实现的。然而,由于事务相关的切面代码已包含在 Spring 框架发行版中,并且可以以样板化的方式使用,因此通常无需深入理解 AOP 概念即可有效地使用这些代码。
Spring 框架的声明式事务管理与 EJB CMT 类似,因为你可以将事务行为(或无事务行为)指定到单个方法级别。
如有必要,你可以在事务上下文中调用 setRollbackOnly() 方法。这两种事务管理方式之间的区别在于:
-
与绑定到 JTA 的 EJB CMT 不同,Spring 框架的声明式事务管理可在任何环境中工作。通过调整配置文件,它可以配合 JTA 事务使用,也可以配合 JDBC、JPA 或 Hibernate 的本地事务使用。
-
可以将Spring框架声明式事务管理应用于任何类, 而不仅仅是特殊类如EJB。
-
Spring 框架提供了声明式的回滚规则,这是 EJB 中所没有的功能。框架同时提供了对回滚规则的编程式和声明式支持。
-
Spring 框架允许你通过使用 AOP 来自定义事务行为。 例如,你可以在事务回滚时插入自定义行为。 你还可以在事务通知之外添加任意的增强逻辑。而使用 EJB CMT 时, 除了调用
setRollbackOnly()之外,你无法干预容器的事务管理。 -
Spring 框架不像高端应用服务器那样支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用 EJB。然而,在使用此类功能之前请仔细考虑,因为通常情况下,我们并不希望事务跨越远程调用。
回滚规则的概念非常重要。它们允许你指定哪些异常(以及可抛出对象)应触发自动回滚。你可以通过声明式方式在配置中指定这些规则,而无需编写 Java 代码。因此,尽管你仍然可以在 setRollbackOnly() 对象上调用 TransactionStatus 来回滚当前事务,但更常见的情况是,你可以指定一条规则:当抛出 MyApplicationException 时必须始终触发回滚。这种方式的一个显著优势在于,业务对象不再依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。
尽管 EJB 容器的默认行为会在系统异常(通常是运行时异常)发生时自动回滚事务,但 EJB 容器管理事务(CMT)在遇到应用异常(即除 java.rmi.RemoteException 之外的受检异常)时不会自动回滚事务。虽然 Spring 声明式事务管理的默认行为遵循 EJB 的约定(仅在未受检异常时自动回滚),但通常自定义此行为会很有用。
1.4.1. 理解 Spring 框架的声明式事务实现
仅仅告诉您在类上添加 @Transactional 注解,并在配置中加入 @EnableTransactionManagement,然后就期望您理解其全部工作原理,这是远远不够的。为了提供更深入的理解,本节将在事务相关问题的上下文中,解释 Spring 框架声明式事务基础设施的内部工作机制。
关于 Spring 框架的声明式事务支持,最重要的概念在于:该支持是通过
AOP 代理启用的,并且事务性通知由元数据(目前基于 XML 或注解)驱动。AOP 与事务元数据相结合,生成一个 AOP 代理,该代理在方法调用周围使用 TransactionInterceptor 并结合适当的 TransactionManager 实现来驱动事务。
| Spring AOP 是在 AOP 部分 介绍的。 |
Spring Framework 的 TransactionInterceptor 为命令式和响应式编程模型提供事务管理。该拦截器通过检查方法的返回类型来判断所需的事务管理风格。返回响应式类型(例如 Publisher 或 Kotlin Flow,或它们的子类型)的方法适用于响应式事务管理。所有其他返回类型(包括 void)则使用命令式事务管理的代码路径。
事务管理风格会影响所需事务管理器的类型。命令式事务需要使用 PlatformTransactionManager,而响应式事务则使用 ReactiveTransactionManager 的实现。
|
由 |
以下图片展示了调用事务代理对象的方法的概念视图:
1.4.2. 声明式事务实现示例
请考虑以下接口及其对应的实现。本示例使用 Foo 和 Bar 类作为占位符,以便您能专注于事务的使用方式,而不必关注特定的领域模型。在本例中,DefaultFooService 类在每个已实现方法的主体中抛出 UnsupportedOperationException 实例,这种做法是合理的。该行为能让您观察到事务被创建后,又因 UnsupportedOperationException 实例而回滚的过程。以下代码清单展示了 FooService 接口:
// the service interface that we want to make transactional
package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}
// the service interface that we want to make transactional
package x.y.service
interface FooService {
fun getFoo(fooName: String): Foo
fun getFoo(fooName: String, barName: String): Foo
fun insertFoo(foo: Foo)
fun updateFoo(foo: Foo)
}
以下示例展示了上述接口的实现:
package x.y.service;
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) {
// ...
}
}
package x.y.service
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) 必须在只读语义的事务上下文中运行,而其他方法 insertFoo(Foo) 和 updateFoo(Foo) 则必须在读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明:
<!-- 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 开头的方法都应在只读事务上下文中运行,而所有其他方法则使用默认的事务语义”。transaction-manager 标签的 <tx:advice/> 属性被设置为用于驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。
如果要注入的 transaction-manager bean 的名称为 <tx:advice/>,则可以在事务通知(TransactionManager)中省略 transactionManager 属性。如果要注入的 TransactionManager bean 使用了其他名称,则必须像前面示例中那样显式使用 transaction-manager 属性。 |
<aop:config/> 的定义确保了由 txAdvice bean 所定义的事务通知在程序中的适当位置执行。首先,您定义一个切入点(pointcut),该切入点匹配 FooService 接口中定义的任意操作的执行(fooServiceOperation)。然后,您通过一个通知器(advisor)将该切入点与 txAdvice 关联起来。其结果表明,在执行 fooServiceOperation 时,会运行由 txAdvice 定义的通知。
在 <aop:pointcut/> 元素内定义的表达式是一个 AspectJ 切点表达式。
有关 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 定义创建的对象生成一个事务代理。该代理配置了事务通知(advice),因此当在代理上调用适当的方法时,会根据与该方法关联的事务配置来启动、挂起、标记为只读等事务操作。请考虑以下程序,它用于测试前面所示的配置:
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());
}
}
import org.springframework.beans.factory.getBean
fun main() {
val ctx = ClassPathXmlApplicationContext("context.xml")
val fooService = ctx.getBean<FooService>("fooService")
fooService.insertFoo(Foo())
}
运行 preceding 程序的输出应类似于以下内容(为了清晰,Log4J 输出和由 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 来确定方法返回类型是否是响应式的。 |
以下列表显示了之前使用的
// the reactive service interface that we want to make transactional
package x.y.service;
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);
}
// the reactive service interface that we want to make transactional
package x.y.service
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>
}
以下示例展示了上述接口的实现:
package x.y.service;
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) {
// ...
}
}
package x.y.service
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 会使用一个事务操作符对返回的响应式类型进行包装,以开启和清理事务。因此,调用一个带事务的响应式方法会将实际的事务管理推迟到订阅(subscription)阶段,此时才会激活对该响应式类型的处理。
另一方面的响应式事务管理涉及到数据逃逸,这是编程模型的自然结果。
方法实现的事务返回值会在事务性方法成功终止时从方法中返回,以确保部分计算的结果不逃逸出方法闭包。
响应式事务方法返回一个表示计算序列并承诺开始和完成该计算的响应式包装类型。
Publisher 可以在事务进行中(但不一定已完成)时发出数据。
因此,依赖于整个事务成功完成的方法需要在调用代码中确保事务完成并缓冲结果。
1.4.3. 回滚声明式事务
上一节概述了如何在应用程序中以声明式方式为类(通常是服务层类)指定事务设置的基础知识。本节将介绍如何以一种简单、声明式的方式来控制事务的回滚。
向 Spring 框架的事务基础设施表明事务工作需要回滚的推荐方式,是从当前在事务上下文中执行的代码中抛出一个 Exception。Spring 框架的事务基础设施代码会在异常沿调用栈向上冒泡时捕获任何未处理的 Exception,并据此判断是否将该事务标记为回滚。
在默认配置下,Spring 框架的事务基础设施代码仅在发生运行时(unchecked)异常时才会将事务标记为回滚。
也就是说,当抛出的异常是 RuntimeException 的实例或其子类时。(默认情况下,Error 实例也会导致回滚。)
在默认配置中,从事务性方法中抛出的受检(checked)异常不会导致事务回滚。
您可以精确配置哪些 Exception 类型会将事务标记为回滚,包括受检异常(checked exceptions)。以下 XML 片段演示了如何为一个特定于应用程序的受检 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>
如果你不希望在抛出异常时回滚事务,也可以指定“不回滚规则”。以下示例告诉 Spring 框架的事务基础设施,即使遇到未处理的 InstrumentNotFoundException,也要提交相应的事务:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
当 Spring 框架的事务基础设施捕获到一个异常时,它会查询配置的回滚规则以确定是否将事务标记为回滚,此时最匹配的规则优先。因此,在以下配置的情况下,除了 InstrumentNotFoundException 以外的任何异常都会导致相关事务回滚:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
你也可以以编程方式指示需要回滚。尽管这种方法很简单, 但它具有很强的侵入性,并且会使你的代码与 Spring 框架的事务基础设施紧密耦合。以下示例展示了如何以编程方式指示需要回滚:
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
fun resolvePosition() {
try {
// some business logic...
} catch (ex: NoProductInStockException) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
如果可能的话,强烈建议您使用声明式方式来实现回滚。只有在绝对必要时才应使用编程式回滚,因为它的使用违背了实现干净的基于 POJO 架构的目标。
1.4.4. 为不同的 Bean 配置不同的事务语义
考虑这样一种场景:你有一组服务层对象,并希望为每个对象应用完全不同的事务配置。你可以通过定义多个不同的 <aop:advisor/> 元素,并为它们分别设置不同的 pointcut 和 advice-ref 属性值来实现这一点。
作为对比,首先假设你所有的服务层类都定义在一个根包 x.y.service 中。要让该包(或其子包)中定义的所有类的实例、且 bean 名称以 Service 结尾的 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="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. -
该事务是读写事务。
-
交易超时默认为底层事务系统的默认超时时间,或者如果不受支持则为无超时。
-
任何
RuntimeException都会触发回滚,而任何已检查的Exception不会。
您可以更改这些默认设置。下表总结了嵌套在 <tx:method/> 和 <tx:advice/> 标签内的 <tx:attributes/> 标签的各种属性:
| 属性 | 必填? | 默认 | <description> </description> |
|---|---|---|---|
|
是的 |
要与事务属性关联的方法名称。可以使用通配符(*)将相同的事务属性设置应用于多个方法(例如, |
|
|
No |
|
事务传播行为。 |
|
No |
|
事务隔离级别。仅适用于 |
|
No |
-1 |
事务超时(秒)。仅适用于传播行为 |
|
No |
false |
读写事务与只读事务。仅适用于 |
|
No |
用逗号分隔的异常列表 |
|
|
No |
不触发回滚的 |
1.4.6. 使用@Transactional
除了基于 XML 的声明式事务配置方法外,您还可以使用基于注解的方法。直接在 Java 源代码中声明事务语义,可以使声明更贴近受影响的代码。这种方式几乎不会造成不必要的耦合,因为那些旨在以事务方式使用的代码,无论如何几乎总是以这种方式部署的。
标准的javax.transaction.Transactional注解也被支持,可以用作Spring自身注解的即插即用替代品。更多详情请参阅JTA 1.2文档。 |
通过使用 @Transactional 注解所带来的易用性,最好通过以下示例来说明,该示例将在随后的文本中进行解释。
请考虑以下类定义:
// 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) {
// ...
}
}
// 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 时,你可以在 @EnableTransactionManagement 类中使用 @Configuration 注解,使该 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实例具有事务性的那行代码。 |
如果要注入的 transaction-manager bean 的名称为 <tx:annotation-driven/>,则可以在 TransactionManager 标签中省略 transactionManager 属性。如果要依赖注入的 TransactionManager bean 具有其他名称,则必须使用 transaction-manager 属性,如前面的示例所示。 |
Reactive事务方法使用响应式返回类型,与命令式编程安排不同,如下例所示:
// 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) {
// ...
}
}
// 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,在响应式流(Reactive Streams)的取消信号方面有特殊注意事项。更多详情请参见“使用TransactionOperator”一节中的取消信号部分。
|
方法可见性与
@Transactional当你在 Spring 的标准配置中使用事务代理时,应仅将 在
Spring TestContext 框架默认支持非私有的 |
您可以将 @Transactional 注解应用于接口定义、接口中的方法、类定义或类中的方法。然而,仅在代码中添加 @Transactional 注解还不足以激活事务行为。@Transactional 注解只是一种元数据,可被某些支持 @Transactional 的运行时基础设施读取,并利用该元数据为相应的 Bean 配置事务行为。在前面的示例中,<tx:annotation-driven/> 元素用于启用事务行为。
Spring 团队建议您仅对具体类(以及具体类中的方法)使用 @Transactional 注解,而不是对接口进行注解。
当然,您也可以将 @Transactional 注解添加到接口(或接口方法)上,但这只有在您使用基于接口的代理时才能按预期工作。
由于 Java 注解不会从接口继承,因此如果您使用基于类的代理(proxy-target-class="true")或基于织入的切面(mode="aspectj"),
代理和织入基础设施将无法识别事务设置,对象也就不会被包装在事务代理中。 |
在代理模式下(这是默认模式),只有通过代理传入的外部方法调用才会被拦截。这意味着自我调用(实际上是指目标对象内部的一个方法调用该目标对象的另一个方法)即使被调用的方法标记了 @Transactional,在运行时也不会真正开启事务。此外,代理必须完全初始化后才能提供预期的行为,因此你不应在初始化代码中依赖此特性——例如,在 @PostConstruct 方法中。 |
如果你希望自调用(self-invocations)也被事务包装,可以考虑使用 AspectJ 模式(参见下表中的 mode 属性)。在这种情况下,根本不会创建代理。取而代之的是,目标类会被织入(即其字节码被修改),以支持在任意类型的方法上实现 @Transactional 的运行时行为。
| XML 属性 | 注解属性 | 默认 | <description> </description> |
|---|---|---|---|
|
不适用(请参阅 |
|
使用事务管理器的名称。仅在事务管理器的名称不是 |
|
|
|
默认模式( |
|
|
|
仅适用于 |
|
|
|
定义应用于使用 |
处理 @Transactional 注解的默认通知模式是 proxy,
该模式仅允许通过代理拦截方法调用。同一类内部的本地方法调用无法通过这种方式被拦截。
如需更高级的拦截模式,请考虑切换到 aspectj 模式,并结合编译时或加载时织入(weaving)使用。 |
proxy-target-class 属性控制为使用 @Transactional 注解标注的类创建何种类型的事务代理。如果将 proxy-target-class 设置为 true,则会创建基于类的代理。如果 proxy-target-class 为 false 或省略该属性,则会创建标准的基于 JDK 接口的代理。(有关不同代理类型的讨论,请参见代理机制。) |
@EnableTransactionManagement 和 <tx:annotation-driven/> 仅在定义它们的同一应用上下文中的 bean 上查找
@Transactional 注解。
这意味着,如果你将基于注解的配置放在 WebApplicationContext 的 DispatcherServlet 中,
它只会检查控制器(controller)中的 @Transactional bean,而不会检查服务(service)中的 bean。
更多信息请参见MVC。 |
在评估某个方法的事务设置时,最具体的(即最派生的)位置具有优先权。在以下示例中,DefaultFooService 类在类级别上使用了只读事务的设置进行注解,但该类中 @Transactional 方法上的 updateFoo(Foo) 注解会覆盖类级别定义的事务设置。
@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) {
// ...
}
}
@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 设置为
PROPAGATION_REQUIRED. -
隔离级别是
ISOLATION_DEFAULT. -
该事务是读写事务。
-
The transaction timeout defaults to the default timeout of the underlying transaction system, or to none if timeouts are not supported.
-
任何
RuntimeException都会触发回滚,而任何已检查的Exception不会。
您可以更改这些默认设置。以下表格概述了@Transactional 注解的各种属性:
| <property> </property> | 类型 | <description> </description> |
|---|---|---|
|
指定要使用的事务管理器的可选注解。 |
|
|
可选传播设置。 |
|
|
|
可选的隔离级别。仅当传播值为 |
|
|
可选的事务超时。仅适用于传播值为 |
|
|
读写与只读事务。仅适用于 |
|
数组形式的 |
必须触发回滚的异常类的可选数组。 |
|
类名数组。这些类必须继承自 |
必须触发回滚的异常类名称的可选数组。 |
|
数组形式的 |
不会导致回滚的异常类的可选数组。 |
|
|
可选的异常类名称数组,这些异常不得触发回滚。 |
目前,您无法显式控制事务的名称。这里的“名称”指的是在事务监视器(如果适用的话,例如 WebLogic 的事务监视器)以及日志输出中显示的事务名称。对于声明式事务,事务名称始终是被事务增强类的全限定类名 + . + 方法名。例如,如果 handlePayment(..) 类的 BusinessService 方法启动了一个事务,则该事务的名称将为:com.example.BusinessService.handlePayment。
多个事务管理器,适用于@Transactional
大多数 Spring 应用程序只需要一个事务管理器,但在某些情况下,你可能希望在单个应用程序中使用多个独立的事务管理器。你可以选择性地使用 value 注解的 transactionManager 或 @Transactional 属性来指定要使用的 TransactionManager 的标识。该标识可以是事务管理器 bean 的名称,也可以是其限定符(qualifier)值。例如,通过使用限定符注解,你可以将以下 Java 代码与应用程序上下文中声明的以下事务管理器 bean 结合使用:
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}
class TransactionalService {
@Transactional("order")
fun setSomething(name: String) {
// ...
}
@Transactional("account")
fun doSomething() {
// ...
}
@Transactional("reactive-account")
fun doSomethingReactive(): Mono<Void> {
// ...
}
}
<p>以下列表显示了 Bean 声明:</p>
<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 上的各个方法将分别在不同的事务管理器下运行,这些事务管理器通过 order、account 和 reactive-account 限定符加以区分。如果未找到带有特定限定符的 <tx:annotation-driven> bean,则仍会使用默认的 transactionManager 目标 bean 名称 TransactionManager。
自定义组合注解
如果你发现自己在许多不同的方法上反复使用相同的属性与 @Transactional 注解,Spring 的元注解支持 允许你为特定的使用场景定义自定义的组合注解。例如,请考虑以下注解定义:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional("order")
annotation class OrderTx
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Transactional("account")
annotation class AccountTx
前面的注解让我们可以将上一节的例子编写如下:
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}
class TransactionalService {
@OrderTx
fun setSomething(name: String) {
// ...
}
@AccountTx
fun doSomething() {
// ...
}
}
在前面的示例中,我们使用了该语法来定义事务管理器限定符,但也可以包含传播行为、回滚规则、超时设置以及其他特性。
1.4.7. 事务传播机制
本节描述了 Spring 中事务传播的一些语义。请注意,本节并非事务传播本身的入门介绍,而是详细说明了 Spring 中有关事务传播的一些语义。
在Spring管理的事务中,请注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种差异。
理解PROPAGATION_REQUIRED
PROPAGATION_REQUIRED 强制使用一个物理事务:如果当前作用域尚未存在事务,则在本地为当前作用域创建一个事务;如果已存在一个为更大作用域定义的“外部”事务,则加入该事务。在同一线程内的常见调用栈结构中(例如,一个服务门面委托给多个仓库方法,且所有底层资源都必须参与服务级别的事务),这是一个合适的默认行为。
默认情况下,参与的事务会加入外部作用域的特性,
静默忽略本地的隔离级别、超时值或只读标志(如果有的话)。
如果你希望在参与一个具有不同隔离级别的现有事务时拒绝隔离级别声明,
请考虑将事务管理器上的 validateExistingTransactions 标志设置为 true。
这种严格模式还会拒绝只读属性不匹配的情况(例如,一个内部的读写事务试图参与一个只读的外部作用域)。 |
当传播设置为 PROPAGATION_REQUIRED 时,会对应用该设置的每个方法创建一个逻辑事务作用域。每个这样的逻辑事务作用域可以独立地决定是否仅回滚(rollback-only),其中外层事务作用域在逻辑上独立于内层事务作用域。在标准的 PROPAGATION_REQUIRED 行为下,所有这些作用域都映射到同一个物理事务。因此,在内层事务作用域中设置的仅回滚标记会影响外层事务实际提交的可能性。
然而,当内部事务作用域设置了回滚标记(rollback-only marker)时,外部事务本身尚未决定要回滚,因此由内部事务作用域静默触发的回滚是出乎意料的。此时会抛出相应的UnexpectedRollbackException异常。这是预期的行为,目的是确保事务的调用方永远不会被误导,误以为事务已提交,而实际上并未提交。因此,如果一个内部事务(外部调用方并不知晓其存在)静默地将事务标记为仅回滚(rollback-only),而外部调用方仍然调用了提交(commit)操作,那么外部调用方必须收到一个UnexpectedRollbackException异常,以明确表明实际执行的是回滚操作而非提交。
理解PROPAGATION_REQUIRES_NEW
PROPAGATION_REQUIRES_NEW 与 PROPAGATION_REQUIRED 不同,它始终为每个受影响的事务作用域使用一个独立的物理事务,从不参与外部作用域的现有事务。在这种安排下,底层资源事务是不同的,因此可以独立地提交或回滚:外部事务不受内部事务回滚状态的影响,且内部事务的锁在其完成后会立即释放。
这种独立的内部事务还可以声明自己的隔离级别、超时时间和只读设置,而不会继承外部事务的特性。
理解PROPAGATION_NESTED
PROPAGATION_NESTED 使用单个物理事务并包含多个可回滚到的保存点。这种部分回滚允许内部事务范围触发其自身的回滚,而外部事务即使某些操作已被回滚,仍能继续执行物理事务。此设置通常映射到 JDBC 保存点,因此仅适用于基于 JDBC 资源的事务。请参阅 Spring 的 DataSourceTransactionManager。
1.4.8. 对事务性操作进行通知
假设您想运行事务性操作和一些基本的性能分析建议。
在<tx:annotation-driven/>的上下文中,如何实现这一点?
当您调用updateFoo(Foo)方法时,您希望看到以下操作:
-
已配置的诊断方面开始启动。
-
事务性通知运行。
-
目标对象的方法运行。
-
事务提交。
-
该切面报告整个事务方法调用的确切持续时间。
| 本章并不旨在详尽地解释AOP(事务相关的应用除外)。有关AOP配置和AOP的一般性详细内容,请参见AOP章节。 |
以下代码展示了前面讨论的简单性能监控切面:
package x.y;
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;
}
}
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
}
}
通知(advice)的排序
通过 Ordered 接口进行控制。有关通知排序的完整详情,请参阅
通知排序。
以下配置创建了一个fooService 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">
<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,其按顺序应用了性能分析(profiling)和事务(transactional)切面。如果你希望在方法进入时,性能分析通知在事务通知之后执行,而在方法退出时,性能分析通知在事务通知之前执行,则可以调整性能分析切面 bean 的 order 属性值,使其高于事务通知的 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 容器之外运行的应用程序,因此我们将向您展示如何通过编程方式实现这一点。
在继续之前,您可能希望阅读 使用 @Transactional 和
AOP。 |
以下示例展示了如何创建一个事务管理器,并配置 AnnotationTransactionAspect 使用它:
// 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);
// 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 开发指南),或者使用加载时织入(load-time weaving)。有关在 Spring 框架中使用 AspectJ 进行加载时织入的讨论,请参阅Spring 框架中的 AspectJ 加载时织入。
1.5. 编程式事务管理
Spring框架提供了两种程序化的事务管理方式,通过使用:
-
The
TransactionTemplate或TransactionalOperator。 -
直接实现一个
TransactionManager。
Spring 团队通常建议在命令式流程中使用 TransactionTemplate 进行编程式事务管理,在响应式代码中使用 TransactionalOperator。
第二种方法类似于使用 JTA 的 UserTransaction API,尽管其异常处理不那么繁琐。
1.5.1. 使用TransactionTemplate
TransactionTemplate 采用了与其他 Spring 模板(例如 JdbcTemplate)相同的方式。它使用回调方法(以避免应用程序代码重复编写获取和释放事务资源的样板代码),从而使代码更具意图导向性,即您的代码只需专注于要完成的任务。
正如以下示例所示,使用 TransactionTemplate 会将您完全绑定到 Spring 的事务基础设施和 API。编程式事务管理是否适合您的开发需求,需要由您自己做出决定。 |
必须在事务上下文中运行并显式使用 TransactionTemplate 的应用程序代码类似于下面的示例。作为应用程序开发人员,您可以编写一个 TransactionCallback 实现(通常以匿名内部类的形式表示),其中包含需要在事务上下文中执行的代码。然后,您可以将自定义的 TransactionCallback 实例传递给 execute(..) 所暴露的 TransactionTemplate 方法。以下示例展示了如何实现这一点:
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();
}
});
}
}
// 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类,并结合匿名类使用,例如如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
updateOperation1()
updateOperation2()
}
})
代码在回调中可以通过调用提供的setRollbackOnly()对象的TransactionStatus方法来回滚事务,如下所示:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
override fun doInTransactionWithoutResult(status: TransactionStatus) {
try {
updateOperation1()
updateOperation2()
} catch (ex: SomeBusinessException) {
status.setRollbackOnly()
}
}
})
指定事务设置
您可以通过编程方式或配置方式在 TransactionTemplate 上指定事务设置(例如传播模式、隔离级别、超时时间等)。默认情况下,TransactionTemplate 实例具有默认的事务设置。以下示例展示了如何以编程方式为特定的 TransactionTemplate: 自定义事务设置:
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...
}
}
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. 使用TransactionOperator
TransactionOperator 采用了一种类似于其他响应式操作符的操作符设计。它使用回调方式(使应用程序代码无需编写样板式的事务资源获取与释放逻辑),从而生成意图驱动的代码,即您的代码只需专注于要执行的操作。
正如以下示例所示,使用 TransactionOperator 会将您完全绑定到 Spring 的事务基础设施和 API。编程式事务管理是否适合您的开发需求,需要由您自己做出决定。 |
必须在事务上下文中运行并且显式使用TransactionOperator的应用代码类似于以下示例:
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);
}
}
// 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>))
在回调中的代码可以通过调用提供的setRollbackOnly()对象上的ReactiveTransaction方法来回滚事务,如下所示:
transactionalOperator.execute(new TransactionCallback<>() {
public Mono<Object> doInTransaction(ReactiveTransaction status) {
return updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
}
}
});
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.2 版本中,TransactionalOperator 默认行为是在取消时提交事务。而在 5.3 版本中,此行为将变更为:在取消时回滚事务,以产生可靠且确定的结果。因此,务必仔细考量位于事务 Publisher 下游所使用的操作符。特别是在处理 Flux 或其他多值 Publisher 的情况下,必须完整消费全部输出,才能使事务顺利完成。
指定事务设置
您可以为 TransactionalOperator 指定事务设置(例如传播模式、隔离级别、超时时间等)。默认情况下,TransactionalOperator 实例具有默认的事务设置。以下示例展示了如何为特定的 TransactionalOperator: 自定义事务设置:
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);
}
}
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
以下部分解释了命令式和响应式事务管理器的程序化使用方法。
使用PlatformTransactionManager
对于命令式事务,您可以直接使用 org.springframework.transaction.PlatformTransactionManager 来管理您的事务。为此,请通过 bean 引用将您所使用的 PlatformTransactionManager 实现传递给您的 bean。然后,通过使用 TransactionDefinition 和 TransactionStatus 对象,您可以启动事务、回滚和提交。以下示例展示了如何实现这一点:
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);
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。然后,通过使用 TransactionDefinition 和 ReactiveTransaction 对象,您可以启动事务、回滚和提交事务。以下示例展示了如何实现这一点:
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)));
});
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 可能是一个很好的方法。此外,显式设置事务名称的功能也只能通过编程式事务管理来实现。
另一方面,如果你的应用程序包含大量的事务操作, 声明式事务管理通常是值得采用的。它将事务管理从业务逻辑中分离出来,并且配置起来并不困难。当使用 Spring 框架而非 EJB CMT 时,声明式事务管理的配置成本大大降低。
1.7. 事务绑定事件
从 Spring 4.2 开始,事件的监听器可以绑定到事务的某个阶段。 典型的例子是在事务成功完成后处理事件。 这样,当当前事务的结果对监听器确实重要时,事件的使用将更加灵活。
你可以使用 @EventListener 注解注册一个普通的事件监听器。
如果你需要将其绑定到事务上,请使用 @TransactionalEventListener。
这样做时,默认情况下该监听器会绑定到事务的提交阶段。
下一个示例展示了这一概念。假设某个组件发布了一个“订单已创建”事件,而我们希望定义一个监听器,该监听器仅在发布该事件的事务成功提交后才处理此事件。以下示例设置了这样一个事件监听器:
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
// ...
}
}
@Component
class MyComponent {
@TransactionalEventListener
fun handleOrderCreatedEvent(creationEvent: CreationEvent<Order>) {
// ...
}
}
@TransactionalEventListener 注解提供了一个 phase 属性,允许你自定义监听器应绑定到的事务阶段。
有效的阶段包括 BEFORE_COMMIT、AFTER_COMMIT(默认值)、AFTER_ROLLBACK,
以及 AFTER_COMPLETION,后者表示事务完成(无论是提交还是回滚)的聚合阶段。
如果没有正在运行的事务,则根本不会调用该监听器,因为我们无法保证所需的语义。不过,您可以通过将注解的 fallbackExecution 属性设置为 true 来覆盖此行为。
|
|
1.8. 与应用服务器特定的集成
Spring 的事务抽象通常与应用服务器无关。此外,
Spring 的 JtaTransactionManager 类(可选择性地执行 JNDI 查找以获取
JTA 的 UserTransaction 和 TransactionManager 对象)会自动检测后者的位置,该位置因应用服务器而异。能够访问 JTA
TransactionManager 可以实现增强的事务语义——特别是支持事务挂起。请参阅
JtaTransactionManager
的 javadoc 以获取详细信息。
Spring 的 JtaTransactionManager 是在 Java EE 应用服务器上运行的标准选择,并且已知可在所有常见的服务器上正常工作。许多服务器(包括 GlassFish、JBoss 和 Geronimo)无需任何特殊配置即可支持高级功能,例如事务挂起(transaction suspension)。然而,为了完全支持事务挂起及其他更高级的集成,Spring 还为 WebLogic Server 和 WebSphere 提供了专用的适配器。这些适配器将在以下章节中进行讨论。
对于包括 WebLogic Server 和 WebSphere 在内的标准场景,请考虑使用便捷的 <tx:jta-transaction-manager/> 配置元素。配置该元素后,它会自动检测底层服务器,并为当前平台选择最合适的事务管理器。这意味着您无需显式配置特定于服务器的适配器类(如下文各节所述),而是由系统自动选择,默认情况下会回退到标准的 JtaTransactionManager。
1.9. 常见问题的解决方案
此部分描述了一些常见问题的解决方案。
1.9.1. 为特定场景使用了错误的事务管理器DataSource
根据您所选择的事务技术及需求,使用正确的 PlatformTransactionManager 实现。只要正确使用,Spring 框架仅提供一种简单且可移植的抽象。如果您使用全局事务,则必须对所有事务操作使用 org.springframework.transaction.jta.JtaTransactionManager 类(或其特定于应用服务器的子类)。否则,事务基础设施会尝试在诸如容器 DataSource 实例等资源上执行本地事务。这类本地事务毫无意义,而且一个好的应用服务器会将其视为错误。
1.10. 更多资源
有关 Spring 框架事务支持的更多信息,请参见:
-
Spring 中的分布式事务(含 XA 与不含 XA) 是 JavaWorld 上的一篇演讲,其中 Spring 的 David Syer 向您介绍了在 Spring 应用程序中处理分布式事务的七种模式,其中三种使用 XA,四种不使用 XA。
-
Java事务设计策略 是一本由 InfoQ 提供的书籍,它对 Java 中的事务进行了循序渐进的介绍。书中还包含了并列对比的示例,展示了如何在 Spring 框架和 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 框架支持中的各类模板类。如果你使用基于拦截器的类,应用程序必须自行处理 HibernateExceptions 和 PersistenceExceptions,最好分别委托给 convertHibernateAccessException(..) 类中的 convertJpaAccessException(..) 或 SessionFactoryUtils 方法。这些方法会将异常转换为与 org.springframework.dao 异常体系结构兼容的异常。由于 PersistenceExceptions 是非受检异常(unchecked),它们也可以直接抛出(尽管这样做会牺牲在异常层面的通用 DAO 抽象)。
下图展示了 Spring 提供的异常层次结构。
(请注意,图中所示的类层次结构仅展示了整个
DataAccessException 层次结构的一个子集。)
2.2. 用于配置 DAO 或 Repository 类的注解
确保你的数据访问对象(DAO)或仓库提供异常转换的最佳方式是使用 @Repository 注解。该注解还能让组件扫描功能自动发现并配置你的 DAO 和仓库,而无需为它们提供 XML 配置项。以下示例展示了如何使用 @Repository 注解:
@Repository (1)
public class SomeMovieFinder implements MovieFinder {
// ...
}
| 1 | @Repository注解。 |
@Repository (1)
class SomeMovieFinder : MovieFinder {
// ...
}
| 1 | @Repository注解。 |
任何 DAO 或仓库(repository)实现都需要访问持久化资源,具体取决于所使用的持久化技术。例如,基于 JDBC 的仓库需要访问 JDBC DataSource,而基于 JPA 的仓库则需要访问 EntityManager。实现这一点最简单的方式是使用 @Autowired、@Inject、@Resource 或 @PersistenceContext 注解之一来注入该资源依赖。以下示例适用于 JPA 仓库:
@Repository
public class JpaMovieFinder implements MovieFinder {
@PersistenceContext
private EntityManager entityManager;
// ...
}
@Repository
class JpaMovieFinder : MovieFinder {
@PersistenceContext
private lateinit var entityManager: EntityManager
// ...
}
如果您使用经典的Hibernate API,可以注入SessionFactory,如下例所示:
@Repository
public class HibernateMovieFinder implements MovieFinder {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
// ...
}
@Repository
class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder {
// ...
}
我们在这里展示的最后一个示例是典型的 JDBC 支持。您可以将 DataSource 注入到初始化方法或构造函数中,在其中使用该 JdbcTemplate 创建 SimpleJdbcCall 以及其他数据访问支持类(例如 DataSource 等)。以下示例自动装配了一个 DataSource:
@Repository
public class JdbcMovieFinder implements MovieFinder {
private JdbcTemplate jdbcTemplate;
@Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {
private val jdbcTemplate = JdbcTemplate(dataSource)
// ...
}
| 见每个持久化技术的具体覆盖范围,以了解如何配置应用上下文以便利用这些注解。 |
3. 使用 JDBC 进行数据访问
Spring Framework JDBC 抽象层所提供的价值,或许最好通过下表中列出的操作序列来体现。该表展示了哪些操作由 Spring 负责处理,哪些操作由您负责。
| 行动 | Spring | 你 |
|---|---|---|
定义连接参数。 |
X |
|
打开连接。 |
X |
|
指定SQL语句。 |
X |
|
声明参数并提供参数值 |
X |
|
准备并运行语句。 |
X |
|
设置循环以遍历结果(如果有)。 |
X |
|
进行每次迭代的工作。 |
X |
|
处理任何异常。 |
X |
|
处理事务。 |
X |
|
关闭连接、语句和结果集。 |
X |
The Spring Framework 处理了所有使 JDBC 成为如此繁琐 API 的低级细节。
3.1. 选择 JDBC 数据库访问方法
您可以从多种方法中选择一种作为 JDBC 数据库访问的基础。
除了三种形式的 JdbcTemplate 之外,新的 SimpleJdbcInsert 和
SimpleJdbcCall 方法利用数据库元数据进行优化,而 RDBMS 对象风格则采用
更面向对象的方式,类似于 JDO 查询的设计。一旦您开始使用其中某一种方法,
仍然可以混合搭配,以包含其他方法中的特性。所有这些方法都需要一个符合 JDBC 2.0 规范的驱动程序,
某些高级功能则需要 JDBC 3.0 驱动程序。
-
JdbcTemplate是经典且最受欢迎的 Spring JDBC 方法。这种 “最低级”的方法以及所有其他方法都 underlying 使用了 JdbcTemplate。 -
NamedParameterJdbcTemplate包装了一个JdbcTemplate,以提供命名参数, 而不是传统的 JDBC?占位符。当你在 SQL 语句中拥有多个参数时,这种方法提供了更好的可读性和易用性。 -
SimpleJdbcInsert和SimpleJdbcCall会利用数据库元数据来减少所需的配置量。这种方法简化了编码,你只需提供表名或存储过程名称,并提供一个参数映射,其键与列名相匹配即可。但此功能仅在数据库提供了充分的元数据时才能正常工作。如果数据库未提供此类元数据,则必须显式地配置参数。 -
关系型数据库管理系统(RDBMS)对象——包括
MappingSqlQuery、SqlUpdate和StoredProcedure—— 要求你在初始化数据访问层时创建可重用且线程安全的对象。这种方法借鉴了 JDO Query 的设计思路:你先定义查询字符串,声明参数,然后编译该查询。 一旦完成这些步骤,就可以多次调用execute(…)、update(…)和findObject(…)方法,并传入不同的参数值。
3.2. 包层次结构
Spring框架的JDBC抽象框架由四个不同的包组成:
-
core:org.springframework.jdbc.core包包含JdbcTemplate类及其各种回调接口,以及一系列相关类。名为org.springframework.jdbc.core.simple的子包包含SimpleJdbcInsert和SimpleJdbcCall类。另一个名为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 时,只需实现回调接口,这些接口具有明确定义的契约。给定由 Connection 类提供的 JdbcTemplate,PreparedStatementCreator 回调接口会创建一个预编译语句(prepared statement),并提供 SQL 语句及所有必要的参数。CallableStatementCreator 接口也是如此,它用于创建可调用语句(callable statements)。RowCallbackHandler 接口则从 ResultSet 的每一行中提取值。
您可以通过直接使用JdbcTemplate和DataSource引用进行实例化,在DAO实现中使用它,或者将其配置在一个Spring IoC容器中,并将它作为bean引用提供给DAO。
DataSource 应始终在 Spring IoC 容器中配置为一个 Bean。在第一种情况下,该 Bean 直接提供给服务;在第二种情况下,它被提供给预定义的模板。 |
此类发出的所有 SQL 都会以 DEBUG 级别记录在与模板实例的完全限定类名对应的类别下(通常为 JdbcTemplate,但如果您使用的是 JdbcTemplate 的自定义子类,则可能不同)。
以下各节提供了一些 JdbcTemplate 的使用示例。这些示例并未涵盖 JdbcTemplate 所提供的全部功能。
有关完整功能,请参阅相应的javadoc。
查询(SELECT)
以下查询获取关系中的行数:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
val rowCount = jdbcTemplate.queryForObject<Int>("select count(*) from t_actor")!!
以下查询使用了一个绑定变量:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
val countOfActorsNamedJoe = jdbcTemplate.queryForObject<Int>(
"select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!!
该查询查找一个 String:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
val lastName = this.jdbcTemplate.queryForObject<String>(
"select last_name from t_actor where id = ?",
arrayOf(1212L))!!
以下查询用于查找并填充单一领域对象:
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);
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"))
}
以下查询查找并填充一个领域对象列表:
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;
});
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 方法按需引用,这样会更加合理。
例如,最好将前面的代码片段改写如下:
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);
}
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)
}
正在更新(INSERT, UPDATE,和DELETE)与JdbcTemplate
您可以使用update(..)方法来执行插入、更新和删除操作。
参数值通常作为可变参数提供,或者作为对象数组的替代方式。
以下示例插入一个新条目:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling")
以下示例更新现有条目:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L)
以下示例删除一个条目:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong())
其他JdbcTemplate操作
您可以使用 execute(..) 方法来执行任意 SQL 语句。因此,该方法通常用于 DDL 语句。它提供了大量重载的变体,支持传入回调接口、绑定变量数组等参数。以下示例创建了一张表:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))")
以下示例调用了一个存储过程:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
unionId.toLong())
更复杂的存储过程支持将在后面介绍。
JdbcTemplate最佳实践
JdbcTemplate 类的实例在配置完成后是线程安全的。这一点非常重要,因为它意味着你可以配置一个 JdbcTemplate 的单例实例,然后安全地将这个共享引用注入到多个 DAO(或仓库)中。
JdbcTemplate 是有状态的,因为它持有一个对 DataSource 的引用,但这种状态并非会话状态(conversational state)。
使用 JdbcTemplate 类(以及相关的
NamedParameterJdbcTemplate 类)时的常见做法是,
在 Spring 配置文件中配置一个 DataSource,然后将该共享的 DataSource Bean 依赖注入到您的 DAO 类中。JdbcTemplate 是在 DataSource 的 setter 方法中创建的。这使得 DAO 类似于以下内容:
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...
}
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>
显式配置的一种替代方式是使用组件扫描(component-scanning)和注解支持进行依赖注入。在这种情况下,您可以使用 @Repository 注解该类(使其成为组件扫描的候选对象),并使用 DataSource 注解 @Autowired 的 setter 方法。以下示例展示了如何实现这一点:
@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 | 使用 DataSource 注解标注 @Autowired 的 setter 方法。 |
| 3 | 使用 JdbcTemplate 创建一个新的 DataSource。 |
@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 | DataSource 的构造函数注入。 |
| 3 | 使用 JdbcTemplate 创建一个新的 DataSource。 |
以下示例显示了相应的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 类都继承自该类,那么你的子类将从 setDataSource(..) 类继承一个 JdbcDaoSupport 方法。你可以自行选择是否继承此类。JdbcDaoSupport 类仅作为便利工具提供。
无论您选择使用上述哪种模板初始化方式(或不使用),通常都无需在每次执行 SQL 时都创建一个新的 JdbcTemplate 类实例。一旦配置完成,JdbcTemplate 实例就是线程安全的。如果您的应用程序需要访问多个数据库,则可能需要多个 JdbcTemplate 实例,这就要求配置多个 DataSources,进而需要多个不同配置的 JdbcTemplate 实例。
3.3.2. 使用NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 类增加了对使用命名参数编写 JDBC 语句的支持,而不是仅使用传统的占位符('?')参数来编写 JDBC 语句。NamedParameterJdbcTemplate 类包装了一个 JdbcTemplate,并将大部分工作委托给被包装的 JdbcTemplate 来完成。本节仅介绍 NamedParameterJdbcTemplate 类与 JdbcTemplate 本身不同的部分——即使用命名参数编写 JDBC 语句。以下示例展示了如何使用 NamedParameterJdbcTemplate:
// 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);
}
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)中的值。
或者,你可以使用基于 NamedParameterJdbcTemplate 的方式,将命名参数及其对应的值传递给 Map 实例。NamedParameterJdbcOperations 所公开的其余方法由 NamedParameterJdbcTemplate 类实现,遵循类似的模式,此处不再赘述。
以下示例展示了基于Map的用法:
// 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);
}
// 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 类)。SqlParameterSource 是为 NamedParameterJdbcTemplate 提供命名参数值的来源。MapSqlParameterSource 类是一个简单的实现,它包装了一个 java.util.Map,其中键是参数名称,值是参数值。
另一个 SqlParameterSource 的实现是 BeanPropertySqlParameterSource 类。该类包装一个任意的 JavaBean(即符合JavaBean 规范的类的实例),并使用被包装的 JavaBean 的属性作为命名参数值的来源。
以下示例展示了典型的JavaBean:
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...
}
data class Actor(val id: Long, val firstName: String, val lastName: String)
该示例使用NamedParameterJdbcTemplate来返回前面示例中所示类的成员数量:
// 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);
}
// 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 实例,以使用仅在 JdbcTemplate 类中提供的功能,可以通过 getJdbcOperations() 方法,经由 JdbcTemplate 接口来访问被包装的 JdbcOperations。
另请参阅 JdbcTemplate 最佳实践,以获取在应用程序上下文中使用 NamedParameterJdbcTemplate 类的指南。
3.3.3. 使用SQLExceptionTranslator
SQLExceptionTranslator 是一个接口,由能够将 SQLExceptions 转换为 Spring 自有的、与数据访问策略无关的 org.springframework.dao.DataAccessException 的类来实现。其实现可以是通用的(例如,使用 JDBC 的 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码),以获得更高的精确度。
SQLErrorCodeSQLExceptionTranslator 是默认使用的 SQLExceptionTranslator 实现。该实现使用特定的厂商代码,比 SQLState 实现更为精确。错误代码的翻译基于一个名为 SQLErrorCodes 的 JavaBean 类型类中持有的代码。此类由一个 SQLErrorCodesFactory 创建并填充,顾名思义,它是一个工厂,用于根据名为 sql-error-codes.xml 的配置文件内容创建 SQLErrorCodes。该文件填充了厂商代码,并基于从 DatabaseMetaData 获取的 DatabaseProductName。实际使用的是您当前所用数据库对应的代码。
The SQLErrorCodeSQLExceptionTranslator 应用匹配规则的顺序如下:
-
由子类实现的任何自定义转换逻辑。通常使用所提供的具体实现
SQLErrorCodeSQLExceptionTranslator,因此此规则不适用。仅当您实际提供了子类实现时,此规则才适用。 -
任何自定义实现
SQLExceptionTranslator接口的类,作为customSqlExceptionTranslator类的SQLErrorCodes属性提供。 -
的
CustomSQLErrorCodesTranslation类的实例列表(作为customTranslations属性提供给SQLErrorCodes类)会被搜索以找到匹配项。 -
错误代码匹配已应用。
-
使用备用翻译器。
SQLExceptionSubclassTranslator是默认的备用翻译器。如果此翻译不可用,则下一个备用翻译器是SQLStateSQLExceptionTranslator。
SQLErrorCodesFactory 默认用于定义 Error 错误代码和自定义异常转换规则。它会从类路径下名为 sql-error-codes.xml 的文件中查找配置,并根据当前所用数据库的元数据中的数据库名称,定位匹配的 SQLErrorCodes 实例。 |
您可以扩展SQLErrorCodeSQLExceptionTranslator,如下例所示:
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;
}
}
class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() {
override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? {
if (sqlEx.errorCode == -12345) {
return DeadlockLoserDataAccessException(task, sqlEx)
}
return null;
}
}
在前面的示例中,特定的错误代码(-12345)会被转换,而其他错误则交由默认的转换器实现进行处理。要使用此自定义转换器,您必须通过 JdbcTemplate 方法将其传递给 setExceptionTranslator,并且在所有需要该转换器的数据访问处理中都必须使用这个 JdbcTemplate。以下示例展示了如何使用此自定义转换器:
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);
}
// 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 提供的便捷方法。以下示例展示了你需要包含哪些内容,才能创建一个最小但功能完整的类来新建一张表:
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))");
}
}
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:
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);
}
}
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,该 3 包含对应行中每一列的条目,并以列名作为键。如果你在前面的示例中添加一个方法来获取所有行的列表,该方法可能如下所示:
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");
}
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. 更新数据库
以下示例更新某个主键对应的列:
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);
}
}
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 语句包含用于行参数的占位符。您可以将参数值 以可变参数(varargs)的形式传入,或者 以对象数组的形式传入。因此,您应当显式地将基本类型 包装在其对应的包装类中,或者使用自动装箱。
3.3.7. 检索自动生成的键
一个便捷的 update() 方法支持获取由数据库生成的主键。此功能属于 JDBC 3.0 标准的一部分,具体细节请参见规范第 13.6 章。该方法以一个 PreparedStatementCreator 作为第一个参数,通过这种方式指定所需的插入语句;另一个参数是 KeyHolder,在更新操作成功返回后,其中将包含生成的主键。由于没有一种标准的通用方式来创建合适的 PreparedStatement(这也解释了该方法签名如此设计的原因),以下示例适用于 Oracle,但在其他平台上可能无法正常工作:
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
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,它们提供 bean 风格的 DataSource 类;
对于现代的 JDBC 连接池,建议使用具有构建器风格 API 的 HikariCP。
您应该仅在测试目的时使用DriverManagerDataSource 和 SimpleDriverDataSource 类(这些类包含在 Spring 发行版中)!
那些变体不提供连接池功能,并且当需要多次请求连接时性能较差。 |
以下部分使用了Spring的DriverManagerDataSource实现。
其他DataSource变体将在后面进行介绍。
要配置一个DriverManagerDataSource:
-
使用
DriverManagerDataSource获得连接,就像您通常获取JDBC连接一样。 -
指定完整的类名(fully qualifiedclassname)的JDBC驱动,以便
DriverManager可以加载驱动类。 -
提供一个根据JDBC驱动程序不同而变化的URL。请参见您所使用的驱动程序的文档以获取正确的值。
-
提供一个用户名和密码以连接到数据库。
以下示例展示了如何在Java中配置一个DriverManagerDataSource:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
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 abstract 实现的DataSource基类。它实现了所有 DataSource 实现所共用的代码。
如果你要编写自己的 AbstractDataSource 实现,应当继承 DataSource 类。
3.4.5. 使用SingleConnectionDataSource
SingleConnectionDataSource 类是 SmartDataSource 接口的一个实现,它封装了一个单一的 Connection,该连接在每次使用后不会被关闭。
此实现不具备多线程能力。
如果任何客户端代码在假定使用连接池的情况下调用 close 方法(例如使用持久化工具时),您应将 suppressClose 属性设置为 true。此设置会返回一个抑制关闭操作的代理对象,该代理包装了物理连接。请注意,此后您将无法再将其强制转换为原生的 Oracle Connection 或类似对象。
SingleConnectionDataSource 主要是一个测试类。它通常与一个简单的 JNDI 环境结合使用,便于在应用服务器之外轻松测试代码。
与 DriverManagerDataSource 不同,它始终重用同一个连接,避免了频繁创建物理连接。
3.4.6. 使用DriverManagerDataSource
The 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 管理的事务中。通常更推荐使用更高层次的资源管理抽象(例如 JdbcTemplate 或 DataSourceUtils)来编写新的代码。 |
请参阅 TransactionAwareDataSourceProxy
javadoc 以获取更多详情。
3.4.8. 使用DataSourceTransactionManager
DataSourceTransactionManager 类是用于单个 JDBC 数据源的 PlatformTransactionManager 实现。它将来自指定数据源的 JDBC 连接绑定到当前执行的线程,可能允许每个数据源为每个线程提供一个连接。
应用程序代码需要通过 DataSourceUtils.getConnection(DataSource) 来获取 JDBC 连接,而不是使用 Java EE 标准的 DataSource.getConnection。它会抛出非受检的 org.springframework.dao 异常,而不是受检的 SQLExceptions。所有框架类(例如 JdbcTemplate)都隐式地使用了这一策略。如果未与该事务管理器一起使用,此查找策略的行为将与常规策略完全相同。因此,它可以在任何情况下使用。
DataSourceTransactionManager 类支持自定义隔离级别和超时设置,这些设置会作为相应的 JDBC 语句查询超时被应用。为了支持后者,应用程序代码必须使用 JdbcTemplate,或者对每个创建的语句调用 DataSourceUtils.applyTransactionTimeout(..) 方法。
在单资源场景下,你可以使用此实现来替代 JtaTransactionManager,因为它不要求容器支持 JTA。只要遵循所需的连接查找模式,两者之间的切换仅涉及配置的更改。JTA 不支持自定义隔离级别。
3.5. JDBC 批量操作
大多数 JDBC 驱动程序在对同一预编译语句(prepared statement)进行多次调用时,如果将这些调用批量处理,可以显著提升性能。通过将更新操作分组为批次,可以减少与数据库之间的往返次数。
3.5.1. 使用 进行基本批量操作JdbcTemplate
您可以通过实现一个特殊接口 JdbcTemplate 的两个方法,并将该实现作为第二个参数传递给 BatchPreparedStatementSetter 方法调用来完成 batchUpdate 的批处理操作。您可以使用 getBatchSize 方法来提供当前批次的大小,也可以使用 setValues 方法来设置预编译语句(prepared statement)的参数值。此方法会被调用的次数等于您在 getBatchSize 方法中指定的次数。以下示例根据列表中的条目更新 t_actor 表,并且整个列表被用作一个批次:
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) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
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. 使用对象列表进行批量操作
JdbcTemplate 和 NamedParameterJdbcTemplate 都提供了另一种执行批量更新的方式。你无需实现特殊的批量接口,而是在调用时以列表形式提供所有参数值。框架会遍历这些值,并使用内部的预编译语句(PreparedStatement)设置器进行处理。该 API 的具体形式取决于你是否使用命名参数。对于命名参数,你需要提供一个 SqlParameterSource 数组,其中每个元素对应批量操作中的一个成员。你可以使用 SqlParameterSourceUtils.createBatch 工具方法来创建这个数组,传入一个由 bean 风格对象(其 getter 方法与参数名对应)、以 String 为键的 Map 实例(包含对应的参数值),或者两者的混合组成的数组。
以下示例展示了使用命名参数进行批量更新:
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
}
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 ? 占位符:
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
}
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。
|
在这种场景下,当底层的 <p>或者,你也可以明确指定对应的JDBC类型,这可以通过以下几种方式实现: <ul> <li>通过一个'BatchPreparedStatementSetter'(如前面所示)</li> <li>通过传递给一个基于'List<Object[]]'的调用的一个显式的类型数组</li> <li>通过对自定义'MapSqlParameterSource'实例调用'registerSqlType'</li> <li>通过一个'BeanPropertySqlParameterSource',即使对于空值也能从Java声明的属性类型推导出SQL类型</li> </ul> </p> |
3.5.3. 使用多个批次的批量操作
前面的批量更新示例处理的是规模非常大的批次,你可能希望将其拆分为若干个较小的批次。你可以通过多次调用之前提到的 batchUpdate 方法来实现这一点,但现在有一种更为便捷的方法。该方法除了接收 SQL 语句外,还接收一个包含参数的对象 Collection、每个批次要执行的更新数量,以及一个 ParameterizedPreparedStatementSetter 用于设置预编译语句中的参数值。框架会遍历所提供的参数值,并将更新调用按指定的批次大小进行拆分。
以下示例展示了一个使用批处理大小为100的批量更新:
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
}
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. 使用 简化 JDBC 操作SimpleJdbc类
SimpleJdbcInsert 和 SimpleJdbcCall 类通过利用 JDBC 驱动程序可检索的数据库元数据,提供了简化的配置。
这意味着您在初始阶段需要进行的配置更少,不过如果您希望在代码中提供所有细节,也可以选择覆盖或关闭元数据处理。
3.6.1. 使用插入数据SimpleJdbcInsert
我们首先查看 SimpleJdbcInsert 类,它具有最少的配置选项。您应在数据访问层的初始化方法中实例化 SimpleJdbcInsert。在本例中,初始化方法是 setDataSource 方法。您无需对 SimpleJdbcInsert 类进行子类化,而是可以创建一个新实例,并使用 withTableName 方法设置表名。该类的配置方法采用fluid风格,会返回 SimpleJdbcInsert 的实例,从而允许您将所有配置方法链式调用。以下示例仅使用了一个配置方法(稍后我们会展示多个方法的示例):
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
}
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 中使用的键必须与数据库中定义的表列名相匹配。
这是因为我们会读取元数据来构建实际的插入语句。
3.6.2. 通过使用检索自动生成的键SimpleJdbcInsert
下一个示例使用与前一个示例相同的插入操作,但不是传入id,而是检索自动生成的键并将其设置到新的Actor对象上。在创建SimpleJdbcInsert时,除了指定表名之外,还通过usingGeneratedKeyColumns方法指定了生成键列的名称。以下代码清单展示了其工作方式:
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
}
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 是你可以依赖的基类。如果你有多个自动生成的列,或者生成的值是非数值类型,那么你可以使用通过 KeyHolder 方法返回的 executeAndReturnKeyHolder。
3.6.3. 指定列SimpleJdbcInsert
你可以通过 usingColumns 方法指定一个列名列表,从而限制插入操作所涉及的列,如下例所示:
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
}
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:
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
}
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
}
另一种选择是 MapSqlParameterSource,它类似于 Map,但提供了更便捷的 addValue 方法,该方法支持链式调用。以下示例展示了如何使用它:
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
}
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 类使用数据库中的元数据来查找 in 和 out 参数的名称,因此您无需显式声明它们。如果您愿意,或者如果您的参数(例如 ARRAY 或 STRUCT)没有自动映射到 Java 类,则可以声明这些参数。第一个示例展示了一个简单的存储过程,该过程从 MySQL 数据库中仅以 VARCHAR 和 DATE 格式返回标量值。该示例存储过程读取指定的演员条目,并以 out 参数的形式返回 first_name、last_name 和 birth_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 参数包含您要查找的演员的 id。out
参数返回从表中读取的数据。
你可以像声明 SimpleJdbcCall 一样来声明 SimpleJdbcInsert。你应该在数据访问层的初始化方法中实例化并配置该类。与 StoredProcedure 类相比,你无需创建子类,也无需声明那些可以通过数据库元数据查找到的参数。SimpleJdbcCall 的以下配置示例使用了前面提到的存储过程(除了 DataSource 之外,唯一的配置选项就是存储过程的名称):
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
}
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,其中包含存储过程中指定名称的输出(out)参数。在本例中,这些参数是 out_first_name、out_last_name 和 out_birth_date。
execute 方法的最后一部分会创建一个 Actor 实例,用于返回检索到的数据。同样重要的是,必须使用存储过程中声明的 out 参数名称。此外,结果映射中存储的 out 参数名称的大小写需与数据库中 out 参数名称的大小写保持一致,而不同数据库之间可能存在差异。为了提高代码的可移植性,您应当执行不区分大小写的查找,或指示 Spring 使用 LinkedCaseInsensitiveMap。
要实现后者,您可以创建自己的 JdbcTemplate,并将 setResultsMapCaseInsensitive 属性设置为 true。然后,您可以将这个定制的 JdbcTemplate 实例传入您的 SimpleJdbcCall 构造函数中。以下示例展示了此配置:
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
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {
private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
isResultsMapCaseInsensitive = true
}).withProcedureName("read_actor")
// ... additional methods
}
通过执行此操作,您可以避免在返回的 out 参数名称的大小写使用上发生冲突。
3.6.6. 显式声明要使用的参数SimpleJdbcCall
在本章前面部分,我们描述了参数如何从元数据中推断得出,但如果你愿意,也可以显式声明它们。
你可以通过使用 SimpleJdbcCall 方法创建并配置 declareParameters 来实现这一点,
该方法接受可变数量的 SqlParameter 对象作为输入。有关如何定义 #jdbc-params 的详细信息,请参见下一节。
| 如果所使用的数据库不是 Spring 支持的数据库,则需要显式声明。目前,Spring 支持以下数据库的存储过程调用元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。 我们还支持 MySQL、Microsoft SQL Server 和 Oracle 的存储函数元数据查找。 |
您可以选择显式声明一个、部分或全部参数。对于未显式声明的参数,仍会使用参数元数据。如果您希望跳过所有潜在参数的元数据查找处理,仅使用已声明的参数,可以在声明中调用 withoutProcedureColumnMetaDataAccess 方法。假设您为某个数据库函数声明了两个或更多不同的调用签名,此时应调用 useInParameterNames 来指定在给定签名中包含的 IN 参数名称列表。
以下示例展示了一个完整声明的存储过程调用,并使用了前述示例中的信息:
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
}
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 中的常量来指定。在本章前面,我们已经看到过类似如下的声明:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
带有 SqlParameter 的第一行声明了一个 IN 参数。您可以使用 IN 参数进行存储过程调用和查询,方法是使用 SqlQuery 及其子类(在 理解 SqlQuery 中介绍)。
第二行(使用 SqlOutParameter)声明了一个在存储过程调用中使用的 out 参数。此外,还有一个 SqlInOutParameter 用于 InOut 参数(即向存储过程提供输入值,同时也返回一个值的参数)。
只有声明为 SqlParameter 和 SqlInOutParameter 的参数才会用于提供输入值。这与 StoredProcedure 类不同,后者(出于向后兼容的原因)允许为声明为 SqlOutParameter 的参数提供输入值。 |
对于 IN 参数,除了指定名称和 SQL 类型外,还可以为数值型数据指定精度(scale),或为自定义数据库类型指定类型名称。对于 out 参数,您可以提供一个 RowMapper 来处理从 REF 游标返回的行数据的映射。另一种选择是指定一个 SqlReturnType,以便自定义返回值的处理方式。
3.6.8. 使用存储函数调用SimpleJdbcCall
你可以几乎以与调用存储过程相同的方式来调用存储函数,唯一的区别是你提供的是函数名而非过程名。你可以在配置中使用 withFunctionName 方法来表明你希望调用一个函数,系统会自动生成对应的函数调用字符串。执行函数时使用专门的方法(executeFunction),该方法会以指定类型的对象形式返回函数的返回值,这意味着你无需从结果映射(results map)中手动获取返回值。对于只有一个 executeObject 参数的存储过程,也有一个类似的便捷方法(名为 out)。以下示例(针对 MySQL)基于一个名为 get_actor_name 的存储函数,该函数返回演员的全名:
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,如下例所示:
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
}
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。
3.6.9. 返回一个ResultSet或来自的 REF 游标SimpleJdbcCall
调用返回结果集的存储过程或函数有点棘手。某些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则要求显式注册一个特定类型的 out 参数。这两种方式都需要额外的处理来遍历结果集并处理返回的行。使用 SimpleJdbcCall 时,你可以使用 returningResultSet 方法,并为特定参数声明一个 RowMapper 实现。如果结果集是在结果处理过程中返回的,则不会定义名称,因此返回的结果必须与你声明 RowMapper 实现的顺序相匹配。不过,所指定的名称仍会被用于将处理后的结果列表存储在 execute 语句返回的结果映射(map)中。
下一个示例(针对 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 规范,因此可以使用 BeanPropertyRowMapper,通过在 newInstance 方法中传入需要映射的目标类来创建它。
以下示例展示了如何实现这一点:
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
}
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,因为该调用不接受任何参数。
然后从结果映射(map)中获取演员列表,并返回给调用者。
3.7. 将 JDBC 操作建模为 Java 对象
org.springframework.jdbc.object 包包含了一些类,可让你以更加面向对象的方式访问数据库。例如,你可以执行查询,并将结果以包含业务对象的列表形式返回,其中关系型数据库的列数据会被映射到业务对象的属性上。你还可以执行存储过程,以及运行更新、删除和插入语句。
|
许多 Spring 开发人员认为,下面描述的各种关系型数据库(RDBMS)操作类( 然而,如果你在使用 RDBMS 操作类时获得了可衡量的价值, 你应该继续使用这些类。 |
3.7.1. 理解SqlQuery
SqlQuery 是一个可重用、线程安全的类,用于封装 SQL 查询。子类必须实现 newRowMapper(..) 方法,以提供一个 RowMapper 实例,该实例能够针对查询执行过程中生成的 ResultSet 所遍历到的每一行数据创建一个对象。SqlQuery 类很少直接使用,因为 MappingSqlQuery 子类提供了将行数据映射到 Java 类的更便捷实现。其他继承自 SqlQuery 的实现包括 MappingSqlQueryWithParameters 和 UpdatableSqlQuery。
3.7.2. 使用MappingSqlQuery
MappingSqlQuery 是一个可重用的查询类,其具体子类必须实现抽象方法 mapRow(..),以将提供的 ResultSet 中的每一行数据转换为指定类型的对象。以下示例展示了一个自定义查询,它将 t_actor 表中的数据映射到 Actor 类的一个实例:
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;
}
}
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,因此它可能包含在执行期间传入的任何参数的占位符。您必须通过使用 declareParameter 方法并传入 SqlParameter 来声明每个参数。SqlParameter 接受一个名称以及 java.sql.Types 中定义的 JDBC 类型。定义所有参数后,您可以调用 compile() 方法,以便准备语句并在以后运行。此类在编译后是线程安全的,因此,只要这些实例在 DAO 初始化时创建,它们就可以作为实例变量保留并被重用。以下示例展示了如何定义此类:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Customer getCustomer(Long id) {
return actorMappingQuery.findObject(id);
}
private val actorMappingQuery = ActorMappingQuery(dataSource)
fun getCustomer(id: Long) = actorMappingQuery.findObject(id)
前面示例中的方法会根据传入的唯一参数 id 检索对应的客户。由于我们只需要返回一个对象,因此调用便捷方法 findObject,并将 id 作为参数传入。如果我们有一个查询需要返回对象列表,并且接受额外的参数,那么我们会使用某个 execute 方法,该方法接收以可变参数(varargs)形式传入的参数值数组。以下示例展示了这样一个方法:
public List<Actor> searchForActors(int age, String namePattern) {
List<Actor> actors = actorSearchMappingQuery.execute(age, namePattern);
return actors;
}
fun searchForActors(age: Int, namePattern: String) =
actorSearchMappingQuery.execute(age, namePattern)
3.7.3. 使用SqlUpdate
SqlUpdate 类封装了一个 SQL 更新操作。与查询类似,更新对象是可重用的;并且与所有 RdbmsOperation 类一样,更新操作可以包含参数,并通过 SQL 语句进行定义。该类提供了多个 update(..) 方法,这些方法类似于查询对象中的 execute(..) 方法。SQLUpdate 类是一个具体类,可以被继承——例如,用于添加自定义的更新方法。
然而,您并不一定需要继承 SqlUpdate 类,因为只需设置 SQL 语句并声明参数,即可轻松地对其进行参数化配置。
以下示例创建了一个名为 execute 的自定义更新方法:
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);
}
}
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 类型,如下列代码片段所示:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 类型使用 java.sql.Types 常量指定。
第一行(带有 SqlParameter)声明了一个 IN 参数。您可以将 IN 参数用于存储过程调用,以及使用 SqlQuery 及其子类进行的查询(相关内容在 了解 SqlQuery 中涵盖)。
第二行(使用 SqlOutParameter)声明了一个在存储过程调用中使用的 out 参数。此外,还有一个 SqlInOutParameter 用于 InOut 参数(即向存储过程提供 in 值,同时也返回一个值的参数)。
对于 in 参数,除了指定名称和 SQL 类型外,还可以为数值数据指定精度(scale),或为自定义数据库类型指定类型名称。对于 out 参数,可以提供一个 RowMapper 来处理从 REF 游标返回的行数据的映射。另一种选择是指定一个 SqlReturnType,以便自定义返回值的处理方式。
下一个简单 DAO 示例使用 StoredProcedure 调用一个函数(sysdate()),该函数随任何 Oracle 数据库提供。要使用存储过程功能,您需要创建一个继承自 StoredProcedure 的类。在本示例中,StoredProcedure 类是一个内部类。但是,如果您需要重用 StoredProcedure,可以将其声明为顶级类。此示例没有输入参数,但通过使用 SqlOutParameter 类将输出参数声明为日期类型。execute() 方法运行该过程,并从结果 Map 中提取返回的日期。结果 Map 为每个声明的输出参数(本例中仅有一个)包含一个条目,使用参数名作为键。以下列表展示了我们自定义的 StoredProcedure 类:
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;
}
}
}
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 游标):
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>());
}
}
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>())
}
}
请注意,在 declareParameter(..) 构造函数中使用的 TitlesAndGenresStoredProcedure 方法的重载变体传入了 RowMapper 实现的实例。这是一种非常便捷且强大的方式,可用于复用现有功能。接下来的两个示例提供了这两个 RowMapper 实现的代码。
TitleMapper 类将所提供的 ResultSet 中的每一行映射为一个 Title 领域对象,如下所示:
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;
}
}
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 中的每一行映射为一个 Genre 领域对象,如下所示:
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"));
}
}
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) 方法,如下例所示:
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);
}
}
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类,例如BeanPropertySqlParameterSource或MapSqlParameterSource。它们都提供了用于为任意命名参数值注册 SQL 类型的方法。
3.8.2. 处理 BLOB 和 CLOB 对象
你可以在数据库中存储图像、其他二进制数据以及大段文本。这些大型对象在二进制数据的情况下被称为 BLOB(Binary Large OBject,二进制大对象),在字符数据的情况下被称为 CLOB(Character Large OBject,字符大对象)。在 Spring 中,你可以直接使用 JdbcTemplate 来处理这些大型对象,也可以通过 RDBMS 对象和 SimpleJdbc 类所提供的更高层次的抽象来处理。所有这些方法都使用 LobHandler 接口的实现类来实际管理 LOB(Large OBject,大对象)数据。LobHandler 通过 LobCreator 方法提供对 getLobCreator 类的访问,该类用于创建新的 LOB 对象以插入数据库。
LobCreator 和 LobHandler 为 LOB 的输入和输出提供以下支持:
-
BLOB
-
byte[]:getBlobAsBytes和setBlobAsBytes -
InputStream:getBlobAsBinaryStream和setBlobAsBinaryStream
-
-
CLOB
-
String:getClobAsString和setClobAsString -
InputStream:getClobAsAsciiStream和setClobAsAsciiStream -
Reader:getClobAsCharacterStream和setClobAsCharacterStream
-
下一个示例展示了如何创建并插入一个 BLOB。稍后我们将展示如何从数据库中将其读取回来。
此示例使用了 JdbcTemplate 和 AbstractLobCreatingPreparedStatementCallback 的一个实现。该实现包含一个方法 setValues。此方法提供了一个 LobCreator,我们用它来为 SQL 插入语句中的 LOB 列设置值。
在本例中,我们假定已存在一个名为 lobHandler 的变量,并且它已被设置为 DefaultLobHandler 的一个实例。通常,您通过依赖注入来设置该值。
以下示例展示了如何创建并插入一个 BLOB:
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 的内容。 |
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 的内容。 |
|
如果你在从 请查阅您所使用的 JDBC 驱动程序的文档,以确认它是否支持在不提供内容长度的情况下流式传输 LOB。 |
现在是时候从数据库中读取 LOB 数据了。同样,您使用一个 JdbcTemplate,配合相同的实例变量 lobHandler 和一个指向 DefaultLobHandler 的引用。
以下示例展示了如何实现这一点:
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 的内容。 |
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. 为 IN 子句传入值列表
SQL 标准允许根据包含可变值列表的表达式来选择行。一个典型的例子是 select * from T_ACTOR where id in
(1, 2, 3)。这种可变值列表在 JDBC 标准中并未被预编译语句(prepared statements)直接支持。你无法声明可变数量的占位符。你需要预先准备多个具有所需占位符数量的不同 SQL 语句,或者在知道所需占位符数量后动态生成 SQL 字符串。NamedParameterJdbcTemplate 和 JdbcTemplate 中提供的命名参数支持采用了后一种方法。你可以将值以 java.util.List 形式的原始对象传入。该列表用于插入所需的占位符,并在语句执行期间传入相应的值。
在传入大量值时要小心。in 表达式列表的 JDBC 标准并不保证可以使用超过 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 的一部分。
以下示例展示了如何返回用户自定义类型 STRUCT 的 Oracle ITEM_TYPE 对象的值:
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;
}));
// ...
}
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)。当前活动的数据库连接会作为参数传入,你可以利用该连接创建特定于数据库的对象,例如 StructDescriptor 实例或 ArrayDescriptor 实例。以下示例创建了一个 StructDescriptor 实例:
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;
}
};
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 添加到包含存储过程 Map 调用输入参数的 execute 中。
SqlTypeValue 的另一种用途是向 Oracle 存储过程传入一个值数组。在这种情况下,必须使用 Oracle 自己的内部 ARRAY 类,你可以使用 SqlTypeValue 来创建 Oracle ARRAY 的实例,并用 Java ARRAY 中的值填充它,如下例所示:
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;
}
};
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 数据库引擎的支持。原生支持 HSQL、
H2 和 Derby。
您还可以使用可扩展的 API 来接入新的嵌入式数据库类型和 DataSource 实现。
3.9.2. 使用 Spring XML 创建嵌入式数据库
如果你想在 Spring ApplicationContext 中将一个嵌入式数据库实例暴露为一个 Bean,可以使用 embedded-database 命名空间中的 spring-jdbc 标签:
<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.sql 和 test-data.sql 资源中的 SQL 脚本进行初始化。此外,作为最佳实践,该嵌入式数据库被赋予一个唯一生成的名称。此嵌入式数据库以 javax.sql.DataSource 类型的 bean 形式提供给 Spring 容器,随后可根据需要注入到数据访问对象中。
3.9.3. 以编程方式创建嵌入式数据库
EmbeddedDatabaseBuilder 类提供了一个流畅的 API,用于以编程方式构建嵌入式数据库。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用该类,如下例所示:
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()
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()
有关所有支持选项的更多详情,请参阅 EmbeddedDatabaseBuilder 的 Javadoc。
你也可以使用 EmbeddedDatabaseBuilder 通过 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();
}
}
@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,请将 type 标签的 embedded-database 属性设置为 HSQL。如果使用构建器 API,则调用 setType(EmbeddedDatabaseType) 方法,并传入 EmbeddedDatabaseType.HSQL。
3.9.5. 使用嵌入式数据库测试数据访问逻辑
嵌入式数据库为测试数据访问代码提供了一种轻量级的方式。下面的示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在多个测试类之间复用时,使用此类模板对于一次性测试非常有用。然而,如果您希望在测试套件中共享一个嵌入式数据库,请考虑使用Spring TestContext 框架,并按照使用 Spring XML 创建嵌入式数据库和以编程方式创建嵌入式数据库中所述,将嵌入式数据库配置为 Spring #jdbc-embedded-database-java 中的一个 Bean。以下代码清单展示了该测试模板:
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();
}
}
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 配置仅在激活的 Bean 定义配置文件(profiles)方面有所不同。
此类错误的根本原因在于: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 的引用,你可以使用 initialize-database 命名空间中的 spring-jdbc 标签:
<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>
前面的示例会对数据库执行两个指定的脚本。第一个脚本用于创建数据库模式(schema),第二个脚本则用测试数据集填充表。脚本的位置也可以使用通配符模式,采用 Spring 中资源常用的 Ant 风格(例如,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 | 从名为 enabled 的系统属性中获取 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 方法,或实现了 afterPropertiesSet() 接口的组件中的 InitializingBean 方法)。如果其他 Bean 依赖于同一个数据源,并在自身的初始化回调中使用该数据源,就可能会出现问题,因为此时数据尚未完成初始化。一个常见的例子是缓存组件,它在应用启动时立即初始化并从数据库加载数据。
要解决这个问题,你有两个选项:将缓存初始化策略更改为稍后的阶段,或者确保数据库初始化器首先被初始化。
如果你能控制该应用程序,更改缓存初始化策略可能会很容易;否则则不然。 关于如何实现这一点的一些建议包括:
-
使缓存在首次使用时延迟初始化,从而提升应用程序的启动速度。
-
让你的缓存或负责初始化缓存的独立组件实现
Lifecycle或SmartLifecycle接口。当应用上下文启动时,你可以通过设置SmartLifecycle的autoStartup标志来自动启动它;也可以通过对包含该组件的上下文调用Lifecycle方法来手动启动一个ConfigurableApplicationContext.start()组件。 -
使用 Spring 的
ApplicationEvent或类似的自定义观察者机制来触发缓存的初始化。ContextRefreshedEvent在上下文准备就绪(所有 Bean 都已初始化完成)时总会被发布,因此这通常是一个很有用的钩子(SmartLifecycle默认就是通过这种方式工作的)。
确保数据库初始化器首先被初始化也可以很容易实现。以下是一些关于如何实现这一点的建议:
-
依赖 Spring
BeanFactory的默认行为,即 Bean 按照注册顺序进行初始化。你可以通过在 XML 配置中采用一组<import/>元素的常见做法来轻松实现这一点,这些元素用于对应用程序模块进行排序,并确保数据库及其初始化配置被列在最前面。 -
将
DataSource与其使用的业务组件分离,并通过将它们置于独立的ApplicationContext实例中(例如,父上下文包含DataSource,子上下文包含业务组件)来控制它们的启动顺序。这种结构在 Spring Web 应用程序中很常见,但也可以更广泛地应用。
4. 对象关系映射(ORM)数据访问
本节介绍在使用对象关系映射(ORM)时的数据访问。
4.1. 使用 Spring 进行 ORM 简介
Spring 框架支持与 Java Persistence API(JPA)的集成,并为资源管理、数据访问对象(DAO)实现和事务策略提供对原生 Hibernate 的支持。例如,对于 Hibernate,Spring 提供了一流的支持,包含多项便捷的 IoC 特性,可解决许多典型的 Hibernate 集成问题。您可以通过依赖注入来配置所有受支持的对象关系(OR)映射工具的功能。这些工具可以参与 Spring 的资源和事务管理,并遵循 Spring 通用的事务和 DAO 异常层次结构。推荐的集成方式是使用普通的 Hibernate 或 JPA API 编写 DAO。
当你构建数据访问应用程序时,Spring 会为你所选择的 ORM 层提供显著增强。你可以根据需要充分利用其集成支持,并应将这种集成工作与自行构建类似基础设施的成本和风险进行比较。由于所有组件均被设计为一组可重用的 JavaBean,因此无论采用何种技术,你都可以像使用库一样使用大部分 ORM 支持功能。在 Spring IoC 容器中使用 ORM 能够简化配置和部署。因此,本节中的大多数示例都展示了在 Spring 容器内部的配置方式。
使用 Spring Framework 创建 ORM DAO 的好处包括:
-
更简便的测试。 Spring 的 IoC 方法使得轻松替换 Hibernate
SessionFactory实例、JDBCDataSource实例、事务管理器以及映射对象的实现(如有需要)及其配置位置成为可能。这反过来使得对每个与持久化相关的代码单元进行隔离测试变得更加容易。 -
通用的数据访问异常。 Spring 可以包装来自你所使用的 ORM 工具的异常, 将其从专有的(可能是受检的)异常转换为统一的运行时
DataAccessException异常体系。这一特性使你能够在适当的层次中处理大多数不可恢复的持久化异常, 而无需编写繁琐的 catch、throws 和异常声明样板代码。你仍然可以根据需要捕获并处理异常。 请记住,JDBC 异常(包括特定数据库的方言异常)也会被转换到相同的异常体系中, 这意味着你可以在一致的编程模型中使用 JDBC 执行某些操作。 -
通用资源管理。 Spring 应用上下文可以处理 Hibernate
SessionFactory实例、JPAEntityManagerFactory实例、JDBCDataSource实例以及其他相关资源的位置和配置。这使得这些值易于管理和更改。Spring 提供了对持久化资源的高效、简便且安全的处理方式。例如,使用 Hibernate 的相关代码通常需要使用同一个 HibernateSession,以确保效率和正确的事务处理。Spring 通过 HibernateSession暴露当前Session,从而透明地在当前线程中轻松创建并绑定SessionFactory。因此,无论是在本地事务环境还是 JTA 事务环境中,Spring 都解决了典型 Hibernate 使用中的许多长期存在的问题。 -
集成的事务管理。 您可以通过
@Transactional注解,或在 XML 配置文件中显式配置事务 AOP 通知,以声明式、面向切面编程(AOP)风格的方法拦截器来包装您的 ORM 代码。在这两种情况下,事务语义和异常处理(如回滚等)都会自动为您处理。正如资源与事务管理一节中所讨论的,您还可以在不影响 ORM 相关代码的情况下,切换使用不同的事务管理器。例如,您可以在本地事务和 JTA 之间进行切换,而在这两种场景下均可使用相同完整的功能(如声明式事务)。此外,JDBC 相关代码也可以在事务层面与您用于 ORM 的代码完全集成。这对于不适合使用 ORM 的数据访问操作(例如批处理和 BLOB 流式传输)非常有用,但这些操作仍需与 ORM 操作共享相同的事务。
| 如需更全面的 ORM 支持,包括对 MongoDB 等其他数据库技术的支持,您可以查看 Spring Data 项目套件。如果您使用 JPA,https://spring.io 提供的 《使用 JPA 访问数据入门》 指南是一份绝佳的入门资料。 |
4.2. 一般 ORM 集成注意事项
本节重点介绍适用于所有 ORM 技术的注意事项。 Hibernate 部分提供了更多详细信息,并在具体上下文中展示了这些特性和配置。
Spring ORM 集成的主要目标是实现清晰的应用分层(适用于任何数据访问和事务技术),并使应用程序对象之间保持松耦合——不再让业务服务依赖于特定的数据访问或事务策略,不再使用硬编码的资源查找,不再使用难以替换的单例,也不再需要自定义的服务注册表。目标是采用一种简单且一致的方式来装配应用程序对象,使其尽可能可重用,并尽量减少对容器的依赖。所有独立的数据访问功能均可单独使用,但也能很好地与 Spring 的应用上下文(application context)概念集成,提供基于 XML 的配置,并支持对普通的 JavaBean 实例进行交叉引用,而这些 JavaBean 实例无需感知 Spring 的存在。在典型的 Spring 应用程序中,许多重要的对象都是 JavaBean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象和事务管理器的业务服务、Web 视图解析器、使用业务服务的 Web 控制器,等等。
4.2.1. 资源和事务管理
典型的企业应用程序充斥着重复的资源管理代码。 许多项目试图自行设计解决方案,有时为了编程上的便利而牺牲了对故障的正确处理。 Spring 提倡采用简单的方案来实现恰当的资源处理,具体而言,对于 JDBC 使用基于模板的 IoC 方式,而对于 ORM 技术则应用 AOP 拦截器。
该基础设施提供了恰当的资源处理,并将特定 API 异常正确地转换为一个非受检(unchecked)的基础设施异常层次结构。Spring 引入了一个 DAO 异常层次结构,适用于任何数据访问策略。对于直接使用 JDBC 的情况,前一节中提到的 #jdbc-JdbcTemplate 类提供了连接管理,并将 SQLException 正确地转换为 DataAccessException 异常层次结构,包括将数据库特定的 SQL 错误代码转换为具有明确含义的异常类。对于 ORM 技术,请参阅下一节,了解如何获得同样的异常转换优势。
在事务管理方面,JdbcTemplate 类集成了 Spring 的事务支持,并通过相应的 Spring 事务管理器同时支持 JTA 和 JDBC 事务。对于所支持的 ORM 技术,Spring 也通过 Hibernate 和 JPA 事务管理器提供了对 Hibernate 和 JPA 的支持,同时也支持 JTA。有关事务支持的详细信息,请参阅事务管理章节。
4.2.2. 异常转换
当你在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久化技术的原生异常类。DAO 抛出 HibernateException 或 PersistenceException 的子类,具体取决于所使用的技术。这些异常都是运行时异常,无需声明或捕获。您还可能需要处理
IllegalArgumentException 和 IllegalStateException。这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久化技术自身的异常结构。在不将调用方与实现策略绑定的情况下,无法捕获特定原因(例如乐观锁失败)。这种权衡对于那些高度基于 ORM 的应用程序,或者不需要任何特殊异常处理(或两者兼有)的应用程序来说,可能是可以接受的。然而,Spring 通过 @ControllerAdvice 注解透明地应用异常处理。以下示例(一个使用 Java 配置,另一个使用 XML 配置)展示了如何实现这一点:
@Repository
public class ProductDaoImpl implements ProductDao {
// class body here...
}
@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 自定义的异常层次结构。
4.3. Hibernate
我们首先介绍在 Spring 环境中使用 Hibernate 5,并以此展示 Spring 集成对象关系映射(ORM)框架所采用的方法。本节详细讨论了许多问题,并展示了 DAO 实现和事务界定的不同变体。这些模式中的大多数都可以直接应用于所有其他受支持的 ORM 工具。本章后续部分将介绍其他 ORM 技术,并提供简要示例。
| 从 Spring Framework 5.0 起,Spring 在 JPA 支持方面要求使用 Hibernate ORM 4.3 或更高版本, 而如果要直接针对原生的 Hibernate Session API 进行编程,则甚至需要 Hibernate ORM 5.0 或更高版本。 请注意,Hibernate 团队目前已不再维护 5.1 之前的任何版本, 并且很可能很快将仅专注于 5.4 及更高版本。 |
4.3.1. SessionFactory在 Spring 容器中设置
为了避免将应用程序对象与硬编码的资源查找绑定在一起,您可以将资源(例如 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 的 SessionFactory / JndiObjectFactoryBean 来访问位于 JNDI 中的 <jee:jndi-lookup>,以检索并暴露它。
然而,这通常在 EJB 上下文之外并不常见。
|
Spring 还提供了一个
从 Spring Framework 5.1 开始,这种原生 Hibernate 配置除了支持原生 Hibernate 访问外,还可以暴露一个 JPA |
4.3.2. 基于原生 Hibernate API 实现 DAO
Hibernate 有一个名为上下文会话(contextual sessions)的特性,其中 Hibernate 本身为每个事务管理一个当前的Session。这大致等同于 Spring 为每个事务同步一个 Hibernate Session 的机制。相应的 DAO 实现类似于以下示例,该示例基于原生的 Hibernate API:
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();
}
}
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()
}
}
这种风格类似于 Hibernate 参考文档和示例中的做法,只是将 SessionFactory 保存在实例变量中。我们强烈推荐采用这种基于实例的设置方式,而不是使用 Hibernate 的 CaveatEmptor 示例应用程序中那种传统的 static HibernateUtil 类。(通常情况下,除非绝对必要,否则不要将任何资源保存在 static 变量中。)
前面的 DAO 示例遵循了依赖注入模式。它能很好地融入 Spring IoC 容器,就像使用 Spring 的 HibernateTemplate 编码时一样。
你也可以在纯 Java 环境中(例如在单元测试中)配置这样的 DAO。为此,只需实例化该 DAO 并调用 setSessionFactory(..) 方法传入所需的 SessionFactory 引用即可。作为 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 支持 Hibernate 的
SessionFactory.getCurrentSession() 方法,适用于任何 Spring 事务策略,
即使使用 Session,也能返回当前由 Spring 管理的事务性 HibernateTransactionManager。
该方法的标准行为仍然是:如果存在正在进行的 JTA 事务,则返回与之关联的当前 Session。
无论你使用的是 Spring 的 JtaTransactionManager、EJB 容器管理的事务(CMT),还是直接使用 JTA,
这一行为都适用。
总之,你可以基于原生的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。
4.3.3. 声明式事务界定
我们建议您使用 Spring 的声明式事务支持,它允许您用 AOP 事务拦截器替代 Java 代码中显式的事务界定 API 调用。您可以在 Spring 容器中通过 Java 注解或 XML 来配置该事务拦截器。这种声明式事务功能可让您避免在业务服务中编写重复的事务界定代码,从而专注于添加业务逻辑——这才是您应用程序的真正价值所在。
| 在继续之前,我们强烈建议您阅读声明式事务管理, 如果您尚未阅读的话。 |
你可以使用 @Transactional 注解对服务层进行标注,并指示 Spring 容器查找这些注解,为这些被标注的方法提供事务语义。以下示例展示了如何实现这一点:
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();
}
}
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>
4.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>
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...
}
});
}
}
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 在回调中仅限于未检查的异常。TransactionTemplate 在发生未检查的应用程序异常时,或者当应用程序将事务标记为仅回滚(通过设置 TransactionStatus)时,会触发回滚。默认情况下,TransactionInterceptor 的行为与此相同,但允许为每个方法配置可自定义的回滚策略。
4.3.5. 事务管理策略
TransactionTemplate和TransactionInterceptor都将实际的事务处理委托给一个PlatformTransactionManager实例(对于单个 Hibernate SessionFactory,它可以是一个HibernateTransactionManager,其底层使用ThreadLocalSession),或者对于 Hibernate 应用程序,委托给一个JtaTransactionManager(将事务委派给容器的 JTA 子系统)。您甚至可以使用自定义的PlatformTransactionManager实现。从原生 Hibernate 事务管理切换到 JTA(例如,当应用程序的某些部署面临分布式事务需求时)仅需进行配置更改。您可以将 Hibernate 事务管理器替换为 Spring 的 JTA 事务实现。事务界定和数据访问代码无需任何修改即可正常工作,因为它们使用的是通用的事务管理 API。
对于跨多个 Hibernate SessionFactory 的分布式事务,您可以将 JtaTransactionManager 作为事务策略,与多个 LocalSessionFactoryBean 定义结合使用。每个 DAO 都会通过其对应的 bean 属性注入一个特定的 SessionFactory 引用。只要所有底层的 JDBC 数据源都是支持事务的容器数据源,并且业务服务使用 JtaTransactionManager 作为事务策略,那么该业务服务就可以在任意数量的 DAO 和任意数量的 SessionFactory 上划分事务,而无需特别关注细节。
HibernateTransactionManager 和 JtaTransactionManager 均支持在 Hibernate 中进行适当的 JVM 级别缓存处理,而无需依赖容器特定的事务管理器查找或 JCA 连接器(前提是您不使用 EJB 来启动事务)。
HibernateTransactionManager 可以将 Hibernate JDBC Connection 导出为针对特定 DataSource 的普通 JDBC 访问代码。此功能允许在仅访问一个数据库的情况下,完全无需 JTA 即可实现混合使用 Hibernate 和 JDBC 数据访问的高层级事务划分。如果您通过 LocalSessionFactoryBean 类的 dataSource 属性将传入的 SessionFactory 配置了 DataSource,那么 HibernateTransactionManager 会自动将 Hibernate 事务暴露为 JDBC 事务。或者,您也可以通过 HibernateTransactionManager 类的 dataSource 属性显式指定应暴露其事务的 DataSource。
4.3.6. 比较容器管理资源与本地定义资源
你可以在容器管理的 JNDI SessionFactory 和本地定义的 SessionFactory 之间切换,而无需更改一行应用程序代码。资源定义是放在容器中还是放在应用程序本地,主要取决于你所采用的事务策略。与 Spring 定义的本地 SessionFactory 相比,手动注册的 JNDI SessionFactory 并不会带来任何额外优势。通过 Hibernate 的 JCA 连接器部署 4 可以使其参与到 Java EE 服务器的管理基础设施中,但除此之外并不会带来实质性的额外价值。
Spring 的事务支持不依赖于容器。当使用任何除了 JTA 外的策略配置时,事务支持也可以在独立或测试环境中工作。
特别是在单一数据库事务的典型情况下,Spring 的单资源本地事务支持是一个轻量级且强大的替代方案,相比于 JTA。当你使用本地 EJB 无状态会话 Bean 驱动事务时,你既依赖于一个 EJB 容器也依赖于 JTA,即使你只访问单一数据库并且仅通过容器管理的事务提供声明式事务。直接程序化使用 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 应用程序要复杂得多。此外,如果你使用的是例如 WebLogic Express 这样的容器,由于它不提供 JCA 支持,通常还需要该容器的企业版(Enterprise Edition)。而一个使用本地资源、事务仅跨越单一数据库的 Spring 应用程序,可以在任何 Java EE Web 容器(如 Tomcat、Resin,甚至普通的 Jetty)中正常运行,无需依赖 JTA、JCA 或 EJB。另外,这样的中间层还可以轻松地在桌面应用程序或测试套件中复用。
综上所述,如果您不使用 EJB,则应坚持采用本地 SessionFactory 配置,并配合使用 Spring 的 HibernateTransactionManager 或 JtaTransactionManager。这样您就能获得所有优势,包括恰当的事务性 JVM 级缓存和分布式事务支持,同时又避免了容器部署带来的不便。只有在与 EJB 结合使用时,通过 JCA 连接器将 Hibernate SessionFactory 注册到 JNDI 中才有实际价值。
4.3.7. 使用 Hibernate 时出现的应用服务器虚假警告
在某些具有非常严格的 XADataSource 实现的 JTA 环境中(目前包括某些 WebLogic Server 和 WebSphere 版本),如果 Hibernate 的配置未考虑该环境的 JTA 事务管理器,则应用程序服务器日志中可能会出现虚假的警告或异常。这些警告或异常表明正在访问的连接已不再有效,或者 JDBC 访问已不再有效,可能是因为事务已不再处于活动状态。例如,以下是来自 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 事务管理器来解决此类问题,Hibernate 会与该事务管理器(以及 Spring)进行同步。你有两种方式可以实现这一点:
-
将您的 Spring
JtaTransactionManagerbean 传递给 Hibernate 配置。最简单的方式是将该 bean 的引用设置到您的jtaTransactionManagerbean 的LocalSessionFactoryBean属性中(参见Hibernate 事务配置)。 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回调被调用。 -
除了其他操作外,这种同步还可能通过 Hibernate 的
afterTransactionCompletion回调(用于清除 Hibernate 缓存)触发 Spring 对 Hibernate 的回调,随后显式调用 Hibernate 会话的close()方法,这将导致 Hibernate 尝试关闭 JDBC 连接(close())。 -
在某些环境中,此
Connection.close()调用会触发警告或错误,因为应用服务器不再认为该Connection可用,原因是事务已经提交。
当 Hibernate 配置为感知 JTA 事务管理器时,JTA 事务提交时将发生以下事件:
-
JTA 事务已准备好提交。
-
Spring 的
JtaTransactionManager与 JTA 事务同步,因此事务会通过 JTA 事务管理器的beforeCompletion回调被调用。 -
Spring 意识到 Hibernate 本身已与 JTA 事务同步,并且其行为与前一种场景有所不同。特别是,它与 Hibernate 的事务性资源管理保持一致。
-
JTA 事务提交。
-
Hibernate 与 JTA 事务同步,因此事务会通过 JTA 事务管理器的
afterCompletion回调被调用,从而能够正确地清空其缓存。
4.4. JPA
Spring JPA 位于 org.springframework.orm.jpa 包下,为 Java 持久化 API 提供了全面的支持,其集成方式类似于 Hibernate 的集成,并且能够感知底层实现,从而提供额外的功能。
4.4.1. Spring 环境中 JPA 配置的三种选项
Spring JPA 支持提供了三种方式来配置应用程序用于获取实体管理器(Entity Manager)的 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 定义,也不支持全局事务。此外,持久化类的织入(字节码转换)是特定于 JPA 提供商的,通常需要在启动时指定一个特定的 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 服务器会自动检测持久化单元(实际上就是应用程序 JAR 包中的 META-INF/persistence.xml 文件)以及 Java EE 部署描述符(例如 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 功能。 这包括 Tomcat 等 Web 容器、独立应用程序,以及具有复杂持久化需求的集成测试。
如果你希望专门配置 Hibernate 设置,一个直接的替代方案是使用 Hibernate 5.2/5.3/5.4,并设置一个原生的 Hibernate LocalSessionFactoryBean,而不是普通的 JPA LocalContainerEntityManagerFactoryBean,使其既能与 JPA 访问代码交互,也能与原生 Hibernate 访问代码交互。
详情请参见用于 JPA 交互的原生 Hibernate 配置。 |
LocalContainerEntityManagerFactoryBean 提供了对 EntityManagerFactory 配置的完全控制,适用于需要细粒度自定义的环境。LocalContainerEntityManagerFactoryBean 会基于 PersistenceUnitInfo 文件、所提供的 persistence.xml 策略以及指定的 dataSourceLookup 创建一个 loadTimeWeaver 实例。因此,可以在 JNDI 之外使用自定义数据源,并控制织入(weaving)过程。以下示例展示了一个典型的 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,同时支持本地事务和全局事务等。然而,它也对运行环境提出了要求,例如,如果持久化提供程序需要字节码转换,则必须提供具备织入(weaving)能力的类加载器。
此选项可能与 Java EE 服务器内置的 JPA 功能发生冲突。在完整的 Java EE 环境中,建议从 JNDI 获取您的 EntityManagerFactory。
或者,在您的 persistenceXmlLocation 定义中指定一个自定义的 LocalContainerEntityManagerFactoryBean(例如,
META-INF/my-persistence.xml),并在您的应用程序 JAR 文件中仅包含具有该名称的持久化描述文件。由于 Java EE 服务器仅查找默认的
META-INF/persistence.xml 文件,因此会忽略此类自定义的持久化单元,从而避免与 Spring 驱动的 JPA 配置发生冲突。(例如,这适用于 Resin 3.1。)
LoadTimeWeaver 接口是 Spring 提供的一个类,它允许以特定方式插入 JPA 的 ClassTransformer 实例,具体方式取决于运行环境是 Web 容器还是应用服务器。通过代理(agent)来挂钩(hook)https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html 通常效率不高。这类代理作用于整个虚拟机,并检查每一个被加载的类,这在生产服务器环境中通常是不希望出现的行为。
Spring 提供了多种环境下的 LoadTimeWeaver 实现,使得 ClassTransformer 实例仅对每个类加载器生效,而不是对每个虚拟机生效。
有关 core.html#aop-aj-ltw-spring 的实现及其配置(包括通用配置或针对各种平台(如 Tomcat、JBoss 和 WebSphere)的定制配置),请参阅 AOP 章节中的Spring 配置以获取更多详细信息。
如Spring 配置中所述,您可以使用 LoadTimeWeaver 注解或 @EnableLoadTimeWeaving XML 元素来配置一个上下文范围的 context:load-time-weaver。所有 JPA 的 LocalContainerEntityManagerFactoryBean 实例会自动检测并使用该全局织入器。以下示例展示了设置加载时织入器的推荐方式,这种方式能够自动检测平台(例如 Tomcat 支持织入的类加载器或 Spring 的 JVM 代理),并将织入器自动传播给所有支持织入功能的 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)上运行,而无需使用Java代理。这一点尤其重要,因为当托管的应用程序依赖于不同的JPA实现时,JPA转换器仅在类加载器级别应用,从而彼此隔离。
处理多个持久化单元
对于依赖多个持久化单元位置(例如,存储在类路径中多个 JAR 文件内)的应用程序,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 使用。
后台引导初始化
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 时,请确保为其仓库也配置延迟启动。
4.4.2. 基于 JPA 实现 DAO:EntityManagerFactory和EntityManager
尽管 EntityManagerFactory 实例是线程安全的,但 EntityManager 实例不是。
根据 JPA 规范的定义,注入的 JPA EntityManager 的行为类似于从应用服务器的 JNDI 环境中获取的 EntityManager。
它会将所有调用委托给当前事务中的 EntityManager(如果存在的话);否则,它会在每次操作时回退到新创建的 EntityManager,
从而实际上使其使用方式具备线程安全性。 |
可以通过注入的 EntityManagerFactory 或 EntityManager 编写不依赖任何 Spring 组件的纯 JPA 代码。如果启用了 @PersistenceUnit,Spring 就能够识别字段级别和方法级别上的 @PersistenceContext 与 PersistenceAnnotationBeanPostProcessor 注解。以下示例展示了一个使用 @PersistenceUnit 注解的纯 JPA DAO 实现:
public class ProductDaoImpl implements ProductDao {
private EntityManagerFactory emf;
@PersistenceUnit
public void setEntityManagerFactory(EntityManagerFactory emf) {
this.emf = emf;
}
public Collection loadProductsByCategory(String category) {
try (EntityManager em = this.emf.createEntityManager()) {
Query query = em.createQuery("from Product as p where p.category = ?1");
query.setParameter(1, category);
return query.getResultList();
}
}
}
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(也称为“共享 EntityManager”,因为它是实际事务型 EntityManager 的一个共享、线程安全的代理)来避免这种情况,而不是注入工厂。以下示例展示了如何实现这一点:
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();
}
}
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 则完全不同。
它会生成所谓的扩展型(extended)EntityManager,该对象不是线程安全的,因此不能用于并发访问的组件中,
例如 Spring 管理的单例 Bean。扩展型 EntityManager 实例仅应被用于有状态的组件中,
例如位于会话(session)中的组件,其 EntityManager 的生命周期并不绑定到当前事务,
而是完全由应用程序自行控制。
注入的 EntityManager 是由 Spring 管理的(能够感知当前正在进行的事务)。
尽管新的 DAO 实现使用了方法级别的 EntityManager 注入,而不是 EntityManagerFactory,但由于使用了注解,应用程序上下文 XML 文件无需做任何更改。
这种 DAO 风格的主要优势在于它仅依赖于 Java Persistence API。 无需导入任何 Spring 类。此外,由于 Spring 容器能够识别 JPA 注解, 因此依赖注入会自动应用。从非侵入性的角度来看,这一点非常有吸引力, 对 JPA 开发者来说也感觉更加自然。
4.4.3. 由 Spring 驱动的 JPA 事务
| 我们强烈建议您阅读声明式事务管理(如果您尚未阅读),以更详细地了解 Spring 的声明式事务支持。 |
JPA 推荐的策略是通过 JPA 自带的事务支持来实现本地事务。Spring 的 JpaTransactionManager 针对任何常规的 JDBC 连接池(无需 XA 支持),提供了许多与本地 JDBC 事务相同的功能(例如事务特定的隔离级别和资源级别的只读优化)。
Spring JPA 还允许配置的 JpaTransactionManager 向访问相同 DataSource 的 JDBC 访问代码公开 JPA 事务,前提是所注册的 JpaDialect 支持获取底层的 JDBC Connection。
Spring 为 EclipseLink 和 Hibernate 的 JPA 实现提供了相应的方言(dialect)。
有关 #orm-jpa-dialect 机制的详细信息,请参见下一节。
作为一种直接的替代方案,从 Spring Framework 5.1 和 Hibernate 5.2/5.3/5.4 开始,Spring 原生的 HibernateTransactionManager 能够与 JPA 访问代码进行交互,适配多种 Hibernate 特性并提供 JDBC 交互支持。
这在与 LocalSessionFactoryBean 配置结合使用时尤为适用。
详情请参见用于 JPA 交互的原生 Hibernate 配置。 |
4.4.4. 理解JpaDialect和JpaVendorAdapter
作为一种高级特性,JpaTransactionManager 和 AbstractEntityManagerFactoryBean 的子类允许通过 JpaDialect bean 属性传入一个自定义的 jpaDialect。通过 JpaDialect 的实现,通常以特定于厂商的方式,可以启用 Spring 支持的以下高级特性:
-
应用特定的事务语义(例如自定义隔离级别或事务超时)
-
获取事务型 JDBC
Connection(用于暴露给基于 JDBC 的 DAO) -
将
PersistenceExceptions高级转换为 Spring 的DataAccessExceptions
这对于特殊的事务语义以及异常的高级转换尤其有价值。默认实现(DefaultJpaDialect)不提供任何特殊功能,如果需要前面列出的特性,则必须指定相应的方言。
作为一种更为通用的提供者适配机制,主要面向 Spring 功能完备的 LocalContainerEntityManagerFactoryBean 配置,JpaVendorAdapter 将 JpaDialect 的功能与其他特定 JPA 提供商的默认设置结合起来。分别指定 HibernateJpaVendorAdapter 或 EclipseLinkJpaVendorAdapter 是为 Hibernate 或 EclipseLink 自动配置 EntityManagerFactory 的最便捷方式。请注意,这些提供者适配器主要是为配合 Spring 管理的事务(即与 JpaTransactionManager 一起使用)而设计的。 |
请参阅 JpaDialect 和
JpaVendorAdapter 的 Javadoc,
以获取更多关于其操作及其在 Spring JPA 支持中如何使用的详细信息。
4.4.5. 使用 JTA 事务管理设置 JPA
作为 JpaTransactionManager 的替代方案,Spring 还允许通过 JTA 实现多资源事务协调,无论是在 Java EE 环境中,还是使用独立的事务协调器(例如 Atomikos)。除了选择 Spring 的 JtaTransactionManager 而非 JpaTransactionManager 之外,你还需要执行以下几个额外步骤:
-
底层的 JDBC 连接池需要支持 XA,并且必须与您的事务协调器集成。这在 Java EE 环境中通常很简单,只需通过 JNDI 暴露一种不同类型的
DataSource即可。具体细节请参阅您的应用服务器文档。类似地,独立的事务协调器通常也会提供特殊的、已集成 XA 的DataSource变体。同样,请查阅其相关文档。 -
JPA 的
EntityManagerFactory设置需要针对 JTA 进行配置。这是特定于 JPA 提供商的,通常通过在jpaProperties上指定LocalContainerEntityManagerFactoryBean来设置一些特殊属性。以 Hibernate 为例,这些属性甚至与 Hibernate 的版本相关。详情请参阅 Hibernate 的官方文档。 -
Spring 的
HibernateJpaVendorAdapter强制使用某些面向 Spring 的默认设置,例如连接释放模式on-close,该模式在 Hibernate 5.0 中与 Hibernate 自身的默认值一致,但在 Hibernate 5.1 及更高版本中则不再一致。对于 JTA 配置,请确保将您的持久化单元事务类型声明为 "JTA"。或者,也可以将 Hibernate 5.2 的hibernate.connection.handling_mode属性设置为DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT,以恢复 Hibernate 自身的默认行为。相关说明请参见Hibernate 导致的应用服务器误报警告。 -
或者,考虑直接从您的应用服务器获取
EntityManagerFactory(即通过 JNDI 查找,而不是使用本地声明的LocalContainerEntityManagerFactoryBean)。服务器提供的EntityManagerFactory可能需要在服务器配置中进行特殊定义(这会降低部署的可移植性),但已针对服务器的 JTA 环境进行了配置。
4.4.6. 原生 Hibernate 配置及用于 JPA 交互的原生 Hibernate 事务
从 Spring Framework 5.1 和 Hibernate 5.2/5.3/5.4 起,原生的 LocalSessionFactoryBean 配置结合 HibernateTransactionManager 可以与 @PersistenceContext 注解及其他 JPA 访问代码进行交互。Hibernate 的 SessionFactory 现在原生实现了 JPA 的 EntityManagerFactory 接口,而 Hibernate 的 Session 句柄本身就是一个原生的 JPA EntityManager。
Spring 的 JPA 支持设施会自动检测原生的 Hibernate Session。
因此,在许多场景中,这种原生的 Hibernate 配置可以替代标准 JPA 的 LocalContainerEntityManagerFactoryBean 与 JpaTransactionManager 组合,允许在同一本地事务中同时使用 SessionFactory.getCurrentSession()(以及 HibernateTemplate)和 @PersistenceContext EntityManager。这种配置还提供了更紧密的 Hibernate 集成和更高的配置灵活性,因为它不受 JPA 启动契约的约束。
在这种情况下,您不需要配置 HibernateJpaVendorAdapter,
因为 Spring 原生的 Hibernate 设置提供了更多功能
(例如,自定义 Hibernate Integrator 配置、Hibernate 5.3 与 Bean 容器的集成,
以及对只读事务的更强优化)。最后但同样重要的是,您也可以通过 LocalSessionFactoryBuilder
来表达原生的 Hibernate 设置,
从而无缝地与 @Bean 风格的配置集成(无需使用 FactoryBean)。
|
在 |
5. 使用对象 -XML 映射器编组 XML
5.1. 简介
本章介绍 Spring 对对象-XML 映射(Object-XML Mapping)的支持。对象-XML 映射(简称 O-X 映射)是指在 XML 文档与对象之间进行相互转换的操作。该转换过程也被称为 XML 编组(XML Marshalling)或 XML 序列化(XML Serialization)。本章中这些术语可互换使用。
在对象-XML映射(O-X mapping)领域中,marshaller 负责将对象(图)序列化为 XML。类似地,unmarshaller 则将 XML 反序列化为对象图。这种 XML 可以采用 DOM 文档、输入或输出流,或者 SAX 处理器的形式。
使用 Spring 来满足您的对象/XML(O/X)映射需求的一些优势包括:
5.1.1. 配置简便性
Spring 的 bean 工厂使得配置编组器(marshallers)变得非常简单,无需手动构建 JAXB 上下文、JiBX 绑定工厂等。您可以像配置应用程序上下文中的其他 bean 一样来配置这些编组器。此外,多种编组器还支持基于 XML 命名空间的配置方式,使配置更加简便。
5.1.2. 一致的接口
Spring 的 O-X 映射通过两个全局接口运作:Marshaller 和
Unmarshaller。这些抽象让您能够相对轻松地切换 O-X 映射框架,而执行编组的类几乎无需更改或完全无需更改。这种方法还有一个额外的好处,即可以以非侵入的方式采用混合搭配的策略进行 XML 编组(例如,部分编组使用 JAXB 完成,另一部分使用 XStream 完成),从而让您能够充分利用每项技术的优势。
5.2. Marshaller和Unmarshaller
正如介绍中所述,编组器(marshaller)将对象序列化为 XML,而解组器(unmarshaller)将 XML 流反序列化为对象。本节介绍了 Spring 中用于此目的的两个接口。
5.2.1. 理解Marshaller
Spring 将所有编组(marshalling)操作抽象到 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;
}
interface Marshaller {
/**
* Marshal the object graph with the given root into the provided Result.
*/
@Throws(XmlMappingException::class, IOException::class)
fun marshal(
graph: Any,
result: Result
)
}
Marshaller 接口有一个主要方法,该方法将给定对象编组(marshal)到指定的 javax.xml.transform.Result 中。2 是一个标记接口,基本上代表了一种 XML 输出的抽象。具体的实现类封装了各种 XML 表示形式,如下表所示:
| 结果实现 | 包装 XML 表示形式 |
|---|---|
|
|
|
|
|
|
尽管 marshal() 方法接受一个普通对象作为其第一个参数,但大多数
Marshaller 实现无法处理任意对象。相反,对象类必须在映射文件中进行映射、
通过注解进行标记、向 marshaller 注册,或者具有一个公共基类。请参阅本章后续章节,
以了解您所使用的 O-X 技术如何管理这一点。 |
5.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;
}
interface Unmarshaller {
/**
* Unmarshal the given provided Source into an object graph.
*/
@Throws(XmlMappingException::class, IOException::class)
fun unmarshal(source: Source): Any
}
该接口还有一个方法,用于从给定的javax.xml.transform.Source(一种XML输入抽象)中读取数据并返回所读取的对象。与Result类似,Source也是一个标记接口,具有三个具体实现。每个实现封装了不同的XML表示形式,如下表所示:
| 源码实现 | 包装 XML 表示形式 |
|---|---|
|
|
|
|
|
|
尽管存在两个独立的编组接口(Marshaller 和
Unmarshaller),但 Spring-WS 中的所有实现都在同一个类中同时实现了这两个接口。
这意味着你可以在 applicationContext.xml 中配置一个编组器类,并同时将其用作
编组器和解编组器。
5.3. 使用Marshaller和Unmarshaller
您可以将 Spring 的 OXM 用于各种场景。在以下示例中, 我们使用它将由 Spring 管理的应用程序的配置设置编组(marshal)为一个 XML 文件。在以下示例中, 我们使用一个简单的 JavaBean 来表示这些设置:
public class Settings {
private boolean fooEnabled;
public boolean isFooEnabled() {
return fooEnabled;
}
public void setFooEnabled(boolean fooEnabled) {
this.fooEnabled = fooEnabled;
}
}
class Settings {
var isFooEnabled: Boolean = false
}
应用程序类使用此 bean 来存储其设置。除了 main 方法外,
该类还包含两个方法:saveSettings() 将设置 bean 保存到名为
settings.xml 的文件中,而 loadSettings() 则重新加载这些设置。以下的 main() 方法
构建了一个 Spring 应用上下文并调用这两个方法:
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();
}
}
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 需要同时设置 marshaller 和 unmarshaller 属性。我们可以通过以下 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>
此应用上下文使用了 XStream,但我们也可以使用本章后面介绍的其他任意 marshaller 实例。请注意,默认情况下,XStream 无需任何额外配置,因此该 bean 的定义相当简单。另外请注意,XStreamMarshaller 同时实现了 Marshaller 和 Unmarshaller 接口,因此我们可以在应用的 xstreamMarshaller 和 marshaller 属性中都引用 unmarshaller 这个 bean。
此示例应用程序会生成以下 settings.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>
5.4. XML 配置命名空间
您可以通过使用 OXM 命名空间中的标签更简洁地配置编组器(marshallers)。 要使用这些标签,您必须首先在 XML 配置文件的开头部分引用相应的 schema。 以下示例展示了如何进行此操作:
<?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 | 引用 oxm 模式。 |
| 2 | 指定 oxm 的 schema 位置。 |
该模式提供了以下元素:
每个标签都在其对应的编组器(marshaller)部分中进行说明。例如,JAXB2 编组器的配置可能如下所示:
<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>
5.5. JAXB
JAXB绑定编译器将W3C XML Schema转换为一个或多个Java类、一个jaxb.properties文件,以及可能的一些资源文件。JAXB还提供了一种从带注解的Java类生成Schema的方法。
Spring 支持 JAXB 2.0 API 作为 XML 编组策略,遵循 Marshaller 和 Unmarshaller 中描述的 Marshaller 和 Unmarshaller 接口。
相应的集成类位于 org.springframework.oxm.jaxb 包中。
5.5.1. 使用Jaxb2Marshaller
Jaxb2Marshaller 类同时实现了 Spring 的 Marshaller 和 Unmarshaller 接口。它需要一个上下文路径(context path)才能运行。您可以通过设置 contextPath 属性来指定该上下文路径。上下文路径是一个由冒号分隔的 Java 包名列表,这些包中包含由 schema 生成的类。此外,它还提供了一个 classesToBeBound 属性,允许您设置一个由 marshaller 支持的类数组。通过向 bean 指定一个或多个 schema 资源,即可执行 schema 验证,如下例所示:
<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 子元素来提供要绑定到编组器(marshaller)的类列表:
<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>
下表描述了可用的属性:
| 属性 | <description> </description> | 必填 |
|---|---|---|
|
编组器的 ID |
No |
|
JAXB 上下文路径 |
No |
5.6. JiBX
JiBX 框架提供了一种类似于 Hibernate 为 ORM 所提供的解决方案:绑定定义规定了 Java 对象与 XML 之间相互转换的规则。在准备好绑定并编译类之后,JiBX 绑定编译器会增强这些类文件,并添加用于处理类实例与 XML 之间相互转换的代码。
有关 JiBX 的更多信息,请参见 JiBX 网站。Spring 集成类位于 org.springframework.oxm.jibx 包中。
5.6.1. 使用JibxMarshaller
JibxMarshaller 类同时实现了 Marshaller 和 Unmarshaller 接口。要正常工作,它需要指定要编组的类名,你可以通过设置 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 是为单个类配置的。如果你想编组多个类,则必须配置多个 JibxMarshaller 实例,并为它们的 targetClass 属性设置不同的值。
XML 配置命名空间
jibx-marshaller 标签用于配置一个 org.springframework.oxm.jibx.JibxMarshaller,如下例所示:
<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>
下表描述了可用的属性:
| 属性 | <description> </description> | 必填 |
|---|---|---|
|
编组器的 ID |
No |
|
此编组器的目标类 |
是的 |
|
此编组器所使用的绑定名称 |
No |
5.7. XStream
XStream 是一个简单的库,用于将对象序列化为 XML 并可反向还原。它无需任何映射配置,并能生成简洁的 XML。
有关 XStream 的更多信息,请参见 XStream
官方网站。Spring 集成类位于
org.springframework.oxm.xstream 包中。
5.7.1. 使用XStreamMarshaller
XStreamMarshaller 无需任何配置,可直接在应用上下文中进行配置。若要进一步自定义 XML,您可以设置一个别名映射(alias map),该映射由字符串别名与类之间的对应关系组成,如下例所示:
<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 序列化后果。因此,我们不建议使用 如果你选择使用
这样做可确保只有已注册的类才有资格进行反序列化。 此外,您可以注册自定义转换器,以确保只有您支持的类才能被反序列化。除了显式支持应被允许的领域类的转换器外,您可能还希望在转换器列表末尾添加一个 |
| 请注意,XStream 是一个 XML 序列化库,而不是数据绑定库。 因此,它对命名空间的支持有限。结果,它不太适合在 Web 服务中使用。 |
6. 附录
6.1. XML 模式
本附录的这一部分列出了数据访问相关的 XML Schema,包括以下内容:
6.1.1. 该tx架构
tx 标签用于配置 Spring 对事务的全面支持中所涉及的所有这些 Bean。这些标签在题为
事务管理 的章节中有详细介绍。
我们强烈建议您查看 Spring 发行版中附带的 'spring-tx.xsd' 文件。该文件包含了 Spring 事务配置的 XML Schema,涵盖了 tx 命名空间中的所有元素,包括属性默认值等相关信息。该文件已在内部进行了详细注释,因此为了遵循 DRY(Don’t Repeat Yourself,不要重复自己)原则,此处不再重复这些信息。 |
为了内容的完整性,若要使用 tx 命名空间中的元素,您需要在 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 命名空间中的元素。 |
6.1.2. 简介jdbc架构
jdbc 元素可让您快速配置嵌入式数据库或初始化现有的数据源。这些元素分别在嵌入式数据库支持和初始化 DataSource中进行了说明。
要使用 jdbc 命名空间中的元素,您需要在 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 位置一起)。 |