Spring基础(三)

11. 事务管理

11.1 Spring Framework事务管理介绍

广泛的事务支持是Spring Framework吸引人们使用的原因之一. Spring Framework提供的对事务的一致性抽象 的好处体现在如下方面:

  • 在不同的API之间使用一致的编程模型, 如Java Transaction API (Java事务API, JTA), JDBC, Hibernate, Java Persistence API (Java持久化API, JPA), 以及 Java Data Objects (JDO).
  • 支持声明式事务管理.
  • 对可编程事务管理比复杂的API, 如JTA,提供更简单的API.
  • 与Spring的数据访问层抽象的完美集成.

11.2 理解 Spring Framework 的事务抽象

Spring事物抽象的关键就是事务策略的概念. 事务策略在接口 org.springframework.transaction.PlatformTransactionManager中定义:

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(
            TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

这是一个主要的的服务提供接口(service provider interface,SPI), 尽管它可以在你的应用中以 编程式的方式使用. 因为PlatformTransactionManager是一个 接口, 所以它就很容易在必要时被更换或者修改. 它没有和任何一种发现策略绑定,如JNDI. PlatformTransactionManager的实现定义起来就和Spring Framework的控制反转(IoC)容器中其他的 object(或者 bean)一样. 这个特性使得Spring Framework的事务管理是一个很有用的抽象即使是你用它来和 JTA一起使用的时候. 有关事务的代码测试起来也比直接使用JTA更加简单.

又一项符合Spring哲学的是, 可以由PlatformTransactionManager接口的任何方法抛出来的 TransactionException异常的类型是未检查的(unchecked)(这是说, 它继承自 java.lang.RuntimeException类的). 事务管理底层的失败往往都是致命的. 在一些罕见的情况下,应用的代码 是忽略事务的失败的, 这时应用的开发人员仍然可以有选择地捕获并处理TransactionException. 关键是 开发者没有被强迫那么做.

返回一个TransactionStatus对象的getTransaction(..)方法需TransactionDefinition参数. 返回的 TransactionStatus可能代表了一个新的事务, 也有可能是代表了一个已经存在的事务, 如果在调用栈中有 已经存在的事务相匹配的话. 后者的一种例子就是, 在Java EE的事务上下文中有一个TransactionStatus和 执行线程相关.
接口TransactionDefinition的定义:

  • 独立性(Isolation): 定义当前事务和其他工作的事务独立的程度. 例如, 当前事务能否感知到其他事务 未提交的写请求
  • 传播性(Propagation): 通常, 在一个事务的范围内执行的代码也都在那个事务里面执行. 然而, 你可以 拥有定义当一个事务相关的方法在一个事务上下文已经存在时的行为. 例如, 代码可以在已经存在的事务中继续 运行(通常情况都是); 或者将那已经存在的事务挂起然后新建一个事务. Spring提供所有和EJB CMT相似的 事务传播属性选项.
  • 超时(Timeout): 事务多长时间会计时归零并自动调用底层事务单元进行回滚.
  • 只读状态(Read-only status): 一个设置了只读的事务可以在你的代码需要读但是却不会修改数据的时候 使用. 只读的事务在进行用例优化的时候很有用, 例如当你使用Hibernate的时候.

不管你是在Spring中使用声明式还是编程式事务管理, 定义正确的PlatformTransactionManager的实现都是 绝对正确的. 你通常会通过依赖注入的方式定义实现.

<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>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

如果我们在使用Hibernate的时候可以像下面这样子来定义txManager:

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.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.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

在这所有的例子中,应用的代码都不需要变动. 你可以只通过更改事务配置来改变事务的管理,如果使用的注解的话,Spring事务就对代码做到来无侵入。

11.3 在事务中同步资源

现在你应该已经知道怎么创建不同的事务管理器了, 并且已经知道了他们是怎样将需要与事务同步的资源联系起来 的(例如DataSourceTransactionManager对应一个JDBC的DataSource, HibernateTransactionManager 对应一个Hibernate的SessionFactory, 等等). 本部分讲解程序代码如何直接或者间接地使用持久化API, 如JDBC, Hibernate, 或者JDO, 并且确保所需要的资源都被正确的创建、重用、清理. 本部分也会泰伦事务 同步机制之怎样通过和PlatformTransactionManager的关联触发(可选的)的.

11.3.1 高级同步方法

首选的方法是使用基于Spring的和持久化集成的API高级模板,或者使用原生的ORM API, 应用于事务支持型工厂bean 或者管理原生资源的工厂的代理. 这些事务型解决方案内建对资源创建、重用、清理、资源的可选事务同步以及 异常的映射的支持. 这样用户的数据访问代码就可以不再关心定位任务, 专心于非样板化的持久化逻辑. 通常, 你使用原生的ORM API或者使用样板化的方法来进行JDBC访问的话, 是使用JdbcTemplate. 这个解决方式 还会在本参考文档的后续章节中详细介绍.

11.3.2 低级的同步方法

像DataSourceUtils (JDBC), EntityManagerFactoryUtils (JPA),SessionFactoryUtils(Hibernate), PersistenceManagerFactoryUtils (JDO), 等等这些类都是属于低级方法中的.当你的代码想要直接使用那 有关本地持久化事务API的时候, 你需要让这些类明确Spring Framework框架管理的实例已经得到了,事务已经 同步好了(可选的),并且异常运行中的异常也都会映射到一个一致的API.

例如, 在JDBC的例子中, 在DataSource中代替传统的JDBC中的getConnection()方法, 赢回感兴趣使用 Spring的org.springframework.jdbc.datasource.DataSourceUtils类,就像下面这样:

Connection conn = DataSourceUtils.getConnection(dataSource);

如果存在一个已经和他同步(已连接)的事务, 那就返回它. 否则, 方法就会激发一个触发器创建一个新的连接, 并且是(可选的)与任何存在的事务同步的, 并且已经准备好在接下来在相同的事务中重用. 就像提到的那样, 所有 的SQLException都会被包装成Spring Framework的CannotGetJdbcConnectionException, 这是 Spring Framework的非检查型数据访问异常(DataAccessExceptions)的一种层次. 这个方法给你的信息比 SQLException给你的信息多, 并且确保跨数据库, 即使是不同的持久化技术的可移植性.

该方法同样可以独立于Spring事务管理工作(事务同步是可选的), 所以你可以使用它不管你是使用或者不使用 Spring的事务管理.

11.4 声明式事务管理(大多数Spring Framework的用户选择声明式事务管理. 这种方式对应用代码的影响最小, 并且最符合一个非 侵入型轻量级容器的理想.)

Spring Framework的声明式事务管理是建立在Spring的面向切面编程(aspect-oriented programming, AOP) 上的.Spring Framework的声明式事务管理在指定事务行为(或者缺少它)下降至单个代码层次的方面和EJB的CMT很像. 它能在必要的时候使得一个setRollbackOnly()调用能够在一个事务上下文中进行。
回滚规则的概念是重要的: 他们让你可以指定哪些异常(或者Throwable)应该触发自动的回滚. 你指定这些通过在 配置文件中声明的方式, 不是在Java代码中. 所以, 虽然你仍然可以在TransactionStatus对象上调用 setRollbackOnly()来让当前事务回滚, 但是你大多数时候会指定一条规则让MyApplicationException必须 回滚. 这样做的显著优点是业务对象不会依赖于事务的底层组成. 例如, 他们通常不会引入Spring事务的API或者 其他Spring的API.

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

这是要告诉你简单的为你的类注释上@Transactional的注释, 为配置加上@EnableTransactionManagement 是不够充分的, 除非你理解了他们全部是如何工作的. 本章将向你讲解Spring Framework内部声明式事务管理的 组件在事务相关问题出现时的工作机制.

掌握Spring Framework声明式事务支持的关键是这个支持是通过AOP 代理起作用的, 以及事务声明是由元数据(metadata) (现在是XML配置或者基于注解的)驱动的. AOP和 事务型的元数据组合让步于使用AOP代理的TransactionInterceptor的PlatformTransactionManager实现 来驱动方法级响应.

11.4.2 声明式事务实现的例子

请参考下面的接口以及他们相关的实现. 这个例子使用Foo和Bar类来表示占位符, 所以你可以专心于事务的 使用方法而不必关心特殊的域模型. 这个例子的目的是说明在DefaultFooService类的实现的每一个方法中 抛出UnsupportedOperationException异常的实例是很好的; 它允许你观察事务创建以及回滚来响应 UnsupportedOperationException异常的实例.

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}

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)必须在可读可写类型 语义的事务上下文环境中执行. 下面的配置的详细解释将在接下来的段落中进行.

