Spring Framework对事务管理的支持

https://docs.spring.io/spring-framework/reference/data-access/transaction.html

全面的事务支持是使用Spring框架最引人注目的理由之一。Spring框架为事务管理提供了一个一致的抽象,带来了以下好处:

  • 在不同的事务API之间提供一致的编程模型,如Java事务API(JTA)、JDBC、Hibernate和Java持久化API(JPA)。
  • 支持声明式事务管理。
  • 与复杂的事务API(如JTA)相比,提供了更简单的编程式事务管理API。
  • 与Spring的数据访问抽象层有着出色的集成。

以下部分描述了Spring框架的事务特性和技术:

  • Spring框架的事务支持模型的优势,描述了为什么你会使用Spring框架的事务抽象,而不是使用EJB容器管理事务(CMT),或选择通过专有API来驱动事务。
  • 理解Spring框架的事务抽象概述了核心类,并描述了如何从各种来源配置和获取DataSource实例。
  • 将资源与事务同步描述了应用程序代码如何确保资源被正确创建、重用和清理。
  • 声明式事务管理描述了对声明式事务管理的支持。
  • 编程式事务管理涵盖了对编程式(即显式编码的)事务管理的支持。
  • 事务绑定事件描述了如何在事务中使用应用程序事件。

该章节还包括了最佳实践、应用服务器集成和常见解决方案的讨论。

Spring框架事务支持模型的优势

传统上,EE应用程序开发人员在事务管理方面有两种选择:全局或本地事务,这两种选择都有深刻的局限性。接下来的两个部分将回顾全局和本地事务管理,然后讨论Spring框架的事务管理支持如何解决全局和本地事务模型的局限性。

全局事务

全局事务允许你使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过JTA管理全局事务,这是一个繁琐的API(部分原因是它的异常模型)。此外,JTA UserTransaction 通常需要从JNDI中获取,这意味着为了使用JTA,还需要使用JNDI。全局事务的使用限制了应用程序代码的任何潜在重用,因为JTA通常只在应用服务器环境中可用。

以前,使用全局事务的首选方式是通过EJB CMT(容器管理事务)。CMT是一种声明式事务管理形式(与编程式事务管理相区别)。EJB CMT消除了与事务相关的JNDI查找的需要,尽管使用EJB本身需要使用JNDI。它减少了编写Java代码来控制事务的需求,但并非全部。显著的缺点是CMT与JTA和应用服务器环境紧密绑定。此外,只有在选择在EJB中实现业务逻辑时(或至少在事务性EJB外观后面),CMT才可用。EJB的负面因素如此之多,以至于这并不具有吸引力,尤其是在面对其他引人注目的声明式事务管理替代方案时。

本地事务(Local Transactions)

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

Spring框架的一致编程模型

Spring解决了全局和本地事务的缺点。它让应用程序开发人员在任何环境中都能使用一致的编程模型。你只需编写一次代码,就能在不同的环境中受益于不同的事务管理策略。Spring框架提供了声明式和编程式事务管理。大多数用户更喜欢声明式事务管理,这也是我们在大多数情况下推荐的。

在编程式事务管理中,开发人员使用Spring框架的事务抽象,它可以在任何底层事务基础设施上运行。在使用首选的声明式模型时,开发人员通常编写很少或没有与事务管理相关的代码,因此不依赖于Spring框架事务API或其他任何事务API。

你需要应用服务器进行事务管理吗?

Spring框架的事务管理支持改变了传统的规则,即何时企业Java应用程序需要应用服务器。

特别是,你不需要仅仅为了通过EJBs进行声明式事务而使用应用服务器。实际上,即使你的应用服务器具有强大的JTA功能,你也可能认为Spring框架的声明式事务提供了比EJB CMT更强大和更高产的编程模型。

通常,只有当你的应用程序需要处理跨多个资源的事务时,才需要应用服务器的JTA功能,而这并非许多应用程序的要求。许多高端应用程序使用单一、高度可扩展的数据库(如Oracle RAC)。独立的事务管理器(如Atomikos Transactions)是其他选择。当然,你可能需要应用服务器的其他功能,如Java消息服务(JMS)和Jakarta EE连接器架构(JCA)。

Spring框架为你提供了何时将应用程序扩展到完全加载的应用服务器的选择。有了Spring框架,你的配置文件中只需要更改一些bean定义(而不是你的代码)。

理解Spring框架事务抽象

Spring事务抽象的关键是事务策略的概念。事务策略由TransactionManager定义,具体来说,对于命令式事务管理,是org.springframework.transaction.PlatformTransactionManager接口;对于响应式事务管理,是org.springframework.transaction.ReactiveTransactionManager接口。以下列表显示了PlatformTransactionManager API的定义:

