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


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

        告诉你简单的为你的类注释上@Transactional的注释, 为配置加上@EnableTransactionManagement 是不够充分的, 除非你理解了他们全部是如何工作的.

        从概念上来讲, 在事务型代理上调用一个方法看起来像这样…



2 声明式事务实现的例子

//我们想使之支持事务的服务层接口

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

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

    <!-- 事务化配置(请看下面的<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属性就是用来设置用来驱动事务的bean`PlatformTransactionManager`的名称, 在这里就是txManager这个bean.

<aop:config/>的定义确保了由txAdvice这个bean定义的事务配置在程序合适的切入点运行. 首先需要定义 一个切入点来匹配FooServicefooServiceOperation)这个接口定义的任何操作. 然后用一个顾问(advisor) 将切入点与txAdvice关联起来. 这样做的结果就是使用txAdvice定义的配置会在fooServiceOperation 上面工作起来.

让整个服务层都是事务型的是一个通常的需求. 要这么做的最好方式就是简单的修改切入点表达式, 使之能够匹配 服务层所有的操作. 就像下面这样:

<aop:config>
    <aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>


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>


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

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

作为一个出发点, 首先假设你服务层所有的类都定义在根包x.y.service中. 为了让在这个包(或者他的子包) 中定义的所有以Service结尾的类的所有实例都具有默认事务配置, 你将会进行如下配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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>

    <!-- 这两个bean将支持事务... -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... 而这两个bean将不支持 -->
    <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>

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

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

    <!-- 这个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>


5 <tx:advice/> 设置

<tx:advice/>标签指定的各种设置. <tx:advice/>标签默认的设置是:

  • 传播行为设置REQUIRED.
  • 隔离等级是DEFAULT.
  • 事务是可读可写.
  • 事务超时是使用系统底层组件的默认值, 在不支持超时的时候没有超时.
  • 任何的RuntimeException均触发回滚, 并且检查的Exception不会.

你可以修改默认的设置; <tx:advice/><tx:attributes/>标签所需要的<tx:method/>标签的属性都 整理在下面了:

Table 11.1. <tx:method/>设置


6 @Transactional 的使用

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

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

当在Spring IoC容器中定义上面的POJO时, 这个bean的实例就仅仅需要在XML配置添加行就可以添加 事务了:

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

方法可见性和@Transactional

        当使用代理时, 你应该只给public可见性的方法添加@Transactional注解. 如果你给protected, private或者包访问的方法添加了@Transactional注解, 不会产生错误, 但是添加了注解的方法并没有真正的 配置了事务. 

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

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

Table 11.2. 基于注解的事务设置


在决定方法的事务设置时, 最精确的配置优先. 在下面的例子中, 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注解是一个用来定义一个接口、类或者方法必须具备事务化语义的元数据; 例如, "在调用 该方法时挂起所有已经存在的事务,开始一个新的只读事务". 下面是@Transactional注解的默认设置:

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

这些默认设置都是可以修改的; @Transactional注解的各种属性都整理在下面的表格中了:

Table 11.3. @Transactional


7 事务传播性

请记住本部分并不是介绍事务的传播性本身; 尽管它比Spring 中的事务传播性更加详细.

在Spring的受管事务中, 存在物理逻辑事务的差别, 以及还有传播性的设置是怎样在这种差别上生效的.

需要 Required

Figure 11.2. 

PROPAGATION_REQUIRED

当传播属性设置为PROPAGATION_REQUIRED时, 将会为设置应用到的每一个方法创建一个逻辑上的事务 作用域. 这每一个单独的逻辑事务作用域可以单独的确定回滚状态, 在逻辑上独立于事务范围的外部事务范围. 当然, 考虑到标准的PROPAGATION_REQUIRED的行为, 所有的这些作用域都将会映射到相同的物理事务上. 因此, 在内部事务作用域中作的事务回滚标记确实会影响到外部事物实际上提交的可能性(这和你所期待的一样).

然而, 在内部事务作用域中标记了回滚, 外部事物决定它自己不回滚的情况下, 这样的回滚(由内部事务作用域 静默触发)就不是期待的了. 一个对应的UnexpectedRollbackException将会在在那里抛出. 这是一个异常 行为, 所以事务的调用者将不可能会在事务其实没有提交的时候被误导为假设提交了. 所以对于内部事务作用域 (在外部调用者没有发觉时)静默的标记了回滚的情况下, 外部调用者调用了提交. 那么外部调用者需要收到一个 UnexpectedRollbackException来清楚的知道需要用一个回滚来取而代之(提交).

需要新的 RequiresNew

Figure 11.3. 

PROPAGATION_REQUIRES_NEW

相比较于PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW对每一个受影响的事务作用域都使用完全 独立的事务. 这样, 物理上的事务就不同了并且可以独立的提交或者回滚, 外部事物不会影响到内部事务的回滚 状态.

嵌套 Nested

PROPAGATION_NESTED对多个可以回滚到的保存点使用了一个单独的底层事务. 这种局部化的回滚允许一个 内部事务触发一个针对它的作用域的回滚, 尽管一些操作已经回滚了, 但外部事物还是可以继续物理上的事务. 这个设置通常都和JDBC的保存点对应, 所以只会在JDBC的资源的事务上有作用. 请查看Spring的DataSourceTransactionManager.


8 通知事务操作

假设你想要同时执行事务型的一些基本的分析通知. 你怎样在<tx:annotation-driven/>的上下文 中体现?

当你执行updateFoo(Foo)方法时, 你期望看到下面的动作:

  • 配置了分析通知的切面启动.
  • 事务通知执行.
  • 被添加了通知的对象的方法执行.
  • 提交事务.
  • 分析切面报告整个事务方法执行的准确时间.

这里是上面讨论的简单分析切面的代码

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;

    // 允许我们对通知排序
    public int getOrder() {
        return this.order;
    }

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

    // 这个方法是关于通知
    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;
    }
}
<?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"/>

    <!-- 这是切面 -->
    <bean id="profiler" class="x.y.SimpleProfiler">
        <!-- 在事务通知之前执行(更低的排序) -->
        <property name="order" __value="1"__/>
    </bean>

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

    <aop:config>
            <!-- 这个通知将会在事务通知执行时执行 -->
            <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>

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