<?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"/>

    <!-- 事务化配置(请看下面的<aop:advisor/>) -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- 事务语义... -->
        <tx:attributes>
            <!-- 所有用'get'开头的方法都是只读的 -->
            <tx:method name="get*" read-only="true"/>
            <!-- 其他的方法使用默认的事务配置(看下面) -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 使得上面的事务配置对FooService接口的所有操作有效 -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>

    <!-- 不要忘了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>

    <!-- 同样的, 也不要忘了PlatformTransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 关于其他的<bean/>的定义 -->

</beans>

检查前面的配置. 你想让一个服务层对象, 就是fooService这个bean, 支持事务. 应用的关于事务语义的封装 是定义在<tx:advice/>的. 那<tx:advice/>的定义的意思就是"… 所有以'get'开头的方法都运行 在只读的事务语义中, 并且其他的所有方法都运行在默认的事务语义中". <tx:advice/>标签的 transaction-manager属性就是用来设置用来驱动事务的beanPlatformTransactionManager的名称, 在这里就是txManager这个bean.

如果你打算填写在事务配置(<tx:advice/>)的transaction-manager属性的PlatformTransactionManager bean的名称是transactionManager, 你可以忽略掉. 如果你想写的PlatformTransactionManagerbean是 其他的名称, 你就必须要使用transaction-manager属性来指明, 就像上面的例子.