public interface PlatformTransactionManager extends TransactionManager {

	TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

	void commit(TransactionStatus status) throws TransactionException;

	void rollback(TransactionStatus status) throws TransactionException;
}

这主要是一个服务提供商接口(SPI),尽管你可以在应用程序代码中以编程方式使用它。因为PlatformTransactionManager是一个接口,所以可以根据需要轻松地进行模拟或打桩。它不依赖于查找策略,如JNDI。PlatformTransactionManager的实现像Spring框架IoC容器中的任何其他对象(或bean)一样定义。与直接使用JTA相比,你可以更容易地测试事务性代码。

符合Spring的理念,任何PlatformTransactionManager接口方法可能抛出的TransactionException是未检查的(即,它扩展了java.lang.RuntimeException类)。事务基础设施故障几乎总是致命的。在极少数情况下,应用程序代码实际上可以从事务失败中恢复,应用程序开发人员仍然可以选择捕获并处理TransactionException。关键点是,开发人员不被强迫这样做。

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

从Spring框架5.2开始,Spring还为使用响应式类型或Kotlin协程的响应式应用程序提供了事务管理抽象。以下列表显示了由org.springframework.transaction.ReactiveTransactionManager定义的事务策略:

public interface ReactiveTransactionManager extends TransactionManager {

	Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;

	Mono<Void> commit(ReactiveTransaction status) throws TransactionException;

	Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}

应式事务管理器主要是一个服务提供商接口(SPI),尽管你可以在应用程序代码中以编程方式使用它。因为ReactiveTransactionManager是一个接口,所以可以根据需要轻松地进行模拟或打桩。

TransactionDefinition接口指定:

  • 传播(Propagation):通常,事务范围内的所有代码都在该事务中运行。但是,如果事务上下文已经存在时运行事务方法,你可以指定行为。例如,代码可以继续在现有事务中运行(常见情况),或者可以挂起现有事务并创建新事务。Spring提供了所有来自EJB CMT的熟悉事务传播选项。
  • 隔离(Isolation):这个事务与其他事务的工作相隔离的程度。例如,这个事务能否看到其他事务未提交的写入?
  • 超时(Timeout):这个事务在超时并由底层事务基础设施自动回滚之前运行的时间。
  • 只读状态(Read-only status):当你的代码读取但不修改数据时,可以使用只读事务。在某些情况下,例如使用Hibernate时,只读事务可以是一个有用的优化。

这些设置反映了标准的事务概念。

TransactionStatus接口为事务性代码提供了一种简单的方式来控制事务执行和查询事务状态。这些概念应该是熟悉的,因为它们对所有事务API都是通用的。以下列表显示了TransactionStatus接口:

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {

	@Override
	boolean isNewTransaction();

	boolean hasSavepoint();

	@Override
	void setRollbackOnly();

	@Override
	boolean isRollbackOnly();

	void flush();

	@Override
	boolean isCompleted();
}

无论你选择在Spring中使用声明式还是编程式事务管理,定义正确的TransactionManager实现都至关重要。通常通过依赖注入来定义此实现。

TransactionManager实现通常需要了解它们所在的环境: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>

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

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

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

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

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

</beans>

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

前面对dataSource bean的定义使用了jee命名空间的标签。

如果使用JTA,无论你使用哪种数据访问技术,无论是JDBC、Hibernate JPA还是任何其他支持的技术,你的事务管理器定义都应该看起来相同。这是因为JTA事务是全局事务,可以将任何事务性资源列入其中。

在所有Spring事务设置中,应用程序代码不需要更改。即使这种变化意味着从本地事务转移到全局事务或反之,你也可以通过更改配置来改变事务的管理方式。

Hibernate事务设置

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

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

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

这种情况下的txManager bean是HibernateTransactionManager类型的。与DataSourceTransactionManager需要引用DataSource的方式相同,HibernateTransactionManager需要引用SessionFactory。以下示例声明了sessionFactorytxManager beans:

<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和Jakarta EE容器管理的JTA事务,应该使用与前面JDBC的JTA示例中相同的JtaTransactionManager,如下所示。此外,建议通过其事务协调器以及可能的连接释放模式配置使Hibernate意识到JTA:

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

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

或者,也可以将JtaTransactionManager传递给你的LocalSessionFactoryBean以强制使用相同的默认设置:

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

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

将资源与事务同步

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

高级同步方法

首选方法是使用Spring最高级别的基于模板的持久化集成API,或者使用带有事务感知工厂bean或代理的原生ORM API来管理原生资源工厂。这些事务感知解决方案在内部处理资源的创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以专注于非样板的持久化逻辑。通常,使用原生ORM API或采用模板方法进行JDBC访问,通过使用JdbcTemplate

