继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Spring 5 中文解析数据存储篇-事务同步和声明式事物管理

youngitman
关注TA
已关注
手记 53
粉丝 1
获赞 7
1.3 资源与事务同步

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

1.3.1 高级同步方法

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

1.3.2 低级同步方法

诸如DataSourceUtils(对于JDBC) 、EntityManagerFactoryUtils(对于JPA)、SessionFactoryUtils(对于Hibernate)等类在较低级别存在。当你希望应用程序代码直接处理本地持久性API的资源类型时,可以使用这些类来确保获取正确的Spring框架管理的实例,(可选)同步事务以及处理过程中发生的异常。正确映射到一致的API。

例如,对于JDBC,你可以使用Spring的org.springframework.jdbc.datasource.DataSourceUtils类,而不是使用传统的JDBC方法在DataSource上调用getConnection()方法:

Connection conn = DataSourceUtils.getConnection(dataSource);

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

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

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

1.3.3 TransactionAwareDataSourceProxy

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

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

1.4 声明式事物管理

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

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

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

  • 与绑定到JTAEJB CMT不同,Spring框架的声明式事务管理可在任何环境中使用。通过调整配置文件,它可以使用JDBCJPAHibernate来处理JTA事务或本地事务。
  • 你可以将Spring框架声明式事务管理应用于任何类,而不仅限于EJB之类的特殊类。
  • Spring框架提供了声明式回滚规则,此功能EJB没有。提供了对回滚规则的编程和声明性支持。
  • Spring框架允许你使用AOP自定义事务行为。例如,在事务回滚的情况下,你可以插入自定义行为。你还可以添加任意通知以及事务通知。使用EJB CMT,除非使用setRollbackOnly(),否则你不能影响容器的事务管理。
  • Spring框架不像高端应用程序服务器那样支持跨远程调用传播事务上下文。如果需要此功能,建议你使用EJB。但是,在使用这种功能之前,请仔细考虑,因为通常情况下,你不希望事务跨远程调用。

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

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

1.4.1 理解Spring 框架的声明式事物实现

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

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

Spring AOP在AOP部分中介绍。

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

事务管理风格影响需要哪个事务管理器。命令式事务需要PlatformTransactionManager,而响应式事务则使用ReactiveTransactionManager实现。

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

tx

1.4.2 声明式事物例子

考虑以下接口及其附带的实现。本示例使用FooBar类作为占位符,以便你可以专注于事务使用而不关注特定的域模型。就本示例而言,DefaultFooService类在每个已实现方法的主体中引发UnsupportedOperationException实例的事实是很好的。该行为使你可以查看正在创建的事务,然后回滚以响应UnsupportedOperationException实例。以下清单显示了FooService接口:

// the service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Foo getFoo(String fooName);

    Foo getFoo(String fooName, String barName);

    void insertFoo(Foo foo);

    void updateFoo(Foo foo);

}

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

package x.y.service;

public class DefaultFooService implements FooService {

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

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

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

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

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

<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 该bean具有事物 -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>

    <!-- 事物通知 -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!-- the transactional semantics... -->
        <tx:attributes>
            <!-- 以‘get’开头的方法具有只读事物设置 -->
            <tx:method name="get*" read-only="true"/>
            <!-- 其他使用默认事物设置 -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 定义事物切面 -->
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
      <!--使用advisor绑定事物通知与切面-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </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>

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

</beans>

检查前面的配置。它假定你要使服务对象fooService bean成为事务性的。要应用的事务语义封装在<tx:advice/>定义中。<tx:advice/>定义为“以get开头的所有方法都将在只读事务的上下文中运行,而所有其他方法都将以默认事务语义运行”。<tx:advice/>标记的transaction-manager属性设置为要驱动事务的TransactionManager bean的名称(在本例中为txManager bean)。

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

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

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

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

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

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

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

前面显示的配置用于围绕从fooService bean定义创建的对象创建事务代理。代理配置有事务通知,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,事务将被启动、挂起、标记为只读等等。

考虑下面的程序,该程序测试驱动前面显示的配置:

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

参考代码:org.liyong.dataaccess.starter.DeclarativeTransactionManagerIocContainer

运行上述程序的输出类似以下内容(为清晰起见,DefaultFooService类的insertFoo(..)方法抛出的Log4J输出和UnsupportedOperationException的堆栈跟踪已被截断):

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

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

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

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

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

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

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

要使用响应式事务管理,代码必须使用响应式类型。

Spring框架使用ReactiveAdapterRegistry来确定方法返回类型是否为响应式。

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

// the reactive service interface that we want to make transactional

package x.y.service;

public interface FooService {