<aop:config/>的定义确保了由txAdvice这个bean定义的事务配置在程序合适的切入点运行. 首先需要定义 一个切入点来匹配FooService( fooServiceOperation)这个接口定义的任何操作. 然后用一个顾问(advisor) 将切入点与txAdvice关联起来. 这样做的结果就是使用txAdvice定义的配置会在fooServiceOperation 上面工作起来.
上面的配置将会在由fooService这个bean的定义创建的对象之上创建一个事务型的代理. 这个代理将会使用 事务配置践行配置, 所以当合适的方法在这个代理上被调用的时候, 一个事务是被开启、悬空、标记为只读 还是怎么样, 这得取决于这个方法的事务配置语义。
上面的程序执行起来的输出看起来像这样. (为了看起来更直观, 已经将Log4J的输出和由DefaultFooService 类的insertFoo(..)方法抛出的UnsupportedOperationException堆栈进行了摘除处理.)

<!-- Spring容器正在启动... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean fooService with 0 common interceptors and 1 specific interceptors

<!-- DefaultFooService是真的被代理了 -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]

<!-- ... insertFoo(..)方法现在是被代理调用的 -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo

<!-- 事务配置切入在这里... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction

<!-- DefaultFooService的insertFoo(..)抛出了一个异常... -->
[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]

<!-- 然后事务就回滚了 (默认情况下, RuntimeException会导致回滚) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource

Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- 为了直观, AOP组件的堆栈已经移除了-->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)

11.4.3 回滚一个声明式事务

