Data Access

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

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

1.TransactionManagement

全面的事务支持是使用 Spring Framework 的最令人信服的原因。 Spring 框架为事务 Management 提供了一致的抽象,具有以下优点:

以下各节描述了 Spring 框架的事务功能和技术:

(本章还讨论了最佳做法应用服务器集成常见问题的解决方案。)

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

传统上,Java EE 开发人员在事务 Management 中有两种选择:全局或本地事务,这两者都有很大的局限性。下两节将回顾全局和本地事务 Management,然后讨论 Spring 框架的事务 Management 支持如何解决全局和本地事务模型的局限性。

1.1.1. GlobalTransaction

全局事务使您可以使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTAManagement 全局事务,该 JTA 是繁琐的 API(部分是由于其异常模型)。此外,通常需要从 JNDI 派生 JTA UserTransaction,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。

以前,使用全局事务的首选方法是通过 EJB CMT(容器 Management 的事务)。 CMT 是声明式事务 Management 的一种形式(与程序性事务 Management 不同)。 EJB CMT 消除了与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身必须使用 JNDI。它消除了编写 Java 代码来控制事务的大部分(但不是全部)需求。重大缺点是 CMT 与 JTA 和应用程序服务器环境相关联。而且,仅当选择在 EJB 中(或至少在事务性 EJB 幕后之后)实现业务逻辑时,此功能才可用。通常,EJB 的缺点很大,以至于这不是一个有吸引力的主张,尤其是面对声明式事务 Management 的强制选择时。

1.1.2. 本地 Transaction

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

1.1.3. Spring 框架的一致编程模型

Spring 解决了 Global 和本地 Transaction 的弊端。它使应用程序开发人员可以在任何环境中使用一致的编程模型。您只需编写一次代码,即可从不同环境中的不同事务 Management 策略中受益。 Spring 框架提供了声明式和程序化事务 Management。大多数用户喜欢声明式事务 Management,在大多数情况下我们建议这样做。

通过程序化事务 Management,开发人员可以使用 Spring Framework 事务抽象,该抽象可以在任何基础事务基础架构上运行。使用首选的声明性模型,开发人员通常很少或几乎不编写与事务 Management 相关的代码,因此,它们不依赖于 Spring Framework 事务 API 或任何其他事务 API。

您需要用于事务 Management 的应用服务器吗?

Spring Framework 的事务 Management 支持更改了有关企业 Java 应用程序何时需要应用程序服务器的传统规则。

特别是,您不需要纯粹用于通过 EJB 进行声明式事务的应用程序服务器。实际上,即使您的应用服务器具有强大的 JTA 功能,您也可以决定 Spring 框架的声明式事务比 EJB CMT 提供更多的功能和更高效的编程模型。

通常,仅当您的应用程序需要处理跨多个资源的事务时才需要应用程序服务器的 JTA 功能,而这并不是许多应用程序所必需的。许多高端应用程序使用单个高度可扩展的数据库(例如 Oracle RAC)来代替。独立事务 Management 器(例如Atomikos TransactionsJOTM)是其他选项。当然,您可能需要其他应用程序服务器功能,例如 Java 消息服务(JMS)和 Java EE 连接器体系结构(JCA)。

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

1.2. 了解 Spring 框架事务抽象

Spring 事务抽象的关键是事务策略的概念。 org.springframework.transaction.PlatformTransactionManager界面定义了一种 Transaction 策略,以下 Lists 显示了该策略:

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

尽管您可以从应用程序代码中使用programmatically,但它主要是服务提供商接口(SPI)。由于PlatformTransactionManager是一个接口,因此可以根据需要轻松对其进行模拟或存根。它与诸如 JNDI 之类的查找策略无关。 PlatformTransactionManager实现的定义与 Spring Framework IoC 容器中的任何其他对象(或 bean)一样。单独使用此好处,即使使用 JTA,Spring 框架事务也成为有价值的抽象。与直接使用 JTA 相比,您可以更轻松地测试事务代码。

同样,根据 Spring 的哲学,未选中可由PlatformTransactionManager接口的任何方法抛出的TransactionException(即,它扩展了java.lang.RuntimeException类)。Transaction 基础架构故障几乎总是致命的。在极少数情况下,应用程序代码实际上可以从事务失败中恢复,应用程序开发人员仍然可以选择捕获并处理TransactionException。突出的一点是,不强制开发人员这样做。

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

TransactionDefinition接口指定:

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

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

public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}

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

PlatformTransactionManager实现通常需要了解其工作环境:JDBC,JTA,Hibernate 等。以下示例显示了如何定义本地PlatformTransactionManager实现(在这种情况下,使用纯 JDBC)。

您可以通过创建类似于以下内容的 bean 来定义 JDBC DataSource

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

然后,相关的PlatformTransactionManager bean 定义引用了DataSource定义。它应类似于以下示例:

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

如果在 Java EE 容器中使用 JTA,则将通过 JNDI 获得的DataSource容器与 Spring 的JtaTransactionManager一起使用。以下示例显示了 JTA 和 JNDI 查找版本的外观:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd">

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

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

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

</beans>

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

Note

dataSource bean 的先前定义使用jee名称空间中的<jndi-lookup/>标记。有关更多信息,请参见JEE 模式

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

DataSource bean 定义与先前显示的本地 JDBC 示例相似,因此在以下示例中未显示。

Note

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

在这种情况下,txManager bean 是HibernateTransactionManager类型。就像DataSourceTransactionManager需要引用DataSource一样,HibernateTransactionManager也需要引用SessionFactory。以下示例声明sessionFactorytxManager bean:

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

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

如果您使用 Hibernate 和 Java EE 容器 Management 的 JTA 事务,则应使用与前面的 JDBC JTA 示例相同的JtaTransactionManager,如以下示例所示:

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

Note

如果您使用 JTA,则无论使用哪种数据访问技术(无论是 JDBC,Hibernate JPA 或任何其他受支持的技术),事务 Management 器定义都应该看起来相同。这是由于 JTA 事务是全局事务,它可以征用任何事务资源。

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

1.3. 将资源与事务同步

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

1.3.1. 高级同步方法

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

1.3.2. 低级同步方法

诸如DataSourceUtils(对于 JDBC),EntityManagerFactoryUtils(对于 JPA),SessionFactoryUtils(对于 Hibernate)等类存在于较低级别。当您希望应用程序代码直接处理本机持久性 API 的资源类型时,可以使用这些类来确保获得正确的 Spring FrameworkManagement 的实例,事务(可选)同步,并且流程中发生的异常是正确 Map 到一致的 API。

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

Connection conn = DataSourceUtils.getConnection(dataSource);

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

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

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

1.3.3. TransactionAwareDataSourceProxy

最低级别是TransactionAwareDataSourceProxy类。这是目标DataSource的代理,该代理包装了目标DataSource以增加对 SpringManagement 的事务的了解。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource

您几乎永远不需要或不想使用此类,除非必须调用现有代码并通过标准的 JDBC DataSource接口实现。在这种情况下,该代码可能可用,但参与了 SpringManagement 的事务。您可以使用前面提到的高级抽象来编写新代码。

1.4. 声明式 TransactionManagement

Note

大多数 Spring Framework 用户选择声明式事务 Management。此选项对应用程序代码的影响最小,因此与无创轻量级容器的 IDEA 最一致。

Spring 面向方面的编程(AOP)使 Spring 框架的声明式事务 Management 成为可能。但是,由于事务方面的代码随 Spring 框架发行版一起提供并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。

Spring 框架的声明式事务 Management 与 EJB CMT 相似,因为您可以指定事务行为(或缺少事务行为),直至单个方法级别。如有必要,您可以在事务上下文中进行setRollbackOnly()调用。两种类型的事务 Management 之间的区别是:

TransactionProxyFactoryBean 在哪里?

Spring 2.0 及更高版本中的声明式事务配置与 Spring 的早期版本有很大不同。主要区别在于不再需要配置TransactionProxyFactoryBean bean。

Spring 2.0 之前的配置样式仍然是 100%有效的配置。将新的<tx:tags/>视为代表您定义的TransactionProxyFactoryBean bean。

回滚规则的概念很重要。他们让您指定哪些异常(和可抛出对象)应引起自动回滚。您可以在配置中而不是在 Java 代码中声明性地指定。因此,尽管您仍然可以在TransactionStatus对象上调用setRollbackOnly()来回滚当前事务,但大多数情况下,您可以指定MyApplicationException必须始终导致回滚的规则。此选项的主要优点是业务对象不依赖于事务基础结构。例如,他们通常不需要导入 Spring 事务 API 或其他 Spring API。

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

1.4.1. 了解 Spring 框架的声明式事务实现

仅仅告诉您使用@TransactionalComments 对类进行 Comments,将@EnableTransactionManagement添加到您的配置中,并希望您了解其全部工作原理是不够的。为了提供更深入的理解,本节介绍了在发生与事务相关的问题时,Spring 框架的声明式事务基础结构的内部工作方式。

关于 Spring Framework 的声明式事务支持,要把握的最重要的概念是启用此支持通过 AOP 代理,并且事务通知由元数据(当前基于 XML 或基于 Comments)驱动。 AOP 与事务元数据的结合产生了一个 AOP 代理,该代理使用TransactionInterceptor结合适当的PlatformTransactionManager实现来驱动方法调用周围的事务。

Note

Spring AOP 包含在AOP 部分中。

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

1.4.2. 声明式事务实现示例

考虑以下接口及其附带的实现。本示例使用FooBar类作为占位符,以便您可以专注于事务使用而不关注特定的域模型。就本示例而言,DefaultFooService类在每个已实现方法的主体中引发UnsupportedOperationException实例的事实是很好的。该行为使您可以看到已创建事务,然后响应UnsupportedOperationException实例而回滚了事务。以下 Lists 显示了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);

}

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

package x.y.service;

public class DefaultFooService implements FooService {

    public Foo getFoo(String fooName) {
        throw new UnsupportedOperationException();
    }

    public Foo getFoo(String fooName, String barName) {
        throw new UnsupportedOperationException();
    }

    public void insertFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

    public void updateFoo(Foo foo) {
        throw new UnsupportedOperationException();
    }

}

假定FooService接口的前两个方法getFoo(String)getFoo(String, String)必须在具有只读语义的事务上下文中执行,而其他方法insertFoo(Foo)updateFoo(Foo)必须在具有 read-语义的事务上下文中执行写语义。以下几节将详细说明以下配置:

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

    <!-- 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 PlatformTransactionManager -->
    <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开始的所有方法都将在只读事务的上下文中执行,而所有其他方法将以默认的事务语义执行”。 <tx:advice/>标记的transaction-manager属性设置为将要驱动事务的PlatformTransactionManager bean 的名称(在本例中为txManager bean)。

Tip

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

<aop:config/>定义可确保txAdvice bean 定义的事务建议在程序的适当位置执行。首先,定义一个切入点,该切入点与FooService接口(fooServiceOperation)中定义的任何操作的执行相匹配。然后,使用顾问将切入点与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>

Note

在前面的示例中,假定您的所有服务接口都在x.y.service包中定义。有关更多详细信息,请参见AOP 部分

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

前面显示的配置用于围绕从fooService bean 定义创建的对象创建事务代理。代理配置有事务建议,以便在代理上调用适当的方法时,根据与该方法相关联的事务配置,事务将被启动,挂起,标记为只读等。考虑下面的程序,该程序测试驱动前面显示的配置:

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml", Boot.class);
        FooService fooService = (FooService) ctx.getBean("fooService");
        fooService.insertFoo (new Foo());
    }
}

运行先前程序的输出应类似于以下内容(为清晰起见,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 [[emailprotected]] 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 [[emailprotected]]
[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)

1.4.3. 回滚声明式事务

上一节概述了如何在应用程序中声明性地指定类(通常是服务层类)的事务设置的基础。本节介绍如何以一种简单的声明性方式控制事务的回滚。

向 Spring Framework 的事务基础结构指示要回滚事务的推荐方法是从事务上下文中当前正在执行的代码中抛出Exception。 Spring 框架的事务基础结构代码会捕获未处理的Exception,因为它会使调用栈冒泡,并确定是否将事务标记为回滚。

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

您可以准确配置哪些Exception类型将事务标记为回滚,包括已检查的异常。以下 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 Framework 的事务基础结构捕获到异常并咨询已配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则获胜。因此,在以下配置的情况下,除InstrumentNotFoundException之外的任何异常都会导致附带事务的回滚:

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

您还可以通过编程方式指示所需的回滚。尽管很简单,但是此过程具有很大的侵入性,并将您的代码紧密耦合到 Spring Framework 的事务基础结构。下面的示例演示如何以编程方式指示所需的回滚:

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

强烈建议您尽可能使用声明性方法进行回滚。如果您绝对需要它,则可以使用程序化回滚,但是面对一个干净的基于 POJO 的体系结构,它的用法就不那么理想了。

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

考虑以下场景:您有许多服务层对象,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义具有不同pointcutadvice-ref属性值的不同<aop:advisor/>元素来做到这一点。

作为比较,首先假定所有服务层类都在根x.y.service包中定义。要使所有在该包(或子包)中定义的类实例且名称以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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

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

    <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 PlatformTransactionManager omitted... -->

</beans>

1.4.5. <tx:advice/>设置

本节总结了您可以使用<tx:advice/>标签指定的各种事务设置。默认的<tx:advice/>设置为:

您可以更改这些默认设置。下表总结了嵌套在<tx:advice/><tx:attributes/>标签中的<tx:method/>标签的各种属性:

表 1.<tx:method/>设置

Attribute Required? Default Description
name Yes 与事务属性关联的方法名称。通配符(*)可用于将相同的事务属性设置与多种方法(例如get*handle*on*Event等)相关联。
propagation No REQUIRED 事务传播行为。
isolation No DEFAULT 事务隔离级别。仅适用于REQUIREDREQUIRES_NEW的传播设置。
timeout No -1 事务超时(秒)。仅适用于传播REQUIREDREQUIRES_NEW
read-only No false 读写与只读事务。仅适用于REQUIREDREQUIRES_NEW
rollback-for No 触发回滚的Exception个实例的逗号分隔列表。例如com.foo.MyBusinessException,ServletException.
no-rollback-for No 逗号分隔的Exception个实例列表,不会触发回滚。例如com.foo.MyBusinessException,ServletException.

1.4.6. 使用@Transactional

除了基于 XML 的声明式方法进行事务配置外,还可以使用基于 Comments 的方法。直接在 Java 源代码中声明事务语义会使声明更接近受影响的代码。不存在过度耦合的危险,因为原本打算以事务方式使用的代码几乎总是以这种方式部署。

Note

还支持使用标准的javax.transaction.TransactionalComments 来代替 Spring 自己的 Comments。请参阅 JTA 1.2 文档以获取更多详细信息。

使用@Transactional注解提供的易用性将通过一个示例得到最好的说明,下面将对此进行说明。考虑以下类定义:

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

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);
}