低级同步方法

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或其他辅助类,因为通过Spring抽象工作比直接与相关API打交道要愉快得多。例如,如果你使用Spring JdbcTemplatejdbc.object包来简化JDBC的使用,正确的连接检索会在幕后发生,你无需编写任何特殊代码。

TransactionAwareDataSourceProxy

在最低级别上,存在TransactionAwareDataSourceProxy类。这是目标DataSource的代理,它包装了目标DataSource以增加对Spring管理事务的感知。在这方面,它类似于Jakarta EE服务器提供的事务性JNDI DataSource

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

声明式事务管理

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

Spring框架的声明式事务管理是通过Spring面向切面编程(AOP)实现的。然而,由于事务方面的代码随Spring框架分发并提供,并且可以以样板方式使用,因此通常不必理解AOP概念就能有效使用这些代码。

Spring框架的声明式事务管理类似于EJB CMT,你可以指定单个方法级别的事务行为(或缺乏事务行为)。如果必要,可以在事务上下文中调用setRollbackOnly()。这两种类型的事务管理之间的区别在于:

  • 与EJB CMT不同,后者与JTA绑定,Spring框架的声明式事务管理适用于任何环境。通过调整配置文件,它可以与JTA事务或本地事务一起工作,使用JDBC、JPA或Hibernate。
  • 可以将Spring框架的声明式事务管理应用于任何类,而不仅仅是像EJB这样的特殊类。
  • Spring框架提供了声明式的回滚规则,这是EJB没有的功能。同时提供了对回滚规则的编程式和声明式支持。
  • Spring框架允许你使用AOP自定义事务行为。例如,可以在事务回滚时插入自定义行为。还可以添加任意的通知(advice),以及事务通知(transactional advice)。使用EJB CMT时,除了setRollbackOnly()之外,你无法影响容器的事务管理。
  • Spring框架不支持事务上下文在远程调用之间的传播,就像高端应用服务器所做的那样。如果需要这个功能,建议使用EJB。然而,在使用此功能之前请仔细考虑,因为通常人们不希望事务跨越远程调用。

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

尽管EJB容器的默认行为是在系统异常(通常是运行时异常)时自动回滚事务,但EJB CMT不会在应用程序异常(即,除了java.rmi.RemoteException之外的已检查异常)上自动回滚事务。虽然Spring对声明式事务管理的默认行为遵循EJB约定(只在未检查异常上自动回滚),但自定义这种行为通常是有用的。

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

本节在与事务相关问题的背景下解释了Spring框架声明式事务基础设施的内部工作原理。

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

Spring框架的TransactionInterceptor为命令式和响应式编程模型提供了事务管理。该拦截器通过检查方法返回类型来检测所需的事务管理类型。返回响应式类型,如Publisher或Kotlin Flow(或它们的子类型)的方法,适用于响应式事务管理。所有其他返回类型,包括void,都使用命令式事务管理的代码路径。

事务管理类型影响了所需的事务管理器。命令式事务需要一个PlatformTransactionManager,而响应式事务使用ReactiveTransactionManager实现。

@Transactional通常与PlatformTransactionManager管理的线程绑定事务一起工作,将事务暴露给当前执行线程内的所有数据访问操作。注意:这不会传播到方法内新启动的线程。

ReactiveTransactionManager管理的响应式事务使用Reactor上下文而不是线程局部属性。因此,所有参与的数据访问操作需要在同一个Reactor上下文中的同一个响应式管道中执行。

当配置了ReactiveTransactionManager时,所有事务界定的方法都应返回一个响应式管道。void方法或常规返回类型需要与常规的PlatformTransactionManager关联,例如通过相应@Transactional声明的transactionManager属性。

下图显示了在事务代理上调用方法的概念视图:
在这里插入图片描述

声明式事务实现示例

考虑以下接口及其伴随的实现。此示例使用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">

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

	<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<!-- the transactional semantics... -->
		<tx:attributes>
			<!-- all methods starting with 'get' are read-only -->
			<tx:method name="get*" read-only="true"/>
			<!-- other methods use the default transaction settings (see below) -->
			<tx:method name="*"/>
		</tx:attributes>
	</tx:advice>

	<!-- ensure that the above transactional advice runs for any execution
		of an operation defined by the FooService interface -->
	<aop:config>
		<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
	</aop:config>

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

	<!-- similarly, don't forget the TransactionManager -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>

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

</beans>

检查前面的配置。它假设你希望使服务对象,即fooService bean,具有事务性。要应用的事务语义封装在<tx:advice/>定义中。<tx:advice/>定义的含义是“所有以get开头的方法都应在只读事务的上下文中运行,而所有其他方法都应使用默认的事务语义运行”。<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)。然后,通过使用顾问将切点与txAdvice关联起来。结果表明,在执行fooServiceOperation时,会运行txAdvice定义的通知。