让Spring Framework事务的基础构件知道事务需要进行回滚的推荐做法是在正在执行的代码的当前上下文中抛出 Exception. Spring Framework事务的基础构件将会在调用栈中出现未处理的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 Framework的事务基础构件提交所进行的事务即使出现了未处理的InstrumentNotFoundException.

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

当Spring Framework的事务基础构件捕获了一条被多个参考配置确定是否需要回滚的异常时, 那一条最精确 的将生效.所以在下面的配置中, 除了InstrumentNotFoundException的所有异常都将被标记为回滚.

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

你也可以以编程的方式标明一个需要回滚的地方. 尽管是很简单的, 但是也很具有侵入性, 并且将你的代码同 Spring Framework的事务基础构件紧耦合在了一起。强烈建议你在所有可能的情况下都使用声明式的方法让事务回滚. 编程式的事务回滚在你迫不得已的时候也是可行的, 但他的用例运行在在实现一个基于POJO的架构中.

11.4.4 为不同的bean配置不同的事务

考虑这样一个场景, 你在服务层有大量的对象, 并且你想对它们每一个都应用完全不同的事务配置. 你完成 这个事情是使用了不同的pointcut和advice-ref属性的值来定义了不同的<aop:advisor/>元素.

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

    <!-- 这个bean是事务型的(查看'defaultServiceOperation'切入点) -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- 这个bean也是事务型的, 但是它拥有完全不一样的事务配置 -->
    <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>

    <!-- 省略其他如PlatformTransactionManager的事务基础构件的配置... -->

</beans>
11.4.5 <tx:advice/> 设置

本章总结整理可以使用<tx:advice/>标签指定的各种设置. <tx:advice/>标签默认的设置是:

  • 传播行为设置是REQUIRED.
  • 隔离等级是DEFAULT.
  • 事务是可读可写.
  • 事务超时是使用系统底层组件的默认值, 在不支持超时的时候没有超时.
  • 任何的RuntimeException均触发回滚, 并且检查的Exception不会.
    <tx:method/>设置可以设置的属性如下:
  • name:事务属性所关联的方法名称(可能不唯一). 通配符()可以用于表示一组相同的方法; 例如, get, handle, onEvent, 等等.
  • propagation:事务传播行为.
  • isolation:事务隔离等级.
  • timeout:事务超时的值(以秒为单位).
  • read-only:事务是不是只读的
  • rollback-for:会触发回滚的Exception(可能不唯一); 使用逗号分隔. 例如, com.foo.MyBusinessException,ServletException.
  • no-rollback-for:不会触发回滚的Exception(可能不唯一); 使用逗号分隔. 例如, com.foo.MyBusinessException,ServletException.
11.4.6 @Transactional 的使用

作为使用基于XML配置声明式事务配置方法的补充, 你可以使用一种基于注解的方法. 直接在Java代码中声明事务 语义声明使得声明更加靠近生效的代码. 这不存在过度危险的耦合, 因为不管怎么说开发代码的就意味着这样 被事务化地使用.

// 我们想要支持事务的服务类
@Transactional
public class DefaultFooService implements FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);
}

<!-- 来自文件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">

    <!-- 这就是我们想要使之支持事务的对象 -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- 使使用注解配置的事务行为生效 -->
    <tx:annotation-driven transaction-manager="txManager"/><!-- 仍然需要一个PlatformTransactionManager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- (这个需要的对象是在其他地方定义的) -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 其他<bean/>的定义 -->

</beans>

如果你想在<tx:annotation-driven/>标签里面的transaction-manager属性值写的 PlatformTransactionManager对象的bean名字是transactionManager的话可以忽略. 如果你需要 依赖注入的PlatformTransactionManagerbean的名字是另外的, 你需要想前面例子中的那样使用transaction-manager属性来指定.

方法可见性和@Transactional