在上面的类级别使用,Comments 指示声明类(及其子类)的所有方法的默认值。另外,每种方法都可以单独 Comments。注意,类级别的 Comments 不适用于类层次结构中的祖先类。在这种情况下,需要在本地重新声明方法,以参与子类级别的 Comments。

当一个 POJO 类(例如上面的一个)在 Spring 上下文中定义为 bean 时,您可以通过@Configuration类中的@EnableTransactionManagementComments 使 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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 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 -->
    <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required --> (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>

Tip

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

方法可见性和@Transactional

使用代理时,应仅将@TransactionalComments 应用于具有公开可见性的方法。如果使用@TransactionalComments 对受保护的,私有的或程序包可见的方法进行 Comments,则不会引发任何错误,但是带 Comments 的方法不会显示已配置的事务设置。如果需要 Comments 非公共方法,请考虑使用 AspectJ(稍后描述)。

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

Tip

Spring 团队建议您仅使用@TransactionalComments 对具体类(以及具体类的方法)进行 Comments,而不是对接口进行 Comments。您当然可以在接口(或接口方法)上放置@Transactional注解,但这仅在您使用基于接口的代理时才可以预期地起作用。 Java 注解不从接口继承的事实意味着,如果您使用基于类的代理(proxy-target-class="true")或基于编织的方面(mode="aspectj"),则代理和编织基础结构无法识别事务设置,并且该对象是没有包装在 Transaction 代理中。

Note

在代理模式(默认设置)下,仅拦截通过代理传入的外部方法调用。这意味着即使调用的方法标记为@Transactional,自调用(实际上是目标对象内的方法调用目标对象的另一种方法)也不会导致实际事务。另外,必须完全初始化代理以提供预期的行为,因此您不应在初始化代码(即@PostConstruct)中依赖此功能。

如果您希望自调用也与事务包装在一起,请考虑使用 AspectJ 模式(请参见下表中的mode属性)。在这种情况下,首先没有代理。而是编织目标类(即,修改其字节码),以将@Transactional转换为任何方法上的运行时行为。

表 2.Comments 驱动的事务设置

XML Attribute Annotation Attribute Default Description
transaction-manager 不适用(请参见TransactionManagementConfigurer javadoc) transactionManager 要使用的事务 Management 器的名称。如上例所示,仅当事务 Management 器的名称不是transactionManager时才需要。
mode mode proxy 缺省模式(proxy)使用 Spring 的 AOP 框架处理要 Comments 的 bean(遵循代理语义,如前所述,仅适用于通过代理传入的方法调用)。替代模式(aspectj)则使用 Spring 的 AspectJ 事务方面来编织受影响的类,修改目标类字节码以应用于任何类型的方法调用。 AspectJ 编织在 Classpath 中需要spring-aspects.jar,并且启用了加载时编织(或编译时编织)。 (有关如何设置加载时编织的详细信息,请参见Spring configuration。)
proxy-target-class proxyTargetClass false 仅适用于proxy模式。控制为带有@TransactionalComments 的类创建哪种类型的事务代理。如果proxy-target-class属性设置为true,则会创建基于类的代理。如果proxy-target-classfalse或省略了属性,则将创建基于标准 JDK 接口的代理。 (有关不同代理类型的详细检查,请参见Proxying mechanisms。)
order order Ordered.LOWEST_PRECEDENCE 定义应用于带有@TransactionalComments 的 bean 的事务通知的 Sequences。 (有关与 AOP 建议的排序有关的规则的更多信息,请参见Advice Ordering。)没有指定的排序意味着 AOP 子系统确定建议的 Sequences。

Note

处理@Transactional注解的默认建议模式是proxy,该模式仅允许通过代理拦截呼叫。同一类内的本地调用无法以这种方式被拦截。对于更高级的侦听模式,请考虑结合编译时或加载时编织切换到aspectj模式。

Note

proxy-target-class属性控制为使用@TransactionalComments 标注的类创建哪种类型的事务代理。如果proxy-target-class设置为true,则会创建基于类的代理。如果proxy-target-classfalse或省略了属性,则将创建基于标准 JDK 接口的代理。 (有关不同代理类型的讨论,请参见[aop-proxying]。)

Note

@EnableTransactionManagement<tx:annotation-driven/>仅在定义它们的相同应用程序上下文中的 Bean 上寻找@Transactional。这意味着,如果将 Comments 驱动的配置放在_6 的WebApplicationContext中,则仅在控制器而不是服务中检查@Transactional bean。有关更多信息,请参见MVC

在评估方法的事务设置时,最派生的位置优先。在下面的示例中,使用只读事务的设置在类级别 CommentsDefaultFooService类,但是同一类中updateFoo(Foo)方法上的@TransactionalComments 优先于在类级别定义的事务设置。

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

    public Foo getFoo(String fooName) {
        // do something
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // do something
    }
}
@Transactional Settings

@TransactionalComments 是元数据,它指定接口,类或方法必须具有事务语义(例如,“在调用此方法时启动一个全新的只读事务,并挂起任何现有事务”)。默认的@Transactional设置如下:

您可以更改这些默认设置。下表总结了@Transactional注解的各种属性:

表 3. @Transaction 设置

Property Type Description
value String 可选的限定词,指定要使用的事务 Management 器。
propagation enum : Propagation 可选的传播设置。
isolation enum : Isolation 可选的隔离级别。仅适用于REQUIREDREQUIRES_NEW的传播值。
timeout int(以秒为单位) 可选的事务超时。仅适用于REQUIREDREQUIRES_NEW的传播值。
readOnly boolean 读写与只读事务。仅适用于REQUIREDREQUIRES_NEW的值。
rollbackFor Class个对象的数组,必须从Throwable.派生 必须引起回滚的异常类的可选数组。
rollbackForClassName 类名数组。这些类必须源自Throwable. 必须引起回滚的异常类名称的可选数组。
noRollbackFor Class个对象的数组,必须从Throwable.派生 不能导致回滚的异常类的可选数组。
noRollbackForClassName String类名称的数组,必须从Throwable.派生 不能引起回滚的异常类名称的可选数组。

当前,您无法对事务名称进行显式控制,其中“名称”是指显示在事务监视器(如果适用)(例如,WebLogic 的事务监视器)和日志输出中的事务名称。对于声明性事务,事务名称始终是完全合格的类名称.事务建议类的方法名称。例如,如果BusinessService类的handlePayment(..)方法启动了事务,则事务的名称应为:com.example.BusinessService.handlePayment

具有@Transactional 的多个事务 Management 器

大多数 Spring 应用程序仅需要一个事务 Management 器,但是在某些情况下,您可能需要在一个应用程序中使用多个独立的事务 Management 器。您可以使用@Transactional注解的value属性来指定要使用的PlatformTransactionManager的标识。这可以是 bean 名称,也可以是事务 Management 器 bean 的限定符值。例如,使用限定符表示法,可以在应用程序上下文中将以下 Java 代码与以下事务 Management 器 bean 声明进行组合:

public class TransactionalService {

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

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

以下 Lists 显示了 bean 声明:

<tx:annotation-driven/>

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

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

在这种情况下,TransactionalService上的两种方法在单独的事务 Management 器下运行,并以orderaccount限定词进行区分。如果未找到特别限定的PlatformTransactionManager bean,仍将使用默认的<tx:annotation-driven>目标 bean 名称transactionManager

自定义快捷方式 Comments

如果发现您在许多不同的方法上重复使用@Transactional的相同属性,则可以使用Spring 的元 Comments 支持为特定用例定义自定义快捷方式 Comments。例如,考虑以下 Comments 定义:

@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 {
}

前面的 Comments 使我们可以编写上一节中的示例,如下所示:

public class TransactionalService {

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

    @AccountTx
    public void doSomething() { ... }
}

在前面的示例中,我们使用了语法来定义事务 Management 器限定符,但是我们还可以包括传播行为,回滚规则,超时和其他功能。

1.4.7. Transaction 传播

本节描述了 Spring 中事务传播的一些语义。请注意,本节不是对事务传播的适当介绍。相反,它详细介绍了有关 Spring 中事务传播的一些语义。

在 SpringManagement 的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于此差异。

Understanding PROPAGATION_REQUIRED

PROPAGATION_REQUIRED强制执行物理事务,如果尚不存在,则在当前范围内本地执行,或参与为较大范围定义的现有“外部”事务。这是同一线程(例如,委派给几种存储库方法的服务立面,所有基础资源都必须参与服务级事务的服务立面)的优良默认设置。

Note

默认情况下,参与的事务将加入外部作用域的 Feature,而忽略本地隔离级别,超时值或只读标志(如果有)。如果要在参与具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务 Management 器上将validateExistingTransactions标志切换为true。这种非宽容模式还拒绝只读不匹配(即,内部读写事务试图参与只读外部作用域)。

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

但是,在内部事务范围设置仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围默默触发)是意外的。此时将抛出一个对应的UnexpectedRollbackException。这是预期的行为,因此事务调用者永远不会被误认为是在确实未执行提交的情况下进行的。因此,如果内部事务(外部调用者不知道)将事务无提示地标记为仅回滚,则外部调用者仍会调用 commit。外部呼叫者需要接收UnexpectedRollbackException来明确指示已执行回滚。

Understanding PROPAGATION_REQUIRES_NEW

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

Understanding PROPAGATION_NESTED

PROPAGATION_NESTED使用具有多个可还原到的保存点的单个物理事务。这种部分回滚使内部事务范围触发其范围的回滚,尽管某些操作已回滚,但外部事务仍能够 continue 物理事务。此设置通常 Map 到 JDBC 保存点,因此仅适用于 JDBC 资源事务。参见 Spring 的DataSourceTransactionManager

1.4.8. Transaction 事务咨询

假设您要执行事务性操作和一些基本的分析建议。您如何在<tx:annotation-driven/>的情况下实现此目的?

调用updateFoo(Foo)方法时,您想要查看以下操作:

Note

本章不涉及任何详细的 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;
    }
}

建议的排序是通过Ordered界面控制的。有关建议 Order 的完整详细信息,请参见Advice ordering

以下配置创建一个fooService bean,该 bean 具有按所需 Sequences 应用的概要分析和事务方面:

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

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

    <!-- this is the aspect -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- execute 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 will execute 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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

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

    <!-- the profiling advice -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- execute 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.*(..))"/>
        <!-- will execute 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 PlatformTransactionManager here -->

</beans>

先前配置的结果是一个fooService bean,该 bean 依次具有概要分析和事务方面的内容。如果您希望性能分析建议在进来的事务处理建议之后和之后的事务处理建议之前执行,则可以交换性能分析方面 Bean 的order属性的值,以使其高于事务处理建议的订单值。

您可以以类似方式配置其他方面。

1.4.9. 在 AspectJ 中使用@Transactional

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

Note

在 continue 之前,您可能需要分别阅读Using @TransactionalAOP

以下示例显示了如何创建事务 Management 器并配置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);

Note

使用此方面时,必须 Comments 实现类(或该类中的方法或两者),而不是 Comments 该类所实现的接口(如果有)。 AspectJ 遵循 Java 的规则,即不继承接口上的 Comments。

类上的@TransactionalComments 指定用于执行该类中任何公共方法的默认事务语义。

类中方法上的@TransactionalComments 将覆盖类 Comments(如果存在)给出的默认事务语义。您可以 Comments 任何方法,而不管可见性如何。

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

1.5. 程序化 TransactionManagement

Spring 框架通过使用以下两种方式提供程序化事务 Management 的方法:

Spring 团队通常建议使用TransactionTemplate进行程序化事务 Management。第二种方法类似于使用 JTA UserTransaction API,尽管异常处理不那么麻烦。

1.5.1. 使用 TransactionTemplate

TransactionTemplate采用与其他 Spring 模板(例如JdbcTemplate)相同的方法。它使用一种回调方法(使应用程序代码不必进行样板获取和释放事务性资源),并生成意向驱动的代码,因为您的代码仅专注于您要执行的操作。

Note

如以下示例所示,使用TransactionTemplate绝对可以使您与 Spring 的事务基础结构和 API 耦合。程序化事务 Management 是否适合您的开发需求是您必须自己做的决定。

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

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 executes in a transactional context
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}

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

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});

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

transactionTemplate.execute(new TransactionCallbackWithoutResult() {

    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});
指定 Transaction 设置

您可以以编程方式或配置方式在TransactionTemplate上指定事务设置(例如传播模式,隔离级别,超时等)。默认情况下,TransactionTemplate个实例具有默认 Transaction 设置。以下示例显示了针对特定TransactionTemplate:的 Transaction 设置的编程自定义

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...
    }
}

以下示例通过使用 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. 使用 PlatformTransactionManager

您也可以直接使用org.springframework.transaction.PlatformTransactionManager来 Management 您的 Transaction。为此,请通过 bean 引用将您使用的PlatformTransactionManager的实现传递给 bean。然后,通过使用TransactionDefinitionTransactionStatus对象,您可以启动事务,回滚和提交。以下示例显示了如何执行此操作:

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 {
    // execute your business logic here
}
catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);

1.6. 在程序性和声明性事务 Management 之间进行选择

仅当您执行少量 Transaction 操作时,程序 TransactionManagement 通常是一个好主意。例如,如果您的 Web 应用程序仅要求对某些更新操作进行事务处理,则可能不希望通过使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用TransactionTemplate可能是一种很好的方法。能够显式设置事务名称也是只能通过使用编程方法进行事务 Management 来完成的事情。

另一方面,如果您的应用程序具有大量事务操作,则声明式事务 Management 通常是值得的。它使事务 Management 脱离业务逻辑,并且不难配置。当使用 Spring 框架而不是 EJB CMT 时,声明式事务 Management 的配置成本大大降低了。

1.7. Transaction 绑定事件

从 Spring 4.2 开始,事件的侦听器可以绑定到事务的某个阶段。典型的示例是在事务成功完成后处理事件。这样,当当前事务的结果实际上对侦听器很重要时,便可以更加灵活地使用事件。