<aop:pointcut/>元素内定义的表达式是一个AspectJ切点表达式。

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

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

在前面的示例中,假设你的所有服务接口都定义在x.y.service包中。

前面显示的配置用于在从fooService bean定义创建的对象周围创建一个事务代理。该代理配置了事务通知,以便在对代理调用适当的方法时,根据与该方法关联的事务配置启动、挂起、标记为只读等事务。考虑以下测试驱动先前显示配置的程序:

public final class Boot {

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

运行前面程序的输出应类似于以下内容(为了清晰起见,已截断Log4J输出和DefaultFooServiceinsertFoo(..)方法抛出的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使用事务操作符装饰返回的响应式类型,以开始和清理事务。因此,调用事务性的响应式方法会将实际的事务管理推迟到激活响应式类型处理的订阅类型。

响应式事务管理的另一个方面与数据逃逸(data escaping)有关,这是编程模型的自然结果。

命令式事务的方法返回值是在方法成功终止后从事务方法中返回的,这样部分计算的结果就不会逃离方法闭包。

响应式事务方法返回一个响应式包装器类型,它代表一个计算序列,并承诺开始并完成计算。

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

回滚声明式事务

本节描述如何在XML配置中以简单、声明式的方式控制事务的回滚。

向Spring框架的事务基础设施指示要回滚事务的工作的推荐方式是在当前事务上下文中执行的代码中抛出异常。Spring框架的事务基础设施代码会捕获任何在调用栈中冒泡上来的未处理异常,并决定是否将事务标记为回滚。

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

自从Spring框架5.2,默认配置还支持Vavr的Try方法,在返回’Failure’时触发事务回滚。这允许使用Try处理函数式错误,并在出现故障时自动回滚事务。

以下是一个如何使用Vavr的Try与事务方法结合使用的示例:

@Transactional
public Try<String> myTransactionalMethod() {
	// If myDataAccessOperation throws an exception, it will be caught by the
	// Try instance created with Try.of() and wrapped inside the Failure class
	// which can be checked using the isFailure() method on the Try instance.
	return Try.of(delegate::myDataAccessOperation);
}

默认配置中,从事务方法抛出的受检异常不会导致回滚。可以通过指定回滚规则来配置哪些Exception 类型标记事务以进行回滚,包括受检异常。

回滚规则确定在给定异常抛出时是否应回滚事务,这些规则基于异常类型或异常模式。

回滚规则可以通过XML中的rollback-forno-rollback-for属性进行配置,这些属性允许将规则定义为模式。使用@Transactional时,可以通过rollbackFor/noRollbackForrollbackForClassName/noRollbackForClassName属性配置回滚规则,这些属性允许基于异常类型或模式定义规则。

当使用异常类型定义回滚规则时,该类型将用于与抛出的异常及其超类型的类型进行匹配,提供类型安全性并避免在使用模式时可能发生的任何意外匹配。例如,jakarta.servlet.ServletException.class的值只会匹配类型为jakarta.servlet.ServletException及其子类的抛出异常。

当使用异常模式定义回滚规则时,该模式可以是完全限定的类名或异常类型(必须是Throwable的子类)的完全限定类名的子字符串,目前不支持通配符。例如,"jakarta.servlet.ServletException"或"ServletException"的值将匹配jakarta.servlet.ServletException及其子类。

你必须仔细考虑模式的具体性以及是否包含包信息(这不是强制性的)。例如,"Exception"几乎可以匹配任何东西,并可能隐藏其他规则。如果要为所有受检异常定义规则,“java.lang.Exception"将是正确的。对于更独特的异常名称,如"BaseBusinessException”,可能不需要使用完全限定的类名作为异常模式。

此外,基于模式的回滚规则可能会导致意外匹配类似命名的异常和嵌套类。这是因为,如果抛出的异常名称包含为回滚规则配置的异常模式,则认为该抛出的异常与给定的基于模式的回滚规则匹配。例如,给定一个配置为匹配"com.example.CustomException"的规则,该规则将与名为com.example.CustomExceptionV2(与CustomException在同一包中,但带有额外后缀的异常)或名为com.example.CustomException$AnotherException(在CustomException中声明为嵌套类的异常)的异常匹配。

以下XML片段演示了如何通过提供rollback-for属性的异常模式来配置针对受检的、特定于应用程序的Exception类型的回滚:

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

如果不希望在抛出异常时回滚事务,还可以指定’不回滚’规则。以下示例告诉Spring框架的事务基础设施,即使面对未处理的InstrumentNotFoundException也要提交伴随的事务。

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

当Spring框架的事务基础设施捕获到异常并咨询配置的回滚规则以确定是否标记事务以进行回滚时,最匹配的规则胜出。因此,在以下配置的情况下,除了InstrumentNotFoundException之外的任何异常都会导致伴随事务的回滚:

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

还可以以编程方式指示所需的回滚。尽管这个过程很简单,但它会将你的代码紧密耦合到Spring框架的事务基础设施。以下示例显示了如何以编程方式指示所需的回滚:

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

强烈建议尽可能使用声明式回滚方法。如果绝对需要,程序式回滚是可用的,但其使用与实现基于POJO的干净架构相悖。

为不同的 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>

	<!-- these two beans will be transactional... -->
	<bean id="fooService" class="x.y.service.DefaultFooService"/>
	<bean id="barService" class="x.y.service.extras.SimpleBarService"/>

	<!-- ... and these two beans won't -->
	<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
	<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->

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

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

</beans>

以下示例展示了如何为两个完全不同的bean配置不同的事务设置:

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

	<aop:config>

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

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

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

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

	</aop:config>

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

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

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

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

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

</beans>

<tx:advice/>设置

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

  • 传播设置为REQUIRED
  • 隔离级别为DEFAULT
  • 事务为读写模式。
  • 事务超时默认为底层事务系统的默认超时,如果不支持超时则无超时。
  • 任何RuntimeException触发回滚,任何受检Exception 不触发回滚。

你可以更改这些默认设置。下表总结了嵌套在<tx:advice/><tx:attributes/>标签内的<tx:method/>标签的各种属性:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用@Transactional

了基于XML的声明式事务配置方法外,还可以使用基于注解的方法。在Java源代码中直接声明事务语义可以将声明放在更接近受影响代码的地方。这并不会增加不必要的耦合风险,因为本意上要事务性使用的代码几乎总是以这种方式部署的。

标准的jakarta.transaction.Transactional注解也作为Spring自己的注解的替代方案得到支持。

使用@Transactional注解所带来的易用性最好通过一个示例来说明,下面的文本中解释了这个示例。考虑以下类定义:

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

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

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

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

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

如上所示,在类级别使用该注解,表示为声明类的所有方法(及其子类)提供一个默认设置。或者,每个方法也可以单独标注。类级别的注解不适用于类层次结构中的祖先类;在这种情况下,为了参与子类级别的注解,继承的方法需要在本地重新声明。

当像上面这样的POJO类在Spring上下文中被定义为bean时,你可以通过在@Configuration类中使用@EnableTransactionManagement注解来使bean实例具有事务性。

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

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

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

	<!-- enable the configuration of transactional behavior based on annotations -->
	<!-- a TransactionManager is still required -->
	<tx:annotation-driven transaction-manager="txManager"/>

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

如果你想要依赖注入的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 {

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

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

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

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

请注意,对于返回的Publisher,关于Reactive Streams取消信号有特殊的考虑事项。

@Transactional注解通常用于具有public 可见性的方法。从6.0版本开始,protected 的或包可见的方法也可以默认使基于类的代理具有事务性。请注意,基于接口的代理中的事务方法必须始终是public 的,并在被代理的接口中定义。对于这两种代理,只有通过代理进入的外部方法调用才会被拦截。

如果希望在不同种类的代理中对方法可见性进行一致处理(直到5.3版本为止这是默认设置),请考虑指定publicMethodsOnly

/**
 * Register a custom AnnotationTransactionAttributeSource with the
 * publicMethodsOnly flag set to true to consistently ignore non-public methods.
 * @see ProxyTransactionManagementConfiguration#transactionAttributeSource()
 */
@Bean
TransactionAttributeSource transactionAttributeSource() {
	return new AnnotationTransactionAttributeSource(true);
}

Spring TestContext框架默认也支持非私有的@Transactional测试方法。

可以将@Transactional注解应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅凭@Transactional注解的存在并不足以激活事务行为。@Transactional注解只是可以被相应的运行时基础设施使用的元数据,该基础设施使用这些元数据来配置具有事务行为的适当bean。在前面的示例中,<tx:annotation-driven/>元素在运行时打开了实际的事务管理。

Spring团队建议使用@Transactional注解来注释具体类的方法,而不是依赖于接口中的注解方法,即使后者在5.0版本之后确实适用于基于接口和目标类的代理。由于Java注解不会从接口继承,当使用AspectJ模式时,织入基础设施仍然无法识别接口声明的注解,因此切面不会被应用。结果可能是你的事务注解被静默忽略:在测试回滚场景之前,代码可能看似“正常工作”。

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

如果希望自调用也用事务包裹,请考虑使用AspectJ模式(见下表中的mode 属性)。在这种情况下,首先没有代理。相反,目标类被织入(即其字节码被修改)以支持任何类型方法上的@Transactional运行时行为。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

proxy-target-class属性控制为使用@Transactional注解的类创建哪种类型的事务代理。如果proxy-target-class设置为true,将创建基于类的代理。如果proxy-target-classfalse或省略了该属性,则创建标准的JDK基于接口的代理。

@EnableTransactionManagement<tx:annotation-driven/>只在同一个应用程序上下文中定义的bean上查找@Transactional。这意味着,如果你在DispatcherServletWebApplicationContext中放置了注解驱动的配置,它只会检查你的控制器中的@Transactional bean,而不是你的服务中的bean。

在评估方法的事务设置时,最具体(derived )的位置优先。在下例中,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
  • 事务为读写类型。
  • 事务超时默认为底层事务系统的默认超时时间,如果不支持超时,则为无。
  • 任何RuntimeExceptionError 都会触发回滚,而任何受检Exception 则不会。

你可以更改这些默认设置。下表总结了@Transactional注解的各种属性:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当前,你无法显式控制事务的名称,这里的’name’指的是在事务监视器和日志输出中出现的事务名称。对于声明式事务,事务名称总是被事务通知的类的完全限定类名 + . + 方法名。例如,如果BusinessService类的handlePayment(..)方法启动了一个事务,那么该事务的名称将是:com.example.BusinessService.handlePayment

多个事务管理器与 @Transactional

大多数 Spring 应用程序只需要一个事务管理器,但可能有一些情况,你希望在一个应用程序中使用多个独立的事务管理器。你可以使用 @Transactional 注解的 valuetransactionManager 属性来可选地指定要使用的事务管理器的标识。这可以是事务管理器 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.support.JdbcTransactionManager">
		...
		<qualifier value="order"/>
	</bean>

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

	<bean id="transactionManager3" class="org.springframework.data.r2dbc.connection.R2dbcTransactionManager">
		...
		<qualifier value="reactive-account"/>
	</bean>

在这种情况下,TransactionalService上的个别方法在不同的事务管理器下运行,这些事务管理器通过orderaccountreactive-account限定符来区分。如果没有找到特定限定的TransactionManager bean,仍然会使用默认的<tx:annotation-driven>目标bean名称transactionManager

自定义组合注解

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

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}

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

前面的注解让我们可以像下面这样编写上一节中的示例:

public class TransactionalService {

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

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

在上述示例中,我们使用了定义事务管理器限定符和事务标签的语法,但我们也可以包括传播行为、回滚规则、超时和其他特性。

事务传播(Transaction Propagation)

本节描述了Spring中事务传播的一些语义。它详细说明了Spring中关于事务传播的一些语义。

在Spring管理的事务中,要注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种区别。

理解PROPAGATION_REQUIRED

在这里插入图片描述

PROPAGATION_REQUIRED强制进行物理事务,如果没有事务存在,则在当前范围内本地执行,或者参与为更大范围定义的现有’外部’事务。在同一线程内的常见调用堆栈安排中(例如,委托给几个存储库方法的服务外观,其中所有底层资源都必须参与服务级事务),这是一个不错的默认设置。

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

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

然而,在内部事务范围设置了只回滚标记的情况下,外部事务尚未决定是否回滚,因此由内部事务范围(静默触发)的回滚是意外的。此时会抛出相应的UnexpectedRollbackException。这是预期的行为,以便调用事务的客户端永远不会被误导以为已经执行了提交,而实际上并没有。因此,如果内部事务(外部调用者不知道)静默地将事务标记为只回滚,外部调用者仍然调用commit。外部调用者需要接收到UnexpectedRollbackException,以明确指示实际执行了回滚。

理解PROPAGATION_REQUIRES_NEW

在这里插入图片描述

PROPAGATION_REQUIRED相对,PROPAGATION_REQUIRES_NEW总是为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。在这种配置下,底层资源事务是不同的,因此它们可以独立地提交或回滚,外部事务不受内部事务回滚状态的影响,并且内部事务完成后其锁立即被释放。这样的独立内部事务还可以声明自己的隔离级别、超时和只读设置,而不会继承外部事务的特性。

外部事务所绑定的资源将保持在那里,而内部事务会获取自己的资源,例如新的数据库连接。这可能会导致连接池耗尽,如果多个线程都有活动的外部事务并等待获取内部事务的新连接,而连接池无法再分配任何此类内部连接,就可能导致死锁。除非连接池大小适当,至少超过并发线程数1个,否则不要使用PROPAGATION_REQUIRES_NEW

理解PROPAGATION_NESTED

PROPAGATION_NESTED使用单个物理事务和多个可以回滚的保存点。这种部分回滚允许内部事务范围触发其范围内的回滚,外部事务可以在一些操作被回滚后继续进行物理事务。这种设置通常映射到JDBC保存点上,因此它只适用于JDBC资源事务。

事务操作的通知(Advising Transactional Operations)

假设你既想运行事务操作,又想进行一些基本的性能分析通知(profiling advice)。在 <tx:annotation-driven/>的上下文中,你如何实现这一点呢?

当你调用updateFoo (Foo) 方法时,希望看到以下操作:

  • 配置的性能分析切面(profiling aspect )启动。
  • 事务通知(advice )运行。
  • 被建议对象上的方法运行。
  • 事务提交。
  • 性能分析切面报告整个事务方法调用的确切持续时间。

以下代码显示了前面讨论的简单性能分析切面:

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,按期望的顺序应用了性能分析和事务切面:

<?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 (cf. 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,按该顺序应用了性能分析和事务切面。如果你希望在进入时性能分析通知(advice)在事务通知(advice)之后运行,并在退出时在事务通知之前运行,可以交换性能分析切面bean的order属性值,使其高于事务通知的order值。

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

使用@Transactional与AspectJ

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

以下示例展示了如何创建事务管理器并配置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注解会覆盖类注解(如果存在)给出的默认事务语义。可以注解任何方法,不管可见性如何。

要将你的应用程序与AnnotationTransactionAspect编织在一起,必须使用AspectJ构建应用程序,或使用加载时编织(load-time weaving)。

编程式事务管理

Spring框架提供了两种编程式事务管理的方法,通过使用:

  • TransactionTemplateTransactionalOperator
  • 直接使用TransactionManager实现。

Spring团队通常推荐在命令式流程中使用TransactionTemplate进行编程式事务管理,而在响应式代码中使用TransactionalOperator。第二种方法类似于使用JTA UserTransaction API,尽管异常处理不那么麻烦。

使用TransactionTemplate

TransactionTemplate采用了与其他Spring模板相同的方法,例如JdbcTemplate。它使用回调方法(从而免于应用程序代码必须执行获取和释放事务资源的样板代码),并导致代码是意图驱动的,即你的代码仅关注你想要做的事情。

正如以下示例所示,使用TransactionTemplate绝对会将你的代码与Spring的事务基础设施和API耦合在一起。进行编程式事务管理是否适合你的开发需求,是你必须自己做出的决定。

必须在事务上下文中运行并且显式使用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 runs 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();
		}
	}
});

指定事务设置

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

public class SimpleService implements Service {

	private final TransactionTemplate transactionTemplate;

	public SimpleService(PlatformTransactionManager transactionManager) {
		this.transactionTemplate = new TransactionTemplate(transactionManager);

		// the transaction settings can be set here explicitly if so desired
		this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
		this.transactionTemplate.setTimeout(30); // 30 seconds
		// and so forth...
	}
}

以下示例使用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实例。

使用TransactionalOperator

TransactionalOperator遵循类似于其他响应式操作符的操作符设计。它使用回调方法(从而免于应用程序代码必须执行获取和释放事务资源的样板代码),并导致代码是意图驱动的,即你的代码仅关注你想要做的事情。

正如以下示例所示,使用TransactionalOperator绝对会将你的代码与Spring的事务基础设施和API耦合在一起。进行编程式事务管理是否适合你的开发需求,是你必须自己做出的决定。

必须在事务上下文中运行并且显式使用TransactionalOperator的应用程序代码类似于下一个例子:

public class SimpleService implements Service {

	// single TransactionalOperator shared amongst all methods in this instance
	private final TransactionalOperator transactionalOperator;

	// use constructor-injection to supply the ReactiveTransactionManager
	public SimpleService(ReactiveTransactionManager transactionManager) {
		this.transactionalOperator = TransactionalOperator.create(transactionManager);
	}

	public Mono<Object> someServiceMethod() {

		// the code in this method runs in a transactional context

		Mono<Object> update = updateOperation1();

		return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
	}
}

TransactionalOperator可以以两种方式使用:

  • 使用Project Reactor类型的操作符风格(mono.as(transactionalOperator::transactional)
  • 用于其他所有情况的回调风格(transactionalOperator.execute(TransactionCallback)

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

transactionalOperator.execute(new TransactionCallback<>() {

	public Mono<Object> doInTransaction(ReactiveTransaction status) {
		return updateOperation1().then(updateOperation2)
					.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
		}
	}
});

取消信号(Cancel Signals)

在响应式流中,Subscriber 可以取消其Subscription 并停止Publisher。Project Reactor中的操作符,以及其他库中的操作符,比如next()take(long)timeout(Duration)等,都可以发出取消信号。没有办法知道取消的原因,无论是由于错误还是简单地缺乏进一步消费的兴趣。自5.3版本起,取消信号会导致回滚。因此,考虑事务发布者下游使用的操作符非常重要。特别是在Flux或其他多值Publisher的情况下,必须消耗完整的输出以允许事务完成。

指定事务设置(Specifying Transaction Settings)

可以为TransactionalOperator指定事务设置(如传播模式、隔离级别、超时等)。默认情况下,TransactionalOperator实例具有默认的事务设置。以下示例显示了如何为特定的TransactionalOperator自定义事务设置:

public class SimpleService implements Service {

	private final TransactionalOperator transactionalOperator;

	public SimpleService(ReactiveTransactionManager transactionManager) {
		DefaultTransactionDefinition definition = new DefaultTransactionDefinition();

		// the transaction settings can be set here explicitly if so desired
		definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
		definition.setTimeout(30); // 30 seconds
		// and so forth...

		this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
	}
}

使用TransactionManager

以下章节解释了命令式和响应式事务管理器的编程用法。

使用PlatformTransactionManager

对于命令式事务,你可以直接使用org.springframework.transaction.PlatformTransactionManager来管理事务。为此,通过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 {
	// put your business logic here
} catch (MyException ex) {
	txManager.rollback(status);
	throw ex;
}
txManager.commit(status);

使用ReactiveTransactionManager

在使用响应式事务时,可以直接使用org.springframework.transaction.ReactiveTransactionManager来管理事务。为此,通过bean引用将你使用的ReactiveTransactionManager实现传递给你的bean。然后,通过使用TransactionDefinitionReactiveTransaction对象,可以启动事务、回滚和提交。以下示例显示了如何执行此操作:

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);

reactiveTx.flatMap(status -> {

	Mono<Object> tx = ...; // put your business logic here

	return tx.then(txManager.commit(status))
			.onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});

在程序化和声明式事务管理之间选择

程序化事务管理通常只在你有少量事务操作时才是一个好主意。例如,如果有一个web应用程序,只有在特定的更新操作时才需要事务,那么你可能不想使用Spring或其他技术设置事务代理。在这种情况下,使用TransactionTemplate可能是一个好方法。能够明确设置事务名称也是只能通过程序化事务管理方法才能做到的事情。

另一方面,如果应用程序有大量事务操作,声明式事务管理通常是值得的。它将事务管理与业务逻辑分离,并且配置起来并不困难。当使用Spring框架而不是EJB CMT时,声明式事务管理的配置成本大大降低。

事务绑定事件(Transaction-bound Events)

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

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

下一个示例展示了这个概念。假设一个组件发布一个订单创建事件,我们想要定义一个监听器,只有当发布它的事务成功提交后,才处理该事件。以下示例设置了这样一个事件监听器:

@Component
public class MyComponent {

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

@TransactionalEventListener注解暴露了一个phase属性,允许自定义监听器应该绑定到事务的阶段。有效的阶段是BEFORE_COMMIT(提交前)、AFTER_COMMIT(默认)、AFTER_ROLLBACK(回滚后),以及AFTER_COMPLETION(完成后),后者汇总了事务的完成情况(无论是提交还是回滚)。

如果没有事务正在运行,根本不会调用监听器,因为我们无法保证所需的语义。但是,可以通过将注解的fallbackExecution属性设置为true来覆盖该行为。

从6.1版本开始,@TransactionalEventListener可以与由PlatformTransactionManager管理的线程绑定事务以及由ReactiveTransactionManager管理的反应式事务一起工作。对于前者,监听器保证能看到当前线程绑定的事务。由于后者使用Reactor上下文而不是线程局部变量,事务上下文需要包含在发布的事件实例中作为事件源。

应用服务器特定集成

Spring的事务抽象通常是与应用服务器无关的。此外,Spring的JtaTransactionManager类(可以可选地执行JNDI查找JTA UserTransactionTransactionManager对象)会自动检测后者对象的位置,这在不同的应用服务器中有所不同。能够访问JTA TransactionManager允许增强事务语义——特别是支持事务挂起。

Spring的JtaTransactionManager是在Jakarta EE应用服务器上运行的标准选择,并且已知在所有常见服务器上都能正常工作。高级功能,如事务挂起,在许多服务器上也能工作(包括GlassFish、JBoss和Geronimo),无需任何特殊配置。

常见问题解决方案

为特定数据源使用错误的事务管理器

根据你选择的事务技术和需求,使用正确的PlatformTransactionManager实现。如果正确使用,Spring框架仅仅提供了一个简单且可移植的抽象。如果使用全局事务,必须为所有事务操作使用org.springframework.transaction.jta.JtaTransactionManager类(或其针对应用服务器的特定子类)。否则,事务基础设施会尝试在容器DataSource实例等资源上执行本地事务。这样的本地事务没有意义,一个好的应用服务器会将其视为错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值