当使用代理时, 你应该只给public可见性的方法添加@Transactional注解. 如果你给protected, private或者包访问的方法添加了@Transactional注解, 不会产生错误, 但是添加了注解的方法并没有真正的 配置了事务. 如果你需要给非公开的方法添加注解可以参考AspectJ (参看后面).

你可以把@Transactional注解添加在接口定义、接口中的方法定义、类定义、或者一个类中public方法的 前面. 然而, 仅仅有@Transactional注解的存在还不足以使事务的行为生效. @Transactional注解仅仅是 一个用来让某些运行期@Transactional-发现的基础构件来发现的元数据, 并且这些发现还会使用这个元数据 来配置bean的事务行为. 在前面的例子中, 元素<tx:annotation-driven/>开启了事务行为.

Spring建议你只为具体类(以及具体类的方法)添加@Transactional注解, 而不要给接口添加注解. 你当然 也可以给接口(或者接口中的方法)添加注解, 但是这只会在你期望的使用的代理时基于接口的时候工作. Java中的 注解不会从接口继承的事实意味着如果你是使用的基于类的代理( proxy-target-class="true")或者基于 编织的方面( mode="aspectj"), 那么关于事务的设置不会被代理或者编织的基础设施注册, 并且对象就不会 被事务型的代理包装, 而这当然是不好的.

在代理模式下(默认值), 只有来自外部方法的调用才会被代理拦截. 这意味着自我调用, 在效果上是, 目标对象的 一个方法调用了目标对象的另一个方法, 不会导致产生运行期的事务, 即使被调用的方法被@Transactional 标记了.

在决定方法的事务设置时, 最精确的配置优先. 在下面的例子中, DefaultFooService是一个在类级别使用只读 事务设置的类, 但是在同一个类的updateFoo(Foo)方法上的@Transactional注解优先于在类级别的事务设置.

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

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

    // 该方法的设置更优先
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateFoo(Foo foo) {
        // do something
    }
}

@Transactional的默认设置和之前介绍的一样的,下面给出@Transactional的默认设置:

  • 传播设置是PROPAGATION_REQUIRED.
  • 隔离等级是ISOLATION_DEFAULT.
  • 事务是可读可写的.
  • 事务超时是使用底层事务系统的默认值, 或者在不支持时没有.
  • 任何的RuntimeException触发回滚, 并且所有的检查的Exception不触发.
@Transactional 使用多个事务管理器

大多数的Spring应用都只需要一个事务管理器, 但也存在你需要在一个单一应用中使用多个不同的事务管理器的情况. @Transactional注解的value属性可以用来指定要使用的不同的PlatformTransactionManager. 这可以是 bean的名称或者是事务管理器bean的修饰值. 例如, 要使用修饰符号, 下面的Java代码

public class TransactionalService {

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

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