    Flux<Foo> getFoo(String fooName);

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

    Mono<Void> insertFoo(Foo foo);

    Mono<Void> updateFoo(Foo foo);

}

下面例子显示前面接口的实现:

package x.y.service;

public class DefaultFooService implements FooService {

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

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

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

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

命令式和响应式事务管理对事务边界和事务属性定义共享相同的语义。命令式事务和响应式事务的主要区别在于后者的延迟特性。TransactionInterceptor用事务运算符修饰返回的响应类型,以开始和清理事务。因此,调用事务响应性方法将实际事务管理延迟到激活响应性类型的处理的订阅类型。

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

命令式事务的方法返回值将在方法成功终止时从事务性方法返回,以便部分计算的结果不会脱离方法闭包。

响应式事务方法返回一个响应式包装器类型,该类型表示一个计算序列以及一个开始和完成计算的承诺(译者:类似Java中的Future的概念)。

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

1.4.3 回滚声明式事物

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

要向Spring框架的事务基础结构表明事务的工作要回滚,推荐的方法是从当前在事务上下文中执行的代码中抛出异常。Spring 框架的事务基础结构代码捕获了所有未处理的Exception,因为它使调用栈冒泡方式,并确定是否将事务标记为回滚。

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

你可以准确配置哪些异常类型将事务标记为回滚,包括检查的异常。以下XML代码段演示了如何为检查、特定于应用程序的异常类型配置回滚:

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

如果你不希望在引发异常时回滚事务,则还可以指定“不回滚规则”。以下示例告诉Spring框架的事务基础结构即使在未处理InstrumentNotFoundException也要提交事务:

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

当Spring框架的事务基础结构捕获到异常并咨询已配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则获胜。因此,在以下配置的情况下,除InstrumentNotFoundException之外的任何异常都将导致事务的回滚:

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

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

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

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

参考代码:org.liyong.dataaccess.starter.RollBackDeclarativeTransactionManagerIocContainer

1.4.4 为不同bean配置不同事物语义

考虑以下场景:你有许多服务层对象,并且你希望对每个对象应用完全不同的事务配置。你可以通过定义具有不同切入点和advice-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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

   <!-- 定义切入点和事物通知 -->
    <aop:config>

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

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

    </aop:config>

    <!-- 在事物中执行 -->
    <bean id="fooService" class="x.y.service.DefaultFooService"/>
    <bean id="barService" class="x.y.service.extras.SimpleBarService"/>

    <!-- ... 不在事物中执行 -->
    <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
    <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

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

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

</beans>

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

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

    <aop:config>

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

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

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

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

    </aop:config>

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

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

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

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

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

</beans>

参考代码:org.liyong.dataaccess.starter.DefferentBeanTransactionManagerIocContainer

1.4.5 <tx: advice/> 配置

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

  • 传播设置REQUIRED
  • 隔离级别为DEFAULT
  • 事务是读写。
  • 事务超时默认为基础事务系统的默认超时,如果不支持超时,则默认为无。
  • 任何RuntimeException都会触发回滚,而任何检查的Exception都不会触发。

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

属性 必填 默认值 描述
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.MyBusinessExceptionServletException
no-rollback-for No 不触发回滚的Exception实例的逗号分隔列表。例如,com.foo.MyBusinessExceptionServletException
1.4.6 使用@Transactional

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

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

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

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

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

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

    void insertFoo(Foo foo) {
        // ...
    }

    void updateFoo(Foo foo) {
        // ...
    }
}

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

当将一个以上的POJO类在Spring上下文中定义为bean时,可以通过@Configuration类中的@EnableTransactionManagement注解使bean实例具有事务性。有关完整的详细信息,请参见javadoc

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

<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

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