您可以使用@EventListener注解注册常规事件侦听器。如果需要将其绑定到事务,请使用@TransactionalEventListener。这样做时,默认情况下,侦听器绑定到事务的提交阶段。

下一个示例显示了此概念。假设某个组件发布了一个订单创建的事件,并且我们想要定义一个侦听器,该侦听器仅在发布该事件的事务成功提交后才应处理该事件。以下示例设置了这样的事件侦听器:

@Component
public class MyComponent {

    @TransactionalEventListener
    public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
        ...
    }
}

@TransactionalEventListenerComments 公开了phase属性,该属性使您可以自定义应将侦听器绑定到的事务阶段。有效阶段为BEFORE_COMMITAFTER_COMMIT(默认),AFTER_ROLLBACKAFTER_COMPLETION,这些阶段汇总事务完成(提交或回滚)。

如果没有事务在运行,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,您可以通过将 Comments 的fallbackExecution属性设置为true来覆盖该行为。

1.8. 应用服务器特定的集成

Spring 的事务抽象通常与应用程序服务器无关。此外,Spring 的JtaTransactionManager类(可以选择对 JTA UserTransactionTransactionManager对象执行 JNDI 查找)自动检测后一个对象的位置,该位置随应用程序服务器的不同而不同。可以访问 JTA TransactionManager来增强事务语义-特别是支持事务挂起。有关详细信息,请参见JtaTransactionManager javadoc。

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

对于包括 WebLogic Server 和 WebSphere 在内的标准方案,请考虑使用方便的<tx:jta-transaction-manager/>配置元素。配置后,此元素将自动检测基础服务器,并选择可用于平台的最佳事务 Management 器。这意味着您无需显式配置服务器特定的适配器类(如以下各节所述)。而是自动选择它们,并以标准JtaTransactionManager作为默认后备。

1.8.1. IBM WebSphere

在 WebSphere 6.1.0.9 和更高版本上,推荐使用的 Spring JTA 事务 Management 器是WebSphereUowTransactionManager。这个特殊的适配器使用 IBM 的UOWManager API,WebSphere Application Server 6.1.0.9 和更高版本中提供了该 API。使用此适配器,IBM 正式支持 Spring 驱动的事务挂起(由PROPAGATION_REQUIRES_NEW发起的挂起和 continue)。

1.8.2. Oracle WebLogic 服务器

在 WebLogic Server 9.0 或更高版本上,通常将使用WebLogicJtaTransactionManager而不是 stock JtaTransactionManager类。常规JtaTransactionManager的特定于 WebLogic 的特殊子类在标准 JTA 语义之外,支持 WebLogicManagement 的事务环境中 Spring 事务定义的全部功能。功能包括事务名称,每个事务的隔离级别以及在所有情况下正确恢复事务。

1.9. 常见问题的解决方案

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

1.9.1. 对特定的数据源使用错误的事务 Management 器

根据您选择的 Transaction 技术和要求,使用正确的PlatformTransactionManager实现。如果使用得当,Spring 框架仅提供了直接且可移植的抽象。如果使用全局事务,则必须对所有事务操作使用org.springframework.transaction.jta.JtaTransactionManager类(或其中的应用服务器特定的子类)。否则,事务基础结构将尝试在诸如容器DataSource实例之类的资源上执行本地事务。这样的本地事务是没有意义的,好的应用服务器会将它们视为错误。

1.10. 更多资源

有关 Spring 框架的事务支持的更多信息,请参见:

2. DAO 支持

Spring 对数据访问对象(DAO)的支持旨在使以一致的方式轻松使用数据访问技术(例如 JDBC,Hibernate 或 JPA)。这使您可以轻松地在上述持久性技术之间进行切换,还使您无需担心捕获每种技术特有的异常即可进行编码。

2.1. 一致的异常层次结构

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

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

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

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

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

确保您的数据访问对象(DAO)或存储库提供异常翻译的最佳方法是使用@Repository注解。此 Comments 还使组件扫描支持可以查找和配置 DAO 和存储库,而不必为其提供 XML 配置条目。以下示例显示了如何使用@Repository注解:

@Repository (1)
public class SomeMovieFinder implements MovieFinder {
    // ...
}

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

@Repository
public class JpaMovieFinder implements MovieFinder {

    @PersistenceContext
    private EntityManager entityManager;

    // ...

}

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

@Repository
public class HibernateMovieFinder implements MovieFinder {

    private SessionFactory sessionFactory;

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

    // ...

}

我们在此显示的最后一个示例是对典型 JDBC 支持的。您可以将DataSource注入到初始化方法中,在该方法中,您可以使用DataSource创建JdbcTemplate和其他数据访问支持类(例如SimpleJdbcCall等)。以下示例自动连接DataSource

@Repository
public class JdbcMovieFinder implements MovieFinder {

    private JdbcTemplate jdbcTemplate;

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

    // ...

}

Note

有关如何配置应用程序上下文以利用这些 Comments 的详细信息,请参见每种持久性技术的特定介绍。

3.使用 JDBC 进行数据访问

下表中概述的操作序列可能最好地显示了 Spring Framework JDBC 抽象提供的值。该表显示了 Spring 负责哪些操作,哪些操作是您的责任。

表 4. Spring JDBC-谁做什么?

Action Spring You
定义连接参数。 X
打开连接。 X
指定 SQL 语句。 X
声明参数并提供参数值 X
准备并执行该语句。 X
设置循环以遍历结果(如果有)。 X
进行每次迭代的工作。 X
处理任何异常。 X
Handle transactions. X
关闭连接,语句和结果集。 X

Spring 框架负责所有可能使 JDBC 成为乏味的 API 的低级细节。

3.1. 选择一种用于 JDBC 数据库访问的方法

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

3.2. 包层次结构

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

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

supportorg.springframework.jdbc.support包提供SQLException转换功能和一些 Util 类。 JDBC 处理期间引发的异常将转换为org.springframework.dao包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有翻译的异常均未选中,这使您可以选择捕获可从中恢复的异常,同时将其他异常传播到调用方。参见Using SQLExceptionTranslator

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

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

3.3.1. 使用 JdbcTemplate

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

在代码中使用JdbcTemplate时,只需实现回调接口,即可为它们明确定义 Contract。给定JdbcTemplate类提供的ConnectionPreparedStatementCreator回调接口将创建一条准备好的语句,提供 SQL 和任何必要的参数。 CallableStatementCreator接口(创建可调用语句)也是如此。 RowCallbackHandler接口从ResultSet的每一行提取值。

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

Note

DataSource应该始终配置为 Spring IoC 容器中的 bean。在第一种情况下,将 Bean 直接提供给服务。在第二种情况下,将其提供给准备好的模板。

此类发出的所有 SQL 都以DEBUG级别记录在与模板实例的标准类名相对应的类别下(通常为JdbcTemplate,但是如果使用JdbcTemplate类的自定义子类,则可能有所不同)。

以下各节提供了JdbcTemplate用法的一些示例。这些示例不是JdbcTemplate公开的所有功能的详尽列表。见服务员javadoc

Querying (SELECT)

以下查询获取关系中的行数:

int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);

以下查询使用绑定变量:

int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");

以下查询查找String

String lastName = this.jdbcTemplate.queryForObject(
        "select last_name from t_actor where id = ?",
        new Object[]{1212L}, String.class);

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

Actor actor = this.jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        new Object[]{1212L},
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

以下查询查找并填充许多域对象:

List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        new RowMapper<Actor>() {
            public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
                Actor actor = new Actor();
                actor.setFirstName(rs.getString("first_name"));
                actor.setLastName(rs.getString("last_name"));
                return actor;
            }
        });

如果最后两个代码段确实存在于同一应用程序中,则删除两个RowMapper匿名内部类中存在的重复并将它们提取到单个类(通常是static嵌套类)中,然后可以引用该重复是有意义的。根据需要使用 DAO 方法。例如,最好编写以下代码片段,如下所示:

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}

private static final class ActorMapper implements RowMapper<Actor> {

    public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
        Actor actor = new Actor();
        actor.setFirstName(rs.getString("first_name"));
        actor.setLastName(rs.getString("last_name"));
        return actor;
    }
}
使用 JdbcTemplate 更新(INSERT,UPDATE 和 DELETE)

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

下面的示例插入一个新条目:

this.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);

下面的示例删除一个条目:

this.jdbcTemplate.update(
        "delete from actor where id = ?",
        Long.valueOf(actorId));
其他 JdbcTemplate 操作

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

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

下面的示例调用一个存储过程:

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

covered later是更复杂的存储过程支持。

JdbcTemplate 最佳做法

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

使用JdbcTemplate类(和关联的NamedParameterJdbcTemplate类)的常见做法是在 Spring 配置文件中配置DataSource,然后将共享的DataSource bean 依赖注入到 DAO 类中。在DataSource的设置器中创建JdbcTemplate。这将导致类似于以下内容的 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...
}

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <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>

显式配置的替代方法是使用组件扫描和 Comments 支持进行依赖项注入。在这种情况下,可以用@RepositoryComments 该类(这使其成为组件扫描的候选对象),并用@AutowiredCommentsDataSource 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...
}

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

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

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

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

</beans>

如果使用 Spring 的JdbcDaoSupport类,并且各种 JDBC 支持的 DAO 类都从该类扩展,则您的子类将从JdbcDaoSupport类继承setDataSource(..)方法。您可以选择是否从此类继承。提供JdbcDaoSupport类只是为了方便。

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

3.3.2. 使用 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate类增加了对使用命名参数编程 JDBC 语句的支持,这与仅使用经典占位符('?')参数进行编程的 JDBC 相反。 NamedParameterJdbcTemplate类包装JdbcTemplate并委托包装的JdbcTemplate完成其许多工作。本节仅描述NamedParameterJdbcTemplate类的与JdbcTemplate本身不同的区域,即使用命名参数对 JDBC 语句进行编程。以下示例显示了如何使用NamedParameterJdbcTemplate

// 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);
}

请注意,在分配给sql变量的值和插入namedParameters变量(类型MapSqlParameterSource)的相应值中使用了命名参数符号。

或者,您可以使用基于Map的样式将命名参数及其对应的值传递给NamedParameterJdbcTemplate实例。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);
}

NamedParameterJdbcTemplate相关联(并且存在于同一 Java 包中)的一项不错的功能是SqlParameterSource接口。您已经在以前的代码片段之一(MapSqlParameterSource类)中看到了此接口的实现示例。 SqlParameterSourceNamedParameterJdbcTemplate的命名参数值的来源。 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...

}

下面的示例使用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);
}

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

另请参阅JdbcTemplate 最佳做法,以获取有关在应用程序上下文中使用NamedParameterJdbcTemplate类的准则。

3.3.3. 使用 SQLExceptionTranslator

SQLExceptionTranslator是要由可以在SQLExceptions和 Spring 自己的org.springframework.dao.DataAccessException之间进行转换的类实现的接口,而在数据访问策略方面则不可知。为了提高精度,实现可以是通用的(例如,使用 SQLState 代码用于 JDBC)或专有的(例如,使用 Oracle 错误代码)。

SQLErrorCodeSQLExceptionTranslatorSQLExceptionTranslator的实现,默认情况下使用。此实现使用特定的供应商代码。它比SQLState实现更为精确。错误代码转换基于 JavaBean 类型类SQLErrorCodes中保存的代码。此类由SQLErrorCodesFactory创建和填充,SQLErrorCodesFactory(顾名思义)是基于名为sql-error-codes.xml的配置文件的内容创建SQLErrorCodes的工厂。该文件使用供应商代码填充,并且基于DatabaseMetaData中的DatabaseProductName填充。使用您正在使用的实际数据库的代码。

SQLErrorCodeSQLExceptionTranslator按以下 Sequences 应用匹配规则:

Note

默认情况下,使用SQLErrorCodesFactory来定义Error代码和自定义异常翻译。在 Classpath 的名为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;
    }
}

在前面的示例中,特定的错误代码(-12345)被转换,而其他错误则由默认转换器实现转换。若要使用此自定义转换器,必须通过setExceptionTranslator方法将其传递给JdbcTemplate,并且必须将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);
}

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

3.3.4. 运行声明

运行 SQL 语句需要很少的代码。您需要DataSourceJdbcTemplate,包括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))");
    }
}

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);
    }
}

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

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");
}

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

[{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);
    }
}

在前面的示例中,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(
    new PreparedStatementCreator() {
        public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
            PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] {"id"});
            ps.setString(1, name);
            return ps;
        }
    },
    keyHolder);

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

3.4. 控制数据库连接

本节内容包括:

3.4.1. 使用数据源

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

使用 Spring 的 JDBC 层时,您可以从 JNDI 获取数据源,也可以使用第三方提供的连接池实现来配置自己的数据源。流行的实现是 Apache Jakarta Commons DBCP 和 C3P0. Spring 发行版中的实现仅用于测试目的,不提供池化。

本节使用 Spring 的DriverManagerDataSource实现,稍后将介绍其他一些实现。

Note

您只能将DriverManagerDataSource类用于测试目的,因为它不提供缓冲池,并且在发出多个连接请求时性能不佳。

要配置DriverManagerDataSource

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

DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");

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

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

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

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

以下示例显示了 DBCP 配置:

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

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

以下示例显示了 C3P0 配置:

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

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

3.4.2. 使用 DataSourceUtils

DataSourceUtils类是一种方便且功能强大的帮助器类,它提供static个方法以从 JNDI 获取连接并在必要时关闭连接。它支持例如DataSourceTransactionManager的线程绑定连接。

3.4.3. 实施 SmartDataSource

SmartDataSource接口应该由可以提供与关系数据库的连接的类来实现。它扩展了DataSource接口,以使使用它的类查询给定操作后是否应关闭连接。当您知道需要重用连接时,这种用法很有效。

3.4.4. 扩展 AbstractDataSource

AbstractDataSource是 Spring 的DataSource实现的abstractBase Class。它实现了所有DataSource实现通用的代码。如果编写自己的DataSource实现,则应扩展AbstractDataSource类。

3.4.5. 使用 SingleConnectionDataSource

SingleConnectionDataSource类是SmartDataSource接口的实现,该接口包装单个Connection,每次使用后都不会关闭。这不是多线程功能。

如果假定共享连接(例如使用持久性工具)时,任何 Client 端代码都调用close,则应将suppressClose属性设置为true。此设置将返回用于封装物理连接的关闭抑制代理。请注意,您不能再将此对象转换为本地 Oracle Connection或类似的对象。