    <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中的两个方法将会分别运行在独立的事务管理器中, 通过"order"和 "account"的修饰符来区分. 默认的<tx:annotation-driven>的目标bean的名称transactionManager 仍然将会在指定的PlatformTransactionManager的bean的修饰符号没有被找到的时候使用.
因为Spring的事务管理仍然是基于AOP的,所以在这里我们仍然可以叠加自己的处理逻辑。切面可以通过Orderd接口来决定具体的执行顺序,这个在之前介绍AOP的时候已经介绍过。所以如果我们想在事务执行的前后做一些操作的话,只需要定义一个Order更小的切面即可。

12. DAO 支持

12.1 介绍

Spring中对数据访问对象(DAO)的支持旨在简化Spring与数据访问技术的操作,使JDBC、Hibernate、JPA和JDO等采用统一的方式访问。 这就允许使用者在各种持久化技术之间能够相对轻松的进行切换,同时,使用者也不必担心每种不同技术造成的异常处理的差异。

12.2 一致的异常层次结构

Spring提供了一个方便地从特定技术异常(如SQLException)转换为以DataAccessException作为根异常的自身异常层次结构中的类。 异常层次结构中的类将原始异常进行了包装,所以,我们不用担心发生错误后异常信息丢失的问题。

除了JDBC异常以外,Spring也可以包装特定的Hibernate异常,可以将所有的checked异常(支持Hibernate 3.0以前的版本)转换为 一组集中的运行时异常,JDO和JPA异常也可以同样包装。这就允许使用者不必在DAO中写大量的烦人的死板的catch和throw语句以及对 应的异常声明 ,就可以处理绝大多数不可恢复的只能在特定层处理的持久化异常。(使用者也还可以再需要的地方捕获和处理异常。) 如上所述,JDBC异常(包括特定的数据库方言)也会转换为同样的级别,这就意味着使用者可以使用一个一致的编程模型来使用JDBC。

13.使用JDBC来访问数据库

13.1 了解Spring 的JDBC模块

我们首先要理解Spring对于JDBC支持的方面,以及Spring需要关心哪些方面,作为开发者又需要关注哪些方面:

具体的行为 Spring关心 开发者关心
定义链接(Connection)的参数 NO YES
如何打开链接 YES NO
指定SQL statement NO YES
声明参数并且提供参数值 NO YES
执行具体的sql语句 YES NO
准备好遍历结果 YES NO
遍历返回结果 NO YES
处理sql异常 YES NO
提交事务 YES NO
关闭查询相关内容 YES NO

从上表中我们可以看到Spring关心的事务都是与具体业务无关的通用流程,而我们只需要做好自己的业务逻辑就行来。

13.1.1 选择一个访问JDBC数据库的方法

你可以选择多个方法去访问JDBC数据库。除了JdbcTemplate和优化了数据库元数据的SimpleJdbcInsert和SimplejdbcCall之外,RDBMS对象则使用了类似JDOQuery设计风格的更为面向对象的方式。一旦你开始使用其中的一种方法,你可以在一个功能里混合使用不同的方法。值得注意的是,所有的方法都需要JDBC 2.0-compliant驱动的支持,一些高级特性甚至需要JDBC 3.0的支持。

  • JdbcTemplate是经典的,也是最流行的Spring JDBC方式。这是最低层次的访问方式,所有其他的方法底层都调用了JdbcTemplate。
  • NamedParameterJdbcTemplate包装了JdbcTemplate,它提供了可命名的参数方式代替了传统的JDBC"?"占位符。当你在一个语句中有多个参数时,这种方法提供了更好的文档,使用起来也更方便。
  • SimpleJdbcInsert和SimpleJdbcCall优化了数据库元数据来减少必需的配置。这种方式简化了编码,你只需要提供表名或者存储过程和列名对应的参数。但只有当数据库提供适当的元数据时才能正常工作。如果数据库没有提供,你就不得不提供显示的参数配置。
  • RDBMS对象包含了MappingSqlQuery,SqlUpdate和StoredProcedure,它需要你在初始化数据访问层的时候提供一个可重用的线程安全的对象。这种方式类似于JDO查询,其中,你需要定义查询字符串,声明参数和编译查询语句。一旦你这样做了,使用传递的参数的execute方法就可以多次调用。

13.2 Spring处理基本的JDBC操作和异常信息

13.2.1 JdbcTemplate

JdbcTemplate就是spring jdbc包的核心类,他可以帮助我们创建和释放connection,这样子我们就不用每次都在finally中释放connection和其他的JDBC组件。JdbcTemplate会帮我们执行具体的sql,并且帮我们遍历ResultSet,而且还会帮我们捕捉JDBC异常信息,并且转换为一种更加通用的异常信息来方便我们发现问题。
JdbcTemplate中所有的SQL处理信息日志默认全都是Debug级别的,这一点需要注意下。
下面我们简单看一些JdbcTemplate的使用例子:

//无参单结果查询
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 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;
            }
        });
//多参更新
this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");
        
//无参更新
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");

//批量查询,查询结果为Map
List<Map<String, Object>> resultMap = this.jdbcTemplate.queryForList("select * from mytable");
JdbcTemplate最佳实践

JdbcTemplate一旦被配置好了就是线程安全。这个条件非常重要,因为你可用同一个JdbcTemplate然后注入到不同的Bean中。JdbcTemplate是有状态的,其维护了一个DataSource的引用。下面介绍一种通用的使用方式:

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

这样的话如果我们有多个DataSource的话可以方便的在不同的dao层使用不同的DataSource。但是一般情况下我们不会这么定义,每次都new一个JdbcTemplate不免得有点浪费,我们在一般情况下都只会有一个DataSource,所以在这种情况下定义一个JdbcTemplate就够了。

13.2.2 NamedParameterJdbcTemplate

NamedParameterJdbcTemplate主要是可以根据名字来进行变量的替换,而不是简单根据?顺序来决定参数的具体值。NamedParameterJdbcTemplate其实就是包装来JdbcTemplate,并且委托JdbcTemplate来完成具体的工作,自己只是参礼参数替换的逻辑。

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

对于具体的参数值与参数的具体映射关系,我们不仅可以通过SqlParameterSource来制定,还可以使用一个Map来处理这些参数映射关系。

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作为占位符替换的包装类,本质上就是通过人们可以读懂的key来替换掉?,那么我们肯定还可以通过Bean的形式来包装key-value了,Spring的设计者当然没有让我们失望,也提供了Bean的形式。

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

}

// 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,如果我们想从NamedParameterJdbcTemplate中获得 JdbcTemplate的话可以直接使用方法getJdbcOperations(),然后就可以通过JdbcTemplate来进行一些基本操作~

13.3 控制数据库连接

13.3.1 DataSource

在spring中我们通过一个DataSource来获得连接,我们可以把DataSource看成是一个连接的工厂,具体这个工厂是怎么产生和管理连接的,这些我们都不需要关注,可以来说DataSource产生连接的过程对我们来说就是黑盒,DataSource中可以利用连接池来管理连接,包括像C3p0或者dbcp这种连接池。在使用连接池的时候,我们只需要配置不同的DataSource就可以做到连接池的切换。下面简单看下:

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

13.4 JDBC的批处理

我们可以通过实现BatchPreparedStatementSetter的两个简单方法来通过JdbcTemplate完成批量操作。通过实现getBatchSize来确认本次batch的大小,通过setValue方法来设置具体的参数,这个方法被调用的次数和getBatchSize的次数一样。下面让我们简单看一下具体例子:

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) {
        int[] updateCounts = 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();
                    }
                });
        return updateCounts;
    }
}
13.4.2 通过List结构来完成批量操作

JdbcTemplate和NamedParameterJdbcTemplate都可以提供另外一种方式来进行批量更新操作,通过这种方式你不用实现一个接口而是通过一个List来提供参数信息,Spring框架会循环遍历这些数据然后自动地填充。参数的包装类的话还是可以用上面介绍的三种:SqlParameterSource,Map和JavaBean。

public class JdbcActorDao implements ActorDao {
    private NamedParameterTemplate namedParameterJdbcTemplate;

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

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

以上的内容会返回一个int数组,数组的内容对应着每一行更新语句的影响行数,如果影响的行数信息娶不到的话,JDBC驱动会返回-2.
有的时候如果Batch的数量太多的话我们需要将一个batch分割成多个Batch,Spring也是支持这种做法的。但是在这个时候返回的信息就不是一个单层数组来,而是一个两层数组信息,第一层代表内部batch的index,第二层代表具体的batch在内部的index值,下面我们来看一个简单的例子:

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

13.6 利用java对象操作JDBC

org.springframework.jdbc.object包中包含了许多类, 这些类提供了更为面向对象的方法来访问数据库。举例来说,你可以通过执行查询,就可以获取包含业务对象的结果集,而这些业务对象和数据库记录是一一映射的。你也可以执行存储过程,或者执行update, delete, 和insert语句

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容