    <!-- enable the configuration of transactional behavior based on annotations -->
    <tx:annotation-driven transaction-manager="txManager"/><!-- a TransactionManager 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>
  1. 使bean实例具有事务性的行为

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

相对于命令式编程,响应式事务方法使用响应式返回类型,如下清单所示:

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

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

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

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

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

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

方法可见性和@Transactional

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

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

Spring团队推荐仅使用@Transactional注解对具体类(以及具体类的方法)进行注解,而不是对接口进行注解。你当然可以在接口(或接口方法)上放置@Transactional注解,但这仅在你使用基于接口的代理时才可以达到预期。Java注解不能从接口继承的事实意味着,如果你使用基于类的代理(proxy-target-class="true")或基于编织的切面(mode =“aspectj”),则事务设置不会由代理和编织基础结构识别,并且该对象未包装在事务代理中。

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

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

XML属性 注解属性 默认值 描述
transaction-manager N/A (查看 Transaction-ManagementConfigurer-javadoc) transactionManager 要使用的事务管理器的名称。如上例所示,仅当事务管理器的名称不是transactionManager时才需要。
mode mode proxy 默认模式(代理)通过使用Spring的AOP框架来处理带注解的bean(遵循代理语义,如前所述,仅适用于通过代理传入的方法调用)。替代模式(aspectj)则将受影响的类与Spring的AspectJ事务切面进行编织,修改目标类字节码以应用于任何类型的方法调用。AspectJ编织需要在类路径中使用spring-aspects.jar并启用加载时编织(或编译时编织)。(有关如何设置加载时编织的详细信息,请参见Spring配置。)
proxy-target-class proxyTargetClass false 仅适用于代理模式。控制为使用@Transactional注解注释的类创建哪种类型的事务代理。如果proxy-target-class属性设置为true,则将创建基于类的代理。如果proxy-target-classfalse或省略了属性,则将创建基于标准JDK接口的代理。(有关不同代理类型的详细检查,请参见代理机制。)
order order Ordered.LOWEST_PRECEDENCE 定义应用于带@Transactional注解的bean的事务通知的顺序。(有关AOP通知排序相关规则的更多信息,请参见通知顺序。)没有指定的顺序意味着AOP子系统确定通知的顺序。

处理@Transactional注解的默认通知模式是代理,它仅允许通过代理拦截调用。同一类内的本地调用无法以这种方式被拦截(自调用)。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到Aspectj模式。

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

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

在评估方法的事务设置时,最派生的位置优先(译者:范围最小的优先级越高,例如:类和方法配置时方法优先级更高)。在下面的示例中,DefaultFooService类在类级别使用只读事务的设置进行注解,但是同一类中updateFoo(Foo)方法上的@Transactional注解优先于定义的事务设置在类级别上。

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

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

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

@Transactional设置

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