SingleConnectionDataSource主要是测试班。例如,它结合简单的 JNDI 环境,可以在应用服务器外部轻松测试代码。与DriverManagerDataSource相比,它始终重用同一连接,避免了过多的物理连接创建。

3.4.6. 使用 DriverManagerDataSource

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

此实现对于 Java EE 容器外部的测试和独立环境很有用,可以作为 Spring IoC 容器中的DataSource bean 或与简单的 JNDI 环境结合使用。池假设Connection.close()调用将关闭连接,因此任何DataSource感知的持久性代码都应起作用。但是,即使在测试环境中,使用 JavaBean 风格的连接池(例如commons-dbcp)也是如此容易,以至总是总是比DriverManagerDataSource更好地使用这样的连接池。

3.4.7. 使用 TransactionAwareDataSourceProxy

TransactionAwareDataSourceProxy是目标DataSource的代理。代理包装该目标DataSource以增加对 SpringManagement 的事务的认识。在这方面,它类似于 Java EE 服务器提供的事务性 JNDI DataSource

Note

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

有关更多详细信息,请参见TransactionAwareDataSourceProxy javadoc。

3.4.8. 使用 DataSourceTransactionManager

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

需要应用程序代码才能通过DataSourceUtils.getConnection(DataSource)而不是 Java EE 的标准DataSource.getConnection检索 JDBC 连接。它引发未检查的org.springframework.dao异常,而不是已检查的SQLExceptions。所有框架类(例如JdbcTemplate)都隐式使用此策略。如果不与该事务 Management 器一起使用,则查找策略的行为与普通策略完全相同。因此,可以在任何情况下使用它。

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

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

3.5. JDBC 批处理操作

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

3.5.1. 使用 JdbcTemplate 的基本批处理操作

通过实现一个特殊接口BatchPreparedStatementSetter的两个方法,并将该实现作为batchUpdate方法调用中的第二个参数传入,可以完成JdbcTemplate批处理。您可以使用getBatchSize方法提供当前批处理的大小。您可以使用setValues方法设置准备好的语句的参数值。该方法称为您在getBatchSize调用中指定的次数。以下示例根据列表中的条目更新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 {
                        ps.setString(1, actors.get(i).getFirstName());
                        ps.setString(2, actors.get(i).getLastName());
                        ps.setLong(3, actors.get(i).getId().longValue());
                    }
                    public int getBatchSize() {
                        return actors.size();
                    }
                });
    }

    // ... additional methods
}

如果处理更新流或从文件读取,则可能具有首选的批处理大小,但最后一批可能没有该数量的条目。在这种情况下,您可以使用InterruptibleBatchPreparedStatementSetter界面,一旦 Importing 源用尽,您就可以中断批处理。 isBatchExhausted方法可让您发出批处理结束的 signal。

3.5.2. 具有对象列表的批处理操作

JdbcTemplateNamedParameterJdbcTemplate都提供了另一种提供批处理更新的方式。无需实现特殊的批处理接口,而是将调用中的所有参数值作为列表提供。框架循环这些值,并使用内部准备好的语句设置器。 API 会有所不同,具体取决于您是否使用命名参数。对于命名参数,您提供一个SqlParameterSource数组,每个批次成员一个条目。您可以使用SqlParameterSourceUtils.createBatch便捷方法创建此数组,传入一个 Bean 样式对象(带有与参数相对应的 getter 方法),String -keyed 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
}

对于使用经典?占位符的 SQL 语句,您传入一个包含带有更新值的对象数组的列表。该对象数组在 SQL 语句中的每个占位符必须具有一个条目,并且它们的 Sequences 必须与 SQL 语句中定义的 Sequences 相同。

除使用经典的 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
}

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

Note

在这种情况下,通过在基础PreparedStatement上自动设置值,需要从给定的 Java 类型派生每个值的对应 JDBC 类型。尽管这通常效果很好,但存在潜在的问题(例如,包含 Map 的null值)。在这种情况下,Spring 默认情况下会调用ParameterMetaData.getParameterType,这对于 JDBC 驱动程序可能会很昂贵。如果遇到性能问题,您应该使用最新的驱动程序版本,并考虑将spring.jdbc.getParameterType.ignore属性设置为true(作为 JVM 系统属性或在 Classpath 根目录中的spring.properties文件中),例如,如 Oracle 12c(SPR)所述-16139)。

或者,您可以考虑通过“ BatchPreparedStatementSetter”(如前所示),通过给基于“ List<Object[]>”的调用的显式类型数组,通过在自定义“ MapSqlParameterSource”上的“ registerSqlType”调用来显式指定相应的 JDBC 类型。 '实例,或者通过'BeanPropertySqlParameterSource'从 Java 声明的属性类型派生 SQL 类型,即使对于 null 值也是如此。

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,
                new ParameterizedPreparedStatementSetter<Actor>() {
                    public void setValues(PreparedStatement ps, Actor argument) throws SQLException {
                        ps.setString(1, argument.getFirstName());
                        ps.setString(2, argument.getLastName());
                        ps.setLong(3, argument.getId().longValue());
                    }
                });
        return updateCounts;
    }

    // ... additional methods
}

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

3.6. 使用 SimpleJdbc 类简化 JDBC 操作

SimpleJdbcInsertSimpleJdbcCall类通过利用可通过 JDBC 驱动程序检索的数据库元数据来提供简化的配置。这意味着您可以更少地进行前期配置,但是如果您愿意在代码中提供所有详细信息,则可以覆盖或关闭元数据处理。

3.6.1. 使用 SimpleJdbcInsert 插入数据

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

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

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

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

下一个示例使用与前面的示例相同的插入内容,但是它没有传递id,而是检索自动生成的密钥并将其设置在新的Actor对象上。当创建SimpleJdbcInsert时,除了指定表名之外,还使用usingGeneratedKeyColumns方法指定生成的键列的名称。以下 Lists 显示了它的工作方式:

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

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

3.6.3. 为 SimpleJdbcInsert 指定列

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

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

插入的执行与依靠元数据确定要使用的列的执行相同。

3.6.4. 使用 SqlParameterSource 提供参数值

使用Map提供参数值可以很好地工作,但这并不是最方便使用的类。 Spring 提供了SqlParameterSource接口的几种实现,您可以代替使用它们。第一个是BeanPropertySqlParameterSource,如果您有一个包含值的 JavaBean 兼容类,这是一个非常方便的类。它使用相应的 getter 方法提取参数值。以下示例显示了如何使用BeanPropertySqlParameterSource

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

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

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

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

3.6.5. 用 SimpleJdbcCall 调用存储过程

SimpleJdbcCall类使用数据库中的元数据来查找inout参数的名称,因此您不必显式声明它们。如果愿意,可以声明参数,也可以声明没有自动 Map 到 Java 类的参数(例如ARRAYSTRUCT)。第一个示例显示了一个简单过程,该过程仅从 MySQL 数据库返回VARCHARDATE格式的标量值。示例过程读取指定的 actor 条目,并以out参数的形式返回first_namelast_namebirth_date列。以下 Lists 显示了第一个示例:

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参数包含您要查找的演员的idout参数返回从表读取的数据。

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

public class JdbcActorDao implements ActorDao {

    private JdbcTemplate jdbcTemplate;
    private SimpleJdbcCall procReadActor;

    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(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
}

您为执行调用而编写的代码涉及创建一个包含 IN 参数的SqlParameterSource。您必须为 Importing 值提供的名称与存储过程中声明的参数名称的名称匹配。大小写不必匹配,因为您使用元数据来确定在存储过程中应如何引用数据库对象。源中为存储过程指定的内容不一定是存储过程在数据库中存储的方式。一些数据库将名称转换为全部大写,而另一些数据库使用小写或指定的大小写。

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

execute方法的最后一部分创建一个Actor实例,以用于返回检索到的数据。同样,使用在存储过程中声明的out参数的名称也很重要。同样,结果 Map 中存储的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
}

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

3.6.6. 明确声明要用于 SimpleJdbcCall 的参数

在本章的前面,我们描述了如何从元数据推导出参数,但是如果需要,可以显式声明它们。您可以通过使用declareParameters方法创建和配置SimpleJdbcCall来实现,该方法采用可变数量的SqlParameter对象作为 Importing。有关如何定义SqlParameter的详细信息,请参见next section

Note

如果您使用的数据库不是 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
}

两个示例的执行和最终结果相同。第二个示例显式指定所有详细信息,而不是依赖于元数据。

3.6.7. 如何定义 SqlParameters

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

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

带有SqlParameter的第一行声明一个 IN 参数。通过使用SqlQuery及其子类(在Understanding SqlQuery中介绍),可以将 IN 参数用于存储过程调用和查询。

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

Note

仅声明为SqlParameterSqlInOutParameter的参数用于提供 Importing 值。这与StoredProcedure类不同,后者(出于向后兼容的原因)允许为声明为SqlOutParameter的参数提供 Importing 值。

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

3.6.8. 通过使用 SimpleJdbcCall 调用存储的函数

可以使用与调用存储过程几乎相同的方式来调用存储函数,除了提供函数名而不是过程名。您将withFunctionName方法用作配置的一部分,以指示您要对函数进行调用,并生成函数调用的相应字符串。专门的执行调用(executeFunction)用于执行函数,它以指定类型的对象的形式返回函数返回值,这意味着您不必从结果 Map 中检索返回值。对于只有一个out参数的存储过程,也可以使用类似的便捷方法(名为executeObject)。以下示例(对于 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
}

所使用的executeFunction方法返回一个String,其中包含该函数调用的返回值。

3.6.9. 从 SimpleJdbcCall 返回 ResultSet 或 REF 游标

调用返回结果集的存储过程或函数有点棘手。一些数据库在 JDBC 结果处理期间返回结果集,而另一些数据库则需要显式注册的特定类型的out参数。两种方法都需要进行额外的处理才能遍历结果集并处理返回的行。使用SimpleJdbcCall,您可以使用returningResultSet方法并声明RowMapper实现用于特定参数。如果在结果处理期间返回了结果集,则没有定义任何名称,因此返回的结果必须与声明RowMapper实现的 Sequences 匹配。指定的名称仍用于将处理后的结果列表存储在从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。因为您要 Map 到的类遵循 JavaBean 规则,所以可以使用通过在newInstance方法中传入要 Map 的必需类而创建的BeanPropertyRowMapper。以下示例显示了如何执行此操作:

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
}

execute调用传入空的Map,因为该调用没有任何参数。然后从结果图中检索参与者列表,并将其返回给调用者。

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

org.springframework.jdbc.object软件包包含一些类,这些类使您可以以更加面向对象的方式访问数据库。例如,您可以执行查询并将结果作为包含业务对象的列表返回,该业务对象的关系列数据 Map 到业务对象的属性。您还可以运行存储过程并运行 update,delete 和 insert 语句。

Note

许多 Spring 开发人员认为,下面描述的各种 RDBMS 操作类(但StoredProcedure类除外)通常可以被直接JdbcTemplate调用替换。通常,编写直接在JdbcTemplate上调用方法的 DAO 方法(与将查询封装为完整的类相反)更容易。

但是,如果通过使用 RDBMS 操作类获得可测量的价值,则应 continue 使用这些类。

3.7.1. 了解 SqlQuery

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

3.7.2. 使用 MappingSqlQuery

MappingSqlQuery是可重用的查询,其中具体子类必须实现抽象mapRow(..)方法,以将提供的ResultSet的每一行转换为指定类型的对象。以下示例显示了一个自定义查询,该查询将数据从t_actor关系 Map 到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;
    }

}

该类扩展了用Actor类型参数化的MappingSqlQuery。此 Client 查询的构造函数将DataSource作为唯一参数。在此构造函数中,可以使用DataSource和应执行以检索此查询的行的 SQL 调用超类上的构造函数。该 SQL 用于创建PreparedStatement,因此它可以包含在执行期间要传递的任何参数的占位符。您必须使用SqlParameter传递的declareParameter方法声明每个参数。 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);
}

前面示例中的方法使用传入的id作为唯一参数来检索 Client。由于只希望返回一个对象,因此我们将id作为参数调用findObject便捷方法。相反,如果有一个查询返回一个对象列表并采用其他参数,则将使用execute方法之一,该方法采用以 varargs 形式传入的参数值数组。以下示例显示了这种方法:

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

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);
    }
}

3.7.4. 使用 StoredProcedure

StoredProcedure类是 RDBMS 存储过程的对象抽象的超类。此类是abstract,并且其各种execute(..)方法具有protected访问权限,除了通过提供更严格的键入的子类之外,其他都禁止使用。

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

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

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

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

第一行(带有SqlParameter)声明一个 IN 参数。您可以将 IN 参数用于存储过程调用和使用SqlQuery及其子类(在Understanding SqlQuery中覆盖)的查询。

第二行(带有SqlOutParameter)声明了要在存储过程调用中使用的out参数。还有SqlInOutParameter表示InOut参数(为过程提供in值并返回值的参数)。

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

下一个简单 DAO 的示例使用StoredProcedure调用任何 Oracle 数据库随附的函数(sysdate())。要使用存储过程功能,您必须创建一个扩展StoredProcedure的类。在此的示例StoredProcedure类是一个内部类。但是,如果需要重用StoredProcedure,则可以将其声明为顶级类。本示例没有 Importing 参数,但是使用SqlOutParameter类将输出参数声明为日期类型。 execute()方法运行该过程,并从结果Map中提取返回的日期。通过使用参数名称作为键,结果Map为每个声明的输出参数(在本例中为一个)都有一个条目。以下 Lists 显示了我们的自定义 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;
        }
    }

}

下面的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>());
    }
}

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

对于提供的ResultSet中的每一行,TitleMapper类将ResultSetMap 到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;
    }
}

对于提供的ResultSet中的每一行,GenreMapper类将ResultSetMap 到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"));
    }
}

要将参数传递给 RDBMS 中定义中具有一个或多个 Importing 参数的存储过程,可以编写一个强类型的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);
    }
}

3.8. 参数和数据值处理的常见问题

Spring Framework 的 JDBC 支持提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决它们。

3.8.1. 提供参数的 SQL 类型信息

通常,Spring 根据传入的参数类型确定参数的 SQL 类型。可以在设置参数值时显式提供要使用的 SQL 类型。有时需要正确设置NULL值。

您可以通过几种方式提供 SQL 类型信息:

3.8.2. 处理 BLOB 和 CLOB 对象

您可以在数据库中存储图像,其他二进制数据和大块文本。这些大对象称为二进制数据的 BLOB(二进制大型对象),而字符数据称为 CLOB(字符大型对象)。在 Spring 中,可以直接使用JdbcTemplate来处理这些大对象,也可以使用 RDBMS Objects 和SimpleJdbc类提供的更高抽象来处理这些大对象。所有这些方法都将LobHandler接口的实现用于 LOB(大对象)数据的实际 Management。 LobHandler通过getLobCreator方法提供对LobCreator类的访问,该方法用于创建要插入的新 LOB 对象。

LobCreatorLobHandler为 LOBImporting 和输出提供以下支持:

下一个示例显示了如何创建和插入 BLOB。稍后,我们展示如何从数据库中读取它。

本示例使用JdbcTemplateAbstractLobCreatingPreparedStatementCallback的实现。它实现了一种方法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();

Note

如果在从DefaultLobHandler.getLobCreator()返回的LobCreator上调用setBlobAsBinaryStreamsetClobAsAsciiStreamsetClobAsCharacterStream方法,则可以选择为contentLength参数指定负值。如果指定的内容长度为负数,则DefaultLobHandler使用不带 length 参数的 set-stream 方法的 JDBC 4.0 变体。否则,它将指定的长度传递给驱动程序。

请参阅有关 JDBC 驱动程序的文档,以用于验证它是否支持流式 LOB,而不提供内容长度。

现在是时候从数据库中读取 LOB 数据了。同样,您使用具有相同实例变量lobHandlerJdbcTemplate和对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;
        }
    });

3.8.3. 传入 IN 子句的值列表

SQL 标准允许根据包含变量值列表的表达式选择行。一个典型的例子是select * from T_ACTOR where id in (1, 2, 3)。 JDBC 标准不直接为准备好的语句支持此变量列表。您不能声明可变数量的占位符。您需要准备好所需数量的占位符的多种变体,或者一旦知道需要多少个占位符,就需要动态生成 SQL 字符串。 NamedParameterJdbcTemplateJdbcTemplate中提供的命名参数支持采用后一种方法。您可以将值作为原始对象的java.util.List传入。此列表用于插入所需的占位符,并在语句执行期间传递值。

Note

传递许多值时要小心。 JDBC 标准不能保证in表达式列表可以使用 100 个以上的值。各种数据库都超过了此数目,但是它们通常对允许多少个值有硬性限制。例如,Oracle 的限制为 1000.

除了值列表中的原始值之外,您还可以创建java.util.List对象数组。该列表可以支持为in子句定义的多个表达式,例如select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'\))。当然,这要求您的数据库支持此语法。

3.8.4. 处理存储过程调用的复杂类型

调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型,Spring 提供了一个SqlReturnType来处理它们(从存储过程调用中返回),并提供SqlTypeValue并将它们作为参数传递给存储过程。

SqlReturnType接口具有必须实现的单个方法(名为getTypeValue)。此接口用作SqlOutParameter声明的一部分。以下示例显示返回声明为ITEM_TYPE类型的用户的 Oracle STRUCT对象的值:

public class TestItemStoredProcedure extends StoredProcedure {

    public TestItemStoredProcedure(DataSource dataSource) {
        ...
        declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
            new SqlReturnType() {
                public Object getTypeValue(CallableStatement cs, int colIndx, int sqlType, String typeName) throws SQLException {
                    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;
                }
            }));
        ...
    }

您可以使用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;
    }
};

现在,您可以将此SqlTypeValue添加到包含用于存储过程execute调用的 Importing 参数的Map

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;
    }
};

3.9. 嵌入式数据库支持

org.springframework.jdbc.datasource.embedded软件包提供对嵌入式 Java 数据库引擎的支持。本地提供对HSQLH2Derby的支持。您还可以使用可扩展的 API 来插入新的嵌入式数据库类型和DataSource实现。

3.9.1. 为什么要使用嵌入式数据库?

嵌入式数据库由于其轻量级的特性,因此在项目的开发阶段可能会很有用。好处包括易于配置,启动时间短,可测试性以及在开发过程中快速演化 SQL 的能力。

3.9.2. 使用 Spring XML 创建嵌入式数据库

如果要在 Spring ApplicationContext中将嵌入式数据库实例作为 Bean 公开,则可以在spring-jdbc名称空间中使用embedded-database标记:

<jdbc:embedded-database id="dataSource" generate-name="true">
    <jdbc:script location="classpath:schema.sql"/>
    <jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>

前面的配置创建了一个嵌入式 HSQL 数据库,该数据库使用来自 Classpath 根目录中schema.sqltest-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()

有关所有支持的选项的更多详细信息,请参见用于 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();
    }
}

3.9.4. 选择嵌入式数据库类型

本节介绍如何选择 Spring 支持的三个嵌入式数据库之一。它包括以下主题:

Using HSQL

Spring 支持 HSQL 1.8.0 及更高版本。如果未明确指定类型,则 HSQL 是默认的嵌入式数据库。要显式指定 HSQL,请将embedded-database标记的type属性设置为HSQL。如果使用构建器 API,请使用EmbeddedDatabaseType.HSQL调用setType(EmbeddedDatabaseType)方法。

Using H2

Spring 支持 H2 数据库。要启用 H2,请将embedded-database标签的type属性设置为H2。如果使用构建器 API,请使用EmbeddedDatabaseType.H2调用setType(EmbeddedDatabaseType)方法。

Using Derby

Spring 支持 Apache Derby 10.5 及更高版本。要启用 Derby,请将embedded-database标签的type属性设置为DERBY。如果使用构建器 API,请使用EmbeddedDatabaseType.DERBY调用setType(EmbeddedDatabaseType)方法。

3.9.5. 使用嵌入式数据库测试数据访问逻辑

嵌入式数据库提供了一种轻量级的方法来测试数据访问代码。下一个示例是使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要在测试类之间重用时,使用这种模板可以一次性使用。但是,如果要创建在测试套件中共享的嵌入式数据库,请考虑使用Spring TestContext 框架并将嵌入式数据库配置为 Spring ApplicationContext中的 Bean,如使用 Spring XML 创建嵌入式数据库以编程方式创建嵌入式数据库中所述。以下 Lists 显示了测试模板:

public class DataAccessIntegrationTestTemplate {

    private EmbeddedDatabase db;

    @Before
    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( /* ... */ );
    }

    @After
    public void tearDown() {
        db.shutdown();
    }

}

3.9.6. 为嵌入式数据库生成唯一名称

如果开发团队的测试套件无意中尝试重新创建同一数据库的其他实例,则开发团队经常会遇到错误。如果 XML 配置文件或@Configuration类负责创建嵌入式数据库,然后在同一测试套件(即,同一 JVM 进程)中的多个测试场景中重复使用相应的配置,则这很容易发生。例如,集成测试针对ApplicationContext配置仅在哪个 bean 定义配置文件处于活动状态方面有所不同的嵌入式数据库。

此类错误的根本原因是,如果没有另外指定,Spring 的EmbeddedDatabaseFactory(由<jdbc:embedded-database> XML 名称空间元素和EmbeddedDatabaseBuilder用于 Java 配置内部使用)会将嵌入式数据库的名称设置为testdb。对于<jdbc:embedded-database>,通常为嵌入式数据库分配一个与 Bean 的id相同的名称(通常为dataSource之类的名称)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。取而代之的是,相同的 JDBC 连接 URL 被重用,并且尝试创建新的嵌入式数据库实际上指向的是从相同配置创建的现有嵌入式数据库。

为了解决这个常见问题,Spring Framework 4.2 提供了对生成嵌入式数据库的唯一名称的支持。要启用生成名称的使用,请使用以下选项之一。

3.9.7. 扩展嵌入式数据库支持

您可以通过两种方式扩展 Spring JDBC 嵌入式数据库的支持:

我们鼓励您在jira.spring.io向 Spring 社区提供扩展。

3.10. 初始化数据源

org.springframework.jdbc.datasource.init软件包为初始化现有的DataSource提供支持。嵌入式数据库支持提供了一个为应用程序创建和初始化DataSource的选项。但是,有时您可能需要初始化在某处的服务器上运行的实例。

3.10.1. 使用 Spring XML 初始化数据库

如果要初始化数据库,并且可以提供对DataSource bean 的引用,则可以使用spring-jdbc命名空间中的initialize-database标记:

<jdbc:initialize-database data-source="dataSource">
    <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/>
    <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/>
</jdbc:initialize-database>

前面的示例对数据库运行两个指定的脚本。第一个脚本创建模式,第二个脚本用测试数据集填充表。脚本位置也可以是带有通配符的模式,该通配符具有用于 Spring 中资源的常用 Ant 样式(例如classpath*:/com/foo/**/sql/*-data.sql)。如果使用模式,则脚本以其 URL 或文件名的词法 Sequences 运行。

数据库初始化程序的默认行为是无条件运行提供的脚本。这可能并不总是您想要的。例如,如果您对已经有测试数据的数据库运行脚本。通过遵循首先创建表然后插入数据的通用模式(如前所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步失败。

但是,为了更好地控制现有数据的创建和删除,XML 名称空间提供了一些其他选项。第一个是用于打开和关闭初始化的标志。您可以根据环境进行设置(例如,从系统属性或环境 Bean 中提取布尔值)。以下示例从系统属性获取值:

<jdbc:initialize-database data-source="dataSource"
    enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1)
    <jdbc:script location="..."/>
</jdbc: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>

在此示例中,两个test-data脚本使用@@作为语句分隔符,只有db-schema.sql使用;。此配置指定默认分隔符为@@并覆盖db-schema脚本的默认分隔符。

如果您需要比从 XML 名称空间获得更多控制权,则可以直接使用DataSourceInitializer并将其定义为应用程序中的组件。

初始化依赖于数据库的其他组件

大量的应用程序(那些在 Spring 上下文启动之后才使用数据库的应用程序)可以使用数据库初始化程序,而不会带来更多麻烦。如果您的应用程序不是其中之一,则可能需要阅读本节的其余部分。

数据库初始化程序取决于DataSource实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的init-method,组件中的@PostConstruct方法或实现InitializingBean的组件中的afterPropertiesSet()方法)。如果其他 bean 依赖于相同的数据源并在初始化回调中使用该数据源,则可能存在问题,因为尚未初始化数据。一个常见的示例是一个高速缓存,它会在应用程序启动时急于初始化并从数据库加载数据。

要解决此问题,您有两个选择:将高速缓存初始化策略更改为以后的阶段,或者确保首先初始化数据库初始化程序。

如果应用程序在您的控制之下,则更改缓存初始化策略可能很容易,否则就不那么容易。有关如何实现此目的的一些建议包括:

确保首先初始化数据库初始化程序也很容易。关于如何实现这一点的一些建议包括:

4.对象关系 Map(ORM)数据访问

本节介绍使用对象关系 Map(ORM)时的数据访问。

4.1. Spring ORM 简介

Spring Framework 支持与 Java Persistence API(JPA)集成,并支持用于资源 Management,数据访问对象(DAO)实现和事务策略的本地 Hibernate。例如,对于 Hibernate,提供了一流的支持以及一些便捷的 IoC 功能,这些功能可以解决许多典型的 Hibernate 集成问题。您可以通过“依赖关系注入”为 OR(对象关系)Map 工具配置所有受支持的功能。他们可以参与 Spring 的资源和事务 Management,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成样式是针对普通的 Hibernate 或 JPA API 编写 DAO。

当您创建数据访问应用程序时,Spring 会为您选择的 ORM 层显着增强。您可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与内部构建类似基础架构的成本和风险进行比较。不管使用哪种技术,您都可以像使用库一样使用许多 ORM 支持,因为所有内容都是作为一组可重用的 JavaBean 设计的。 Spring IoC 容器中的 ORM 有助于配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内部的配置。

使用 Spring 框架创建 ORM DAO 的好处包括:

Tip

要获得更全面的 ORM 支持,包括对 MongoDB 等替代数据库技术的支持,您可能需要查看Spring Data项目套件。如果您是 JPA 用户,则https://spring.io使用 JPA 访问数据的入门指南提供了很好的介绍。

4.2. ORM 集成的一般注意事项

本节重点介绍适用于所有 ORM 技术的注意事项。 Hibernate部分提供更多详细信息,并在具体上下文中显示这些功能和配置。

Spring 的 ORM 集成的主要目标是清晰的应用程序分层(具有任何数据访问和事务技术)以及松散耦合应用程序对象–不再对数据访问或事务策略依赖业务服务,不再进行硬编码资源查找,更多难以替换的单例,不再需要自定义服务注册表。目标是采用一种简单且一致的方法来连接应用程序对象,使它们尽可能可重用,并尽可能避免容器依赖性。所有单独的数据访问功能都可以单独使用,但可以与 Spring 的应用程序上下文概念很好地集成,从而提供基于 XML 的配置和对不需要 Spring 意识的纯 JavaBean 实例的交叉引用。在典型的 Spring 应用程序中,许多重要的对象是 JavaBean:数据访问模板,数据访问对象,事务 Management 器,使用数据访问对象和事务 Management 器的业务服务,Web 视图解析器,使用业务服务的 Web 控制器等等。 。

4.2.1. 资源与 TransactionManagement

典型的业务应用程序中充斥着重复的资源 Management 代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。 Spring 提倡简单的解决方案来进行适当的资源处理,即通过在 JDBC 情况下进行模板化以及为 ORM 技术应用 AOP 拦截器来实现 IoC。

基础结构提供适当的资源处理,并将特定的 API 异常适当地转换为未经检查的基础结构异常层次结构。 Spring 引入了 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,previous section中提到的JdbcTemplate类提供了连接处理和SQLExceptionDataAccessException层次结构的正确转换,包括将特定于数据库的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参见next section以获取相同的异常翻译好处。

在事务 Management 方面,JdbcTemplate类通过相应的 Spring 事务 Management 器挂接到 Spring 事务支持并支持 JTA 和 JDBC 事务。对于受支持的 ORM 技术,Spring 通过 Hibernate 和 JPA 事务 Management 器提供了 Hibernate 和 JPA 支持以及 JTA 支持。有关 Transaction 支持的详细信息,请参见Transaction Management章。

4.2.2. exception 翻译

在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。 DAO 抛出HibernateExceptionPersistenceException的子类,具体取决于技术。这些异常都是运行时异常,不必声明或捕获。您可能还必须处理IllegalArgumentExceptionIllegalStateException。这意味着调用者只能将异常视为一般致命的,除非他们希望依赖于持久性技术自身的异常结构。如果不将调用者与实现策略联系在一起,则无法捕获特定原因(例如乐观锁定失败)。这种权衡可能对于基于 ORM 的应用程序或不需要任何特殊异常处理(或两者都使用)的应用程序是可接受的。但是,Spring 允许通过@RepositoryComments 透明地应用异常转换。以下示例(一个用于 Java 配置,一个用于 XML 配置)显示了如何执行此操作:

@Repository
public class ProductDaoImpl implements 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 和 Comments 来实现 DAO,同时仍可受益于 SpringManagement 的事务,依赖项注入以及对 Spring 的自定义异常层次结构的透明异常转换(如果需要)。

4.3. Hibernate

我们从 Spring 环境中的Hibernate 5开始,使用它来演示 Spring 集成 ORMap 器所采用的方法。本节详细讨论了许多问题,并展示了 DAO 实现和事务划分的不同变体。这些模式中的大多数都可以直接转换为所有其他受支持的 ORM 工具。然后,本章后面的部分将介绍其他 ORM 技术,并显示一些简短的示例。

Note

从 Spring Framework 5.0 开始,Spring 需要 Hibernate ORM 4.3 或更高版本来支持 JPA,甚至需要 Hibernate ORM 5.0 来针对本机 Hibernate Session API 进行编程。请注意,Hibernate 团队不再维护 5.1 之前的任何版本,并且可能很快会专注于 5.3.

4.3.1. Spring 容器中的 SessionFactory 设置

为了避免将应用程序对象与硬编码的资源查找绑定在一起,可以在 Spring 容器中将资源(例如 JDBC DataSource或 Hibernate SessionFactory)定义为 bean。需要访问资源的应用程序对象通过 Bean 引用接收对此类 sched 义实例的引用,如next section中的 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(通常由应用服务器 Management)只是配置问题,如以下示例所示:

<beans>
    <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/>
</beans>

您还可以使用 Spring 的JndiObjectFactoryBean/<jee:jndi-lookup>来访问位于 JNDI 的SessionFactory,以检索并公开它。但是,这通常在 EJB 上下文之外并不常见。

Note

Spring 还提供了LocalSessionFactoryBuilder变体,可与@Bean样式配置和编程设置(不涉及FactoryBean)无缝集成。

LocalSessionFactoryBeanLocalSessionFactoryBuilder都支持后台引导,并且 Hibernate 初始化与给定引导执行程序(例如SimpleAsyncTaskExecutor)上的应用程序引导线程并行运行。在LocalSessionFactoryBean上,可以通过bootstrapExecutor属性使用。在程序化LocalSessionFactoryBuilder上,有一个重载的buildSessionFactory方法,该方法带有引导执行程序参数。

从 Spring Framework 5.1 开始,这样的本地 Hibernate 设置还可以在本地 Hibernate 访问旁边公开用于标准 JPA 交互的 JPA EntityManagerFactory。有关详情,请参见JPA 的本机 Hibernate 设置

4.3.2. 基于 Plain Hibernate API 实现 DAO

Hibernate 具有称为上下文会话的功能,其中,Hibernate 本身每个事务 Management 一个当前的Session。这大致相当于 Spring 对每个事务同步一个 Hibernate Session。基于普通的 Hibernate API,相应的 DAO 实现类似于以下示例:

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();
    }
}

该样式与 Hibernate 参考文档和示例的样式类似,不同之处在于将SessionFactory保留在实例变量中。我们强烈建议在 Hibernate 的 CaveatEmptor 示例应用程序中的老式static HibernateUtil类上使用基于实例的设置。 (通常,除非绝对必要,否则不要在static变量中保留任何资源.)

前面的 DAO 示例遵循依赖项注入模式。它很好地适合于 Spring IoC 容器,就像针对 Spring 的HibernateTemplate进行编码一样。您还可以在纯 Java 中设置这种 DAO(例如,在单元测试中)。为此,请对其进行实例化并使用所需的工厂参考调用setSessionFactory(..)。作为 Spring bean 的定义,DAO 类似于以下内容:

<beans>

    <bean id="myProductDao" class="product.ProductDaoImpl">
        <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>

</beans>

这种 DAO 样式的主要优点是它仅依赖于 Hibernate API。不需要导入任何 Spring 类。从非侵入性的角度来看,这很有吸引力,并且对于 Hibernate 开发人员而言可能更自然。

但是,DAO 会抛出普通的HibernateException(未经检查,因此不必声明或捕获),这意味着调用方只能将异常视为一般致命的消息-除非他们希望依赖于 Hibernate 自己的异常层次结构。如果不将调用者与实现策略联系在一起,则无法捕获特定原因(例如乐观锁定失败)。这种权衡对于基于 Hibernate 的应用程序,不需要任何特殊异常处理或两者都可以接受。

幸运的是,Spring 的LocalSessionFactoryBean支持任何 Spring 事务策略的 Hibernate 的SessionFactory.getCurrentSession()方法,甚至返回HibernateTransactionManager,返回当前的 SpringManagement 的事务Session。该方法的标准行为仍然是返回与正在进行的 JTA 事务关联的当前Session(如果有)。无论您使用 Spring 的JtaTransactionManager,EJB 容器 Management 的事务(CMT)还是 JTA,此行为均适用。

总之,您可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 SpringManagement 的事务。

4.3.3. 声明式事务划分

我们建议您使用 Spring 的声明式事务支持,该支持使您可以用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。您可以使用 Java 注解或 XML 在 Spring 容器中配置此事务拦截器。这种声明式事务处理功能使您可以使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的 true 价值。

Note

在 continue 之前,我们强烈建议您阅读声明式 TransactionManagement(如果您尚未阅读的话)。

您可以使用@TransactionalComments 对服务层进行 Comments,并指示 Spring 容器查找这些 Comments 并为这些带 Comments 的方法提供事务性语义。以下示例显示了如何执行此操作:

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();
    }

}

在容器中,您需要设置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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 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. 程序化 Transaction 划分

您可以在应用程序的较高级别中划分事务,而这些较低级别的数据访问服务可以跨越任意数量的操作。对周围业务服务的实施也没有限制。它只需要一个 Spring PlatformTransactionManager。同样,后者可以来自任何地方,但最好通过setTransactionManager(..)方法作为 bean 的引用。同样,应通过setProductDao(..)方法设置productDAO。以下几对代码片段显示了 Spring 应用程序上下文中的事务 Management 器和业务服务定义,以及业务方法实现的示例:

<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...
            }
        });
    }
}

Spring 的TransactionInterceptor允许将任何已检查的应用程序异常与回调代码一起引发,而TransactionTemplate仅限于回调中的未检查的异常。如果未检查的应用程序异常或应用程序将事务标记为仅回滚(通过设置TransactionStatus),则TransactionTemplate触发回滚。默认情况下,TransactionInterceptor的行为方式相同,但允许每个方法配置可回退策略。

4.3.5. TransactionManagement 策略

TransactionTemplateTransactionInterceptor都将实际的事务处理委托给PlatformTransactionManager实例(对于内部使用ThreadLocal Session可以是HibernateTransactionManager(对于单个 Hibernate SessionFactory))或JtaTransactionManager(委托给容器的 JTA 子系统)。应用程序。您甚至可以使用自定义PlatformTransactionManager实现。从本机 Hibernate 事务 Management 切换到 JTA(例如,当面对某些应用程序部署的分布式事务要求时)仅是配置问题。您可以用 Spring 的 JTA 事务实现替换 Hibernate 事务 Management 器。事务划分和数据访问代码都无需更改即可工作,因为它们使用通用的事务 ManagementAPI。

对于跨多个 Hibernate 会话工厂的分布式事务,可以将JtaTransactionManager作为事务策略与多个LocalSessionFactoryBean定义结合使用。然后,每个 DAO 都将一个特定的SessionFactory引用传递到其相应的 bean 属性中。如果所有基础 JDBC 数据源都是事务性容器数据源,那么只要使用JtaTransactionManager作为策略,业务服务就可以在任何数量的 DAO 和任意数量的会话工厂之间划分事务。

HibernateTransactionManagerJtaTransactionManager都允许使用 Hibernate 进行正确的 JVM 级别的缓存处理,而无需特定于容器的事务 Management 器查找或 JCA 连接器(如果您不使用 EJB 来启动事务)。

HibernateTransactionManager可以将 Hibernate JDBC Connection导出为特定DataSource的普通 JDBC 访问代码。此功能允许使用混合的 Hibernate 和 JDBC 数据访问进行高级事务划分,而完全无需 JTA,前提是您仅访问一个数据库。如果已通过LocalSessionFactoryBean类的dataSource属性使用DataSource设置了传入的SessionFactory,则HibernateTransactionManager自动将 Hibernate 事务公开为 JDBC 事务。或者,您可以通过HibernateTransactionManager类的dataSource属性显式指定应该为其公开事务的DataSource

4.3.6. 比较容器 Management 的资源和本地定义的资源

您可以在容器 Management 的 JNDI SessionFactory和本地定义的 JNDI SessionFactory之间切换,而无需更改单行应用程序代码。将资源定义保留在容器中还是在应用程序中本地保留,主要取决于您使用的事务策略。与 Spring 定义的本地SessionFactory相比,手动注册的 JNDI SessionFactory没有任何好处。通过 Hibernate 的 JCA 连接器部署SessionFactory可以增加参与 Java EE 服务器的 Management 基础结构的附加价值,但不会增加实际价值。

Spring 的事务支持未绑定到容器。当配置了 JTA 以外的任何策略时,事务支持也可以在独立或测试环境中工作。尤其是在单数据库事务的典型情况下,Spring 的单资源本地事务支持是 JTA 的轻量级功能强大的替代方案。当您使用本地 EJBStateless 会话 Bean 驱动事务时,即使您仅访问单个数据库并且仅使用 Stateless 会话 Bean 通过容器 Management 的事务来提供声明性事务,也要依赖 EJB 容器和 JTA。以编程方式直接使用 JTA 还需要 Java EE 环境。就 JTA 本身和 JNDI DataSource实例而言,JTA 不仅仅涉及容器依赖项。对于非 Spring 的,由 JTA 驱动的 Hibernate 事务,您必须使用 Hibernate JCA 连接器或额外的 Hibernate 事务代码,并将TransactionManagerLookup配置为正确的 JVM 级别的缓存。

如果 Spring 访问的事务访问单个数据库,则它们可以与本地定义的 Hibernate SessionFactory以及本地 JDBC DataSource一起工作。因此,当您具有分布式事务需求时,仅需要使用 Spring 的 JTA 事务策略。 JCA 连接器需要特定于容器的部署步骤,并且首先需要(显然)JCA 支持。与部署具有本地资源定义和 Spring 驱动的事务的简单 Web 应用程序相比,此配置需要更多的工作。另外,如果使用(例如)不提供 JCA 的 WebLogic Express,则通常需要容器的企业版。具有跨单个数据库的本地资源和事务的 Spring 应用程序可以在任何 Java EE Web 容器(没有 JTA,JCA 或 EJB)中运行,例如 Tomcat,Resin 甚至是普通 Jetty。此外,您可以轻松地在桌面应用程序或测试套件中重用这样的中间层。

考虑到所有问题,如果您不使用 EJB,请坚持使用本地SessionFactory设置和 Spring 的HibernateTransactionManagerJtaTransactionManager。您将获得所有好处,包括适当的事务性 JVM 级别的缓存和分布式事务,而不会给容器部署带来不便。通过 JCA 连接器对 Hibernate SessionFactory进行 JNDI 注册仅在与 EJB 结合使用时才增加价值。

4.3.7. Hibernate 虚 Pseudo 应用程序服务器警告

在某些具有非常严格的XADataSource实现的 JTA 环境中(当前仅某些 WebLogic Server 和 WebSphere 版本),在不考虑该环境的 JTA PlatformTransactionManager对象的情况下配置 Hibernate 时,虚假警告或异常会显示在应用程序服务器日志中。这些警告或异常指示正在访问的连接不再有效或 JDBC 访问不再有效,这可能是因为事务不再有效。例如,这是 WebLogic 的实际异常:

java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No
further JDBC access is allowed within this transaction.

您可以通过使 Hibernate 知道与之同步的 JTA PlatformTransactionManager实例(以及 Spring)来解决此警告。您可以通过以下两种方式执行此操作:

本节的其余部分描述了在 Hibernate 不了解 JTA PlatformTransactionManager的情况下发生的事件的 Sequences。

如果未配置 Hibernate 来识别 JTA PlatformTransactionManager,则在 JTA 事务提交时会发生以下事件:

当 Hibernate 配置为具有 JTA PlatformTransactionManager感知能力时,在 JTA 事务提交时会发生以下事件:

4.4. JPA

可以在org.springframework.orm.jpa软件包下获得的 Spring JPA 以类似于与 Hibernate 集成的方式为Java 持久性 API提供全面的支持,同时知道底层实现以提供附加功能。

4.4.1. 在 Spring 环境中设置 JPA 的三个选项

Spring JPA 支持提供了三种设置 JPA EntityManagerFactory的方式,供应用程序用来获取实体 Management 器。

Using LocalEntityManagerFactoryBean

您只能在简单的部署环境(例如独立应用程序和集成测试)中使用此选项。

LocalEntityManagerFactoryBean创建一个EntityManagerFactory,适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 bean 使用 JPA PersistenceProvider自动检测机制(根据 JPA 的 Java SE 自举),并且在大多数情况下,仅要求您指定持久性单元名称。以下 XML 示例配置了这样的 bean:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
        <property name="persistenceUnitName" value="myPersistenceUnit"/>
    </bean>
</beans>

JPA 部署的这种形式是最简单和最有限的。您不能引用现有的 JDBC DataSource bean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节码转换)是特定于提供程序的,通常需要在启动时指定特定的 JVM 代理。该选项仅对于设计了 JPA 规范的独立应用程序和测试环境就足够了。

从 JNDI 获取 EntityManagerFactory

部署到 Java EE 服务器时,可以使用此选项。查看服务器的文档,以了解如何将自定义 JPA 提供程序部署到服务器中,从而允许使用不同于服务器默认值的提供程序。

从 JNDI(例如在 Java EE 环境中)获取EntityManagerFactory是更改 XML 配置的问题,如以下示例所示:

<beans>
    <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/>
</beans>

此操作假定标准 Java EE 引导。 Java EE 服务器自动检测持久性单元(实际上是应用程序 jar 中的META-INF/persistence.xml个文件)和 Java EE 部署 Descriptors 中的persistence-unit-ref个条目(例如web.xml),并为这些持久性单元定义环境命名上下文位置。

在这种情况下,整个持久性单元部署,包括持久性类的编织(字节码转换),都取决于 Java EE 服务器。 JDBC DataSource是通过META-INF/persistence.xml文件中的 JNDI 位置定义的。 EntityManager事务与服务器的 JTA 子系统集成在一起。 Spring 仅使用获得的EntityManagerFactory,通过依赖性注入将其传递给应用程序对象,并 Management 持久性单元的事务(通常通过JtaTransactionManager)。

如果在同一应用程序中使用多个持久性单元,则此类 JNDI 检索的持久性单元的 Bean 名称应与应用程序用来引用它们的持久性单元名称匹配(例如,在@PersistenceUnit@PersistenceContext注解中)。

Using LocalContainerEntityManagerFactoryBean

您可以将此选项用于基于 Spring 的应用程序环境中的完整 JPA 功能。这包括 Web 容器(例如 Tomcat),独立应用程序以及具有复杂持久性要求的集成测试。

Note

如果要专门配置 Hibernate 设置,则直接的替代方法是使用 Hibernate 5.2 或 5.3 并设置本机 Hibernate LocalSessionFactoryBean而不是纯 JPA LocalContainerEntityManagerFactoryBean,从而使其与 JPA 访问代码以及本机 Hibernate 访问代码交互。有关详情,请参见用于 JPA 交互的本地 Hibernate 设置

LocalContainerEntityManagerFactoryBean可以完全控制EntityManagerFactory的配置,适用于需要细粒度自定义的环境。 LocalContainerEntityManagerFactoryBean基于persistence.xml文件,提供的dataSourceLookup策略和指定的loadTimeWeaver创建一个PersistenceUnitInfo实例。因此,可以在 JNDI 之外使用自定义数据源并控制编织过程。以下示例显示了LocalContainerEntityManagerFactoryBean的典型 bean 定义:

<beans>
    <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="someDataSource"/>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
        </property>
    </bean>
</beans>

以下示例显示了一个典型的persistence.xml文件:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
    <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL">
        <mapping-file>META-INF/orm.xml</mapping-file>
        <exclude-unlisted-classes/>
    </persistence-unit>
</persistence>

Note

<exclude-unlisted-classes/>快捷方式指示不应进行对带 Comments 的实体类的扫描。显式的“ true”值(<exclude-unlisted-classes>true</exclude-unlisted-classes/>)也表示不进行扫描。 <exclude-unlisted-classes>false</exclude-unlisted-classes/>确实触发了扫描。但是,如果您希望进行实体类扫描,我们建议省略exclude-unlisted-classes元素。

使用LocalContainerEntityManagerFactoryBean是最强大的 JPA 设置选项,可以在应用程序中进行灵活的本地配置。它支持到现有 JDBC DataSource的链接,同时支持本地和全局事务,等等。但是,它还对运行时环境提出了要求,例如,如果持久性提供程序要求字节码转换,则具有可编织类加载器的可用性。

此选项可能与 Java EE 服务器的内置 JPA 功能冲突。在完整的 Java EE 环境中,请考虑从 JNDI 获取您的EntityManagerFactory。或者,在LocalContainerEntityManagerFactoryBean定义上指定一个自定义persistenceXmlLocation(例如 META-INF/my-persistence.xml),并在应用程序 jar 文件中仅包含具有该名称的 Descriptors。因为 Java EE 服务器仅查找默认的META-INF/persistence.xml文件,所以它忽略了此类自定义持久性单元,因此避免了与 Spring 预先驱动的 JPA 设置发生冲突。 (例如,这适用于 Resin 3.1. )

什么时候需要加载时编织?

并非所有的 JPA 提供程序都需要 JVM 代理。休眠是一个没有的例子。如果您的提供程序不需要代理,或者您有其他选择,例如在构建时通过自定义编译器或 Ant 任务应用增强功能,则不应使用加载时织布器。

LoadTimeWeaver接口是 Spring 提供的类,可让 JPA ClassTransformer实例以特定方式插入,具体取决于环境是 Web 容器还是应用程序服务器。通过agent钩子ClassTransformers通常效率不高。代理针对整个虚拟机工作,并检查每个已加载的类,这在生产服务器环境中通常是不希望的。

Spring 为各种环境提供了许多LoadTimeWeaver实现,让ClassTransformer实例仅应用于每个类加载器,而不应用于每个 VM。

有关LoadTimeWeaver实现及其设置的更多信息,请参见 AOP 章节中的Spring configuration,这些实现是通用的还是针对各种平台(例如 Tomcat,WebLogic,GlassFish,Resin 和 JBoss)定制的。

Spring configuration中所述,您可以使用context:load-time-weaver XML 元素的@EnableLoadTimeWeaving注解来配置上下文范围内的LoadTimeWeaver。所有 JPA LocalContainerEntityManagerFactoryBean实例都会自动拾取这样的全局编织器。以下示例显示了设置加载时织构的首选方法,该方法可自动检测平台(WebLogic,GlassFish,Tomcat,Resin,JBoss 或 VM 代理),并将织构自动传播到所有可识别织构的 bean :

<context:load-time-weaver/>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    ...
</bean>

但是,您可以根据需要通过loadTimeWeaver属性手动指定专用的编织器,如以下示例所示:

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="loadTimeWeaver">
        <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
    </property>
</bean>

无论 LTW 的配置方式如何,通过使用此技术,依赖于检测的 JPA 应用程序都可以在目标平台(例如 Tomcat)中运行,而无需代理。当托管应用程序依赖于不同的 JPA 实现时,这一点尤其重要,因为 JPA 转换器仅应用于类加载器级别,因此彼此隔离。

处理多个持久性单元

对于依赖多个持久性单元位置的应用程序(例如,存储在 Classpath 中的各种 JARS 中),Spring 提供了PersistenceUnitManager充当中央存储库并避免了持久性单元发现过程,这可能是昂贵的。默认实现允许指定多个位置。解析这些位置,然后通过持久性单元名称进行检索。 (默认情况下,在 Classpath 中搜索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>

默认实现允许以声明方式(通过影响所有托管单元的属性)或以编程方式(通过允许持久化单元选择的PersistenceUnitPostProcessor)自定义PersistenceUnitInfo实例(在将其馈送到 JPA 提供程序之前)。如果未指定PersistenceUnitManager,则LocalContainerEntityManagerFactoryBean在内部创建和使用一个。

Background Bootstrapping

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

Note

尽管EntityManagerFactory个实例是线程安全的,但EntityManager个实例不是线程安全的。根据 JPA 规范定义,注入的 JPA EntityManager的行为类似于从应用程序服务器的 JNDI 环境中获取的EntityManager。它将所有调用委派给当前事务EntityManager(如果有)。否则,它会退回到每个操作新创建的EntityManager,实际上使它的使用成为线程安全的。

通过使用注入的EntityManagerFactoryEntityManager,可以针对不带任何 Spring 依赖关系的普通 JPA 编写代码。如果启用了PersistenceAnnotationBeanPostProcessor,则 Spring 可以在字段和方法级别理解@PersistenceUnit@PersistenceContext注解。以下示例显示了使用@PersistenceUnitComments 的普通 JPA DAO 实现:

public class ProductDaoImpl implements ProductDao {

    private EntityManagerFactory emf;

    @PersistenceUnit
    public void setEntityManagerFactory(EntityManagerFactory emf) {
        this.emf = emf;
    }

    public Collection loadProductsByCategory(String category) {
        EntityManager em = this.emf.createEntityManager();
        try {
            Query query = em.createQuery("from Product as p where p.category = ?1");
            query.setParameter(1, category);
            return query.getResultList();
        }
        finally {
            if (em != null) {
                em.close();
            }
        }
    }
}

前面的 DAO 不依赖于 Spring,并且仍然非常适合 Spring 应用程序上下文。此外,DAO 利用 Comments 的优势要求注入默认的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 标准后处理器以进行基于 Comments 的配置,包括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();
    }
}

@PersistenceContextComments 具有一个名为type的可选属性,默认为PersistenceContextType.TRANSACTION。您可以使用此默认值来接收共享的EntityManager代理。替代PersistenceContextType.EXTENDED是完全不同的事情。这导致所谓的EntityManager扩展,它不是线程安全的,因此不能在并发访问的组件(例如 SpringManagement 的单例 bean)中使用。扩展的EntityManager实例仅应用于有状态的组件中,例如,驻留在会话中的状态组件,而EntityManager的生命周期不依赖于当前事务,而完全取决于应用程序。

方法级和 site 级进样

您可以在类中的字段或方法上应用指示依赖项注入的 Comments(例如@PersistenceUnit@PersistenceContext),因此表达式为“方法级注入”和“字段级注入”。字段级 Comments 简洁明了,易于使用,而方法级 Comments 则允许对注入的依赖项进行进一步处理。在这两种情况下,成员的可见性(公共,受保护或私有)都无关紧要。

那么类级 Comments 呢?

在 Java EE 平台上,它们用于依赖性声明,而不用于资源注入。

注入的EntityManager是 SpringManagement 的(意识到正在进行的事务)。即使新的 DAO 实现使用EntityManager而不是EntityManagerFactory的方法级注入,由于 Comments 的使用,应用程序上下文 XML 也不需要更改。

这种 DAO 样式的主要优点是,它仅取决于 Java Persistence API。不需要导入任何 Spring 类。而且,由于可以理解 JPA 注解,因此 Spring 容器会自动应用注入。从非侵入性的角度来看,这是有吸引力的,并且对于 JPA 开发人员而言,感觉会更自然。

4.4.3. Spring 驱动的 JPATransaction

Note

我们强烈建议您阅读声明式 TransactionManagement(如果您尚未阅读的话),以更详细地介绍 Spring 的声明式事务支持。

对于 JPA,推荐的策略是通过 JPA 的本机事务支持来进行本地事务。 Spring 的JpaTransactionManager提供了针对任何常规 JDBC 连接池(无需 XA 要求)的本地 JDBC 事务中已知的许多功能(例如,特定于事务的隔离级别和资源级别的只读优化)。

Spring JPA 还允许已配置的JpaTransactionManager将 JPA 事务公开给访问同一DataSource的 JDBC 访问代码,只要注册的JpaDialect支持底层 JDBC Connection的检索。 Spring 为 EclipseLink 和 Hibernate JPA 实现提供了方言。有关JpaDialect机制的详细信息,请参见next section

Note

作为直接替代方案,Spring 的本机HibernateTransactionManager能够与 Spring Framework 5.1 和 Hibernate 5.2/5.3 以及更高版本的 JPA 访问代码进行交互,以适应多种 Hibernate 规范并提供 JDBC 交互。结合LocalSessionFactoryBean设置,这特别有意义。有关详情,请参见用于 JPA 交互的本机 Hibernate 设置

4.4.4. 了解 JpaDialect 和 JpaVendorAdapter

作为高级功能,JpaTransactionManagerAbstractEntityManagerFactoryBean的子类允许将自定义JpaDialect传递到jpaDialect bean 属性中。 JpaDialect实现通常可以以特定于供应商的方式启用 Spring 支持的以下高级功能:

这对于特殊的事务语义和异常的高级翻译特别有价值。默认实现(DefaultJpaDialect)不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。

Tip

JpaVendorAdapter作为主要用于 Spring 的全功能LocalContainerEntityManagerFactoryBean设置的更广泛的提供程序适应工具,JpaVendorAdapterJpaDialect的功能与其他特定于提供程序的默认值结合在一起。分别指定HibernateJpaVendorAdapterEclipseLinkJpaVendorAdapter是分别为 Hibernate 或 EclipseLink 自动配置EntityManagerFactory设置的最便捷方法。请注意,这些提供程序适配器主要设计用于与 Spring 驱动的事务 Management 一起使用(即,与JpaTransactionManager一起使用)。

有关其操作以及在 Spring 的 JPA 支持中如何使用它们的更多详细信息,请参见JpaDialectJpaVendorAdapter javadoc。

4.4.5. 使用 JTA 事务 Management 设置 JPA

作为JpaTransactionManager的替代方法,Spring 还允许通过 JTA 在 Java EE 环境中或与独立事务协调器(例如 Atomikos)一起进行多资源事务协调。除了选择 Spring 的JtaTransactionManager而不是JpaTransactionManager之外,您还需要采取其他步骤:

4.4.6. 用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务

从 Spring Framework 5.1 和 Hibernate 5.2/5.3 开始,与LocalSessionFactoryBean结合使用的本地LocalSessionFactoryBean设置允许与@PersistenceContext和其他 JPA 访问代码进行交互。现在,Hibernate SessionFactory本机实现 JPA 的EntityManagerFactory接口,而 Hibernate Session句柄本机则是 JPA EntityManager。 Spring 的 JPA 支持工具会自动检测本地 Hibernate 会话。

因此,这种本机 Hibernate 设置可以在许多情况下替代标准 JPA LocalContainerEntityManagerFactoryBeanJpaTransactionManager组合,从而允许在同一本地事务中与@PersistenceContext EntityManager旁边的SessionFactory.getCurrentSession()(以及HibernateTemplate)进行交互。这样的设置还提供了更强大的 Hibernate 集成和更大的配置灵 Active,因为它不受 JPA 引导 Contract 的约束。

在这种情况下,您不需要HibernateJpaVendorAdapter配置,因为 Spring 的本机 Hibernate 设置提供了更多功能(例如,自定义 Hibernate Integrator 设置,Hibernate 5.3 Bean 容器集成以及对只读事务的更强优化)。最后但并非最不重要的一点是,您还可以通过LocalSessionFactoryBuilder表示本机 Hibernate 设置,并与@Bean样式配置(不涉及FactoryBean)无缝集成。

Note

LocalSessionFactoryBeanLocalSessionFactoryBuilder支持后台引导,就像 JPA LocalContainerEntityManagerFactoryBean一样。有关简介,请参见Background Bootstrapping

LocalSessionFactoryBean上,可以通过bootstrapExecutor属性使用。在程序化LocalSessionFactoryBuilder上,重载的buildSessionFactory方法采用引导执行程序参数。

5.使用对象 XMLMap 器编组 XML

5.1. Introduction

本章描述了 Spring 的 Object-XML Mapping 支持。对象 XMLMap(简称 O-XMap)是将 XML 文档与对象进行相互转换的动作。此转换过程也称为 XML 编组或 XML 序列化。本章可以互换使用这些术语。

在 O-XMap 领域,编组负责将对象(图形)序列化为 XML。以类似的方式,解组器将 XML 反序列化为对象图。该 XML 可以采用 DOM 文档,Importing 或输出流或 SAX 处理程序的形式。

使用 Spring 满足 O/XMap 需求的一些好处是:

5.1.1. 易于配置

Spring 的 bean 工厂使配置编组器变得容易,而无需构造 JAXB 上下文,JiBX 绑定工厂等。您可以像在应用程序上下文中配置任何其他 bean 一样配置编组器。此外,许多编组人员都可以使用基于 XML 名称空间的配置,从而使配置更加简单。

5.1.2. 一致的接口

Spring 的 O-XMap 通过两个全局接口MarshallerUnmarshaller进行操作。这些抽象使您可以相对轻松地切换 O-XMap 框架,而对进行编组的类几乎不需要更改。这种方法还有一个好处,就是可以以非介入方式使用混合匹配方法(例如,一些使用 JAXB 执行的编组和某些由 Castor 执行的编组)进行 XML 编组,从而使您可以利用每种方法的优势技术。

5.1.3. 一致的异常层次结构

Spring 提供了从底层 O-XMap 工具的异常到其自己的异常层次的转换,并以XmlMappingException作为根异常。这些运行时异常包装了原始异常,因此不会丢失任何信息。

5.2. 马歇尔与非马歇尔

introduction中所述,编组器将对象序列化为 XML,解组器将 XML 流反序列化为对象。本节描述了用于此目的的两个 Spring 接口。

5.2.1. 了解马歇尔

Spring 在org.springframework.oxm.Marshaller接口之后抽象了所有编组操作,其主要方法如下:

public interface Marshaller {

    /**
     * Marshal the object graph with the given root into the provided Result.
     */
    void marshal(Object graph, Result result) throws XmlMappingException, IOException;
}

Marshaller接口有一个主要方法,该方法将给定对象封送给给定javax.xml.transform.Result。结果是一个标记接口,该接口基本上表示 XML 输出抽象。如下表所示,具体的实现包装了各种 XML 表示形式:

Result implementation 包装 XML 表示形式
DOMResult org.w3c.dom.Node
SAXResult org.xml.sax.ContentHandler
StreamResult java.io.Filejava.io.OutputStreamjava.io.Writer

Note

尽管marshal()方法接受一个普通对象作为其第一个参数,但是大多数Marshaller实现无法处理任意对象。相反,必须将对象类 Map 到 Map 文件中,用 Comments 标记,在编组器中注册或具有公共 Base Class。请参阅本章后面的部分,以确定 O-X 技术如何 Management 此问题。

5.2.2. 了解解组器

Marshaller类似,我们具有org.springframework.oxm.Unmarshaller界面,以下 Lists 显示了该界面:

public interface Unmarshaller {

    /**
     * Unmarshal the given provided Source into an object graph.
     */
    Object unmarshal(Source source) throws XmlMappingException, IOException;
}

该接口还有一个方法,该方法从给定的javax.xml.transform.Source(XMLImporting 抽象)中读取并返回读取的对象。与Result一样,Source是具有三种具体实现的标记接口。每个表都包装了不同的 XML 表示形式,如下表所示:

Source implementation 包装 XML 表示形式
DOMSource org.w3c.dom.Node
SAXSource org.xml.sax.InputSourceorg.xml.sax.XMLReader
StreamSource java.io.Filejava.io.InputStreamjava.io.Reader

即使有两个单独的编组接口(MarshallerUnmarshaller),Spring-WS 中的所有实现都在一个类中实现。这意味着您可以连接一个编组类,并在applicationContext.xml中将其称为编组类和解编组。

5.2.3. 了解 XmlMappingException

Spring 使用XmlMappingException作为根异常将底层 O-XMap 工具中的异常转换为它自己的异常层次。这些运行时异常包装了原始异常,因此不会丢失任何信息。

此外,MarshallingFailureExceptionUnmarshallingFailureException提供了编组和解组操作之间的区别,即使底层的 O-XMap 工具没有这样做。

O-XMap 异常层次结构如下图所示:

5.3. 使用 Marshaller 和 Unmarshaller

您可以在多种情况下使用 Spring 的 OXM。在下面的示例中,我们使用它来将 Spring 托管应用程序的设置作为 XML 文件进行编组。在下面的示例中,我们使用一个简单的 JavaBean 来表示设置:

public class Settings {

    private boolean fooEnabled;

    public boolean isFooEnabled() {
        return fooEnabled;
    }

    public void setFooEnabled(boolean fooEnabled) {
        this.fooEnabled = fooEnabled;
    }
}

应用程序类使用此 bean 存储其设置。除了主要方法外,该类还有两个方法: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 {
        FileOutputStream os = null;
        try {
            os = new FileOutputStream(FILE_NAME);
            this.marshaller.marshal(settings, new StreamResult(os));
        } finally {
            if (os != null) {
                os.close();
            }
        }
    }

    public void loadSettings() throws IOException {
        FileInputStream is = null;
        try {
            is = new FileInputStream(FILE_NAME);
            this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is));
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ApplicationContext appContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Application application = (Application) appContext.getBean("application");
        application.saveSettings();
        application.loadSettings();
    }
}

Application要求同时设置marshallerunmarshaller属性。我们可以使用以下applicationContext.xml来做到这一点:

<beans>
    <bean id="application" class="Application">
        <property name="marshaller" ref="castorMarshaller" />
        <property name="unmarshaller" ref="castorMarshaller" />
    </bean>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
</beans>

该应用程序上下文使用 Castor,但我们可以使用本章后面介绍的任何其他编组实例。请注意,默认情况下,Castor 不需要任何进一步的配置,因此 bean 的定义非常简单。还要注意CastorMarshaller同时实现MarshallerUnmarshaller,因此我们可以在应用程序的marshallerunmarshaller属性中引用castorMarshaller bean。

该示例应用程序生成以下settings.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<settings foo-enabled="false"/>

5.4. XML 配置命名空间

您可以使用 OXM 名称空间中的标签来更简洁地配置编组器。要使这些标签可用,您必须首先在 XML 配置文件的序言中引用适当的架构。以下示例显示了如何执行此操作:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:oxm="http://www.springframework.org/schema/oxm" (1)
xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2)

当前,该模式使以下元素可用:

每个标签在其各自的编组部分中进行了说明。但是,作为示例,JAXB2 编组器的配置可能类似于以下内容:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

5.5. JAXB

JAXB 绑定编译器将 W3C XML Schema 转换为一个或多个 Java 类,一个jaxb.properties文件以及可能的一些资源文件。 JAXB 还提供了一种从带 Comments 的 Java 类生成模式的方法。

Spring 遵循马歇尔与非马歇尔中描述的MarshallerUnmarshaller接口,将 JAXB 2.0 API 作为 XML 编组策略。相应的集成类位于org.springframework.oxm.jaxb包中。

5.5.1. 使用 Jaxb2Marshaller

Jaxb2Marshaller类同时实现 Spring 的MarshallerUnmarshaller接口。它需要上下文路径才能运行。您可以通过设置contextPath属性来设置上下文路径。上下文路径是冒号分隔的 Java 程序包名称的列表,其中包含模式派生的类。它还提供了classesToBeBound属性,该属性使您可以设置编组支持的类的数组。通过向 bean 指定一个或多个模式资源来执行模式验证,如以下示例所示:

<beans>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>org.springframework.oxm.jaxb.Flight</value>
                <value>org.springframework.oxm.jaxb.Flights</value>
            </list>
        </property>
        <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/>
    </bean>

    ...

</beans>
XML 配置命名空间

jaxb2-marshaller元素配置org.springframework.oxm.jaxb.Jaxb2Marshaller,如以下示例所示:

<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/>

或者,您可以使用class-to-be-bound子元素提供要绑定到编组的类的列表:

<oxm:jaxb2-marshaller id="marshaller">
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/>
    <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/>
    ...
</oxm:jaxb2-marshaller>

下表描述了可用的属性:

Attribute Description Required
id 编组的 ID No
contextPath JAXB 上下文路径 No

5.6. Castor

Castor XMLMap 是一个开源 XML 绑定框架。它使您可以将 Java 对象模型中包含的数据与 XML 文档进行相互转换。默认情况下,它不需要任何进一步的配置,尽管您可以使用 Map 文件来更好地控制 Castor 的行为。

有关 Castor 的更多信息,请参见Castor 网站。 Spring 集成类位于org.springframework.oxm.castor包中。

5.6.1. 使用 CastorMarshaller

与 JAXB 一样,CastorMarshaller实现MarshallerUnmarshaller接口。可以如下进行连接:

<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" />
    ...
</beans>

5.6.2. Mapping

尽管可以依赖 Castor 的默认编组行为,但可能有必要对其进行更多控制。您可以通过使用 CastorMap 文件来获得更多控制。有关更多信息,请参见Castor XMLMap

您可以使用mappingLocation资源属性来设置 Map,在以下示例中使用 Classpath 资源来指示:

<beans>
    <bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller" >
        <property name="mappingLocation" value="classpath:mapping.xml" />
    </bean>
</beans>
XML 配置命名空间

castor-marshaller标签配置org.springframework.oxm.castor.CastorMarshaller,如以下示例所示:

<oxm:castor-marshaller id="marshaller" mapping-location="classpath:org/springframework/oxm/castor/mapping.xml"/>

您可以通过两种方式配置 marshaller 实例:通过指定 Map 文件的位置(通过mapping-location属性)或通过标识 Java POJO(通过target-classtarget-package属性)来确定存在其相应 XMLDescriptors 类的 Java POJO。后一种方法通常与从 XML 模式生成 XML 代码结合使用。

下表描述了可用的属性:

Attribute Description Required
id 编组的 ID No
encoding 用于从 XML 解组的编码 No
target-class POJO 的 Java 类名,可为其使用 XML 类 Descriptors(通过代码生成生成) No
target-package Java 包名称,用于标识包含 POJO 及其对应的 Castor XMLDescriptors 类的包(由 XML 模式生成的代码生成) No
mapping-location Castor XMLMap 文件的位置 No

5.7. JiBX

JiBX 框架提供了与 Hibernate 为 ORM 提供的解决方案类似的解决方案:绑定定义定义了 Java 对象如何与 XML 相互转换的规则。在准备好绑定并编译了类之后,JiBX 绑定编译器将增强类文件并添加代码以处理从 XML 到 XML 的类实例的转换。

有关 JiBX 的更多信息,请参见JiBX 网站。 Spring 集成类位于org.springframework.oxm.jibx包中。

5.7.1. 使用 JibxMarshaller

JibxMarshaller类同时实现MarshallerUnmarshaller接口。要进行操作,需要 Importing 要编组的类的名称,您可以使用targetClass属性进行设置。 (可选)您可以通过设置bindingName属性来设置绑定名称。在下面的示例中,我们绑定Flights类:

<beans>
    <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller">
        <property name="targetClass">org.springframework.oxm.jibx.Flights</property>
    </bean>
    ...
</beans>

为单个类配置了JibxMarshaller。如果要封送多个类,则必须使用不同的targetClass属性值配置多个JibxMarshaller实例。

XML 配置命名空间

jibx-marshaller标签配置org.springframework.oxm.jibx.JibxMarshaller,如以下示例所示:

<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/>

下表描述了可用的属性:

Attribute Description Required
id 编组的 ID No
target-class 该编组的目标类 Yes
bindingName 该编组器使用的绑定名称 No

5.8. XStream

XStream 是一个简单的库,用于将对象序列化为 XML 并再次返回。它不需要任何 Map 并生成干净的 XML。

有关 XStream 的更多信息,请参见XStream 网站。 Spring 集成类位于org.springframework.oxm.xstream包中。

5.8.1. 使用 XStreamMarshaller

XStreamMarshaller不需要任何配置,可以直接在应用程序上下文中进行配置。为了进一步自定义 XML,可以设置一个别名 Map,该 Map 由 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>

Warning

默认情况下,XStream 允许将任意类取消编组,这可能导致不安全的 Java 序列化效果。因此,我们不建议使用XStreamMarshaller从外部源(即 Web)中解组 XML,因为这可能会导致安全漏洞。

如果选择使用XStreamMarshaller从外部源解组 XML,请在XStreamMarshaller上设置supportedClasses属性,如以下示例所示:

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/>
...
</bean>

这样做可确保只有注册的类才有资格进行编组。

此外,您可以注册custom converters以确保只能解组受支持的类。除了显式支持应支持的域类的转换器之外,您可能还想添加CatchAllConverter作为列表中的最后一个转换器。结果,不会调用具有较低优先级和可能的安全漏洞的默认 XStream 转换器。

Note

请注意,XStream 是 XML 序列化库,而不是数据绑定库。因此,它具有有限的名称空间支持。结果,它非常不适合在 Web 服务中使用。

6. Appendix

6.1. XML 模式

附录的此部分列出了用于数据访问的 XML 模式,包括以下内容:

6.1.1. tx 模式

tx标签用于在 Spring 的全面事务支持中配置所有这些 bean。这些标签在标题为Transaction Management的章节中介绍。

Tip

我们强烈建议您查看 Spring 发行版随附的'spring-tx.xsd'文件。该文件包含用于 Spring 事务配置的 XML 模式,并涵盖tx名称空间中的所有各个元素,包括属性默认值和类似信息。该文件已内联记录,因此,出于遵守 DRY(请勿重复自己)原则的考虑,此处不再重复信息。

为了完整起见,要使用tx模式中的元素,您需要在 Spring XML 配置文件的顶部具有以下序言。以下代码段中的文本引用了正确的架构,以便您可以使用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 http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd (2)
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- bean definitions here -->

</beans>

Note

通常,当您使用tx名称空间中的元素时,也会同时使用aop名称空间中的元素(因为 Spring 中的声明式事务支持是使用 AOP 实现的)。前面的 XML 代码段包含引用aop模式所需的相关行,以便aop命名空间中的元素可供您使用。

6.1.2. jdbc 模式

jdbc元素使您可以快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在嵌入式数据库支持初始化数据源中。

要使用jdbc模式中的元素,您需要在 Spring XML 配置文件的顶部具有以下序言。以下代码段中的文本引用了正确的架构,以便您可以使用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 http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2)

    <!-- bean definitions here -->

</beans>
首页