  • 事物传播设置为PROPAGATION_REQUIRED
  • 事物隔离级别为ISOLATION_DEFAULT
  • 事务是读写的。
  • 事务超时默认为基础事务系统的默认超时,如果不支持超时,则默认为无。
  • 任何RuntimeException都会触发回滚,而任何检测的Exception都不会触发。

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

属性 类型 描述
value String 可选的限定符,指定要使用的事务管理器。
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 字符串类名称的数组,必须从Throwable派生。 不能引起回滚的异常类名称的可选数组。

当前,你无法对事务名称进行显式控制,其中“名称”是指显示在事务监视器(如果适用)(例如,WebLogic的事务监视器)和日志输出中的事务名称。对于声明式事务,事务名称始终是全限定类名称+’.’ +通知类的方法名称。例如,如果BusinessService类的handlePayment(..)方法启动了事务,则事务的名称将为:com.example.BusinessService.handlePayment

具有@Transactional的多个事务管理器

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

public class TransactionalService {

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

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

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

以下清单显示了bean声明:

<tx:annotation-driven/>

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

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

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

在这种情况下,TransactionalService上的各个方法在单独的事务管理器下运行,并根据orderaccountreactive-account限定符进行区分。如果未找到特别限定的TransactionManager bean,则仍使用默认的<tx:annotation-driven>目标bean名称transactionManager

参考代码:org.liyong.dataaccess.starter.QualifierAnnotationTransactionManagerIocContainer

自定义组合的注解

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

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("order")
public @interface OrderTx {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}

前面的注解使我们可以按照上一节的内容编写示例,如下所示:

public class TransactionalService {

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

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

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

参考代码:org.liyong.dataaccess.starter.CustomAnnotationTransactionManagerIocContainer

1.4.7 事物传播属性

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

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

理解PROPAGATION_REQUIRED

tx prop required

PROPAGATION_REQUIRED强制执行一个物理事务,如果尚不存在当前事务,则在本地为当前范围创建新事物执行,或参与为更大范围定义的现有“外部”事务。在同一线程内的公共调用堆栈中,这是一个很好的缺省值(例如,一个服务facade委托给几个存储库方法,其中所有底层资源都必须参与服务级事务)。(译者:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务)

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

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

但是,在内部事务范围设置回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围默默触发)是意外的。此时将引发相应的UnexpectedRollbackException。这是预期的行为,因此永远不会误导事务的调用方以假定在确实未执行提交的情况下执行该提交。因此,如果内部事务(外部调用者不知道)将事务无提示地标记为只回滚,则外部调用者仍会调用commit。外部调用者需要接收一个UnexpectedRollbackException,以清楚地表示已执行回滚。

理解PROPAGATION_REQUIRES_NEW

tx prop requires new

PROPAGATION_REQUIRED相比,PROPAGATION_REQUIRES_NEW始终对每个受影响的事务范围使用独立的物理事务,而从不参与外部范围的现有事务。在这种安排中,基础资源事务是不同的,因此可以独立地提交或回滚,而外部事务不受内部事务回滚状态的影响,并且内部事务在完成后立即释放锁。这种独立的内部事务还可以声明其自己的隔离级别、超时和只读设置,而不会继承外部事务的特征。(译者:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。)

理解PROPAGATION_NESTED

PROPAGATION_NESTED使用具有多个保存点的单个物理事务,它可以回滚到该保存点。这种部分回滚使内部事务范围触发其范围的回滚,尽管某些操作已回滚,但外部事务仍能够继续物理事务。此设置通常映射到JDBC保存点,因此仅适用于JDBC资源事务。请参阅Spring的DataSourceTransactionManager。(译者:如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行)

参考代码:org.liyong.dataaccess.starter.PropagationTransactionManagerIocContainer

1.4.8 通知事务操作

假设你要同时运行事务性操作和一些基本的配置通知。你如何在<tx:annotation-driven/>的上下文中实现此目的?

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

  • 已配置的分析切面启动。
  • 事务通知运行。
  • 通知对象上的方法运行。
  • 事物提交。
  • 分析切面报告整个事务方法调用的确切持续时间。

本章不涉及任何详细的AOP解释(除非它适用于事务)。有关AOP配置和AOP的详细介绍,请参见AOP。

以下代码显示了前面讨论的简单配置方面:

package x.y;

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

public class SimpleProfiler implements Ordered {

    private int order;

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

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

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

通知的排序是通过Ordered接口控制的。有关通知顺序的完整详细信息,请参阅通知顺序。以下配置创建一个fooService bean,该bean具有按所需顺序应用的概要分析和事务切面:

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

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

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

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

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

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

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

</beans>

你可以以类似的方式配置任意数量的其他切面。

下面的示例创建与前两个示例相同的设置,但是使用纯XML声明性方法:

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

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

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

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

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

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

    </aop:config>

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

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

</beans>

上述配置的结果是fooService bean,它按此顺序应用了分析和事务切面。如果你希望性能分析通知在进来的事务处理通知之后和事务处理通知之前运行,则可以交换性能分析切面bean的order属性的值,使其高于事务处理通知的order值。

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

1.4.9 在AspectJ中使用@Transactional

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

在继续之前,你可能需要分别阅读《使用@Transactional》和《 AOP》。

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

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

// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);

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

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

类中方法上的@Transactional注解将覆盖类注解(如果存在)给出的默认事务语义。你可以注解任何方法,而不管可见性如何(可见性:publicprivate等)。

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

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

博客地址: http://youngitman.tech

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP