• 第20章 使用Spring进行事务管理(二)


    第20章 使用Spring进行事务管理

    20.2 声明式事务管理

    直接使用编程式事务管理的不足就是,事务管理代码与业务逻辑代码相互混杂。而声明式事务管理则可以避免这种不同系统关注点之间的纠缠,使得事务管理代码不用再去影响具体业务逻辑的实现。

    20.2.1 引子

    声明式事务实际上并没有想象中那么神秘,在我们将事务管理这一横切关注点从原来硬编码事务管理逻辑的系统中剥离出来后,就会发现,声明式事务已经在“幸福终点站”那里等着我们了。

    先不管Spring是如何提供声明式事务管理的。如果要我们从原来硬编码事务管理的系统中,将这些事务管理相关的代码从业务对象中剥离出来,我们会怎么做?最土的一个办法是,为每个Service都提供一个TransactionFacade,将事务管理逻辑集中到TransactionFacade中,而Service实现类可以对事务管理一无所知,只要保证针对所有Service的调用必须走TransactionFacade即可。

    整个情形看起来就是图20-1所示的样子。

    image-20220619171747211

    虽然这种方法可以实现事务管理代码与业务逻辑代码之间的分离,但是如果不做一些变通的话,在实际的开发中也不会给我们带来更多的好处。难道你想为每个Serivce对象都实现一个对应的TransactionFacade对象吗?

    对,经过SpringAOP的洗礼,你应该已经想到了,动态代理不就是为这个设计的吗?呵呵,不过,原理归原理,要真的实现这种功能,直接使用Spring AOP才是正道啊。事务管理本身就是一种横切关注点,与其他的横切关注点本质上没有任何区别,所以,我们完全可以为其提供相应的Advice实现,然后织入到系统中需要该横切逻辑的Joinpoint处。这样不就达到了将事务管理逻辑从业务逻辑实现中剥离出来的目的了吗?现在,我们要做的就是提供一个拦截器,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。

    有了Spring的编程式事务管理API的支持,实现这样的一个拦截器对于我们来说应该就很容易了,下方代码清单给出了这样一个拦截器原型的代码实现。

    public class PrototypeTransactionInterceptor imp1ements MethodInterceptor {
    	private PlatformTransactionManager transactionManager;
      
    	public Object invoke(MethodInvocation invocation) throws Throwable {
    		Method method = invocation.getMethod();
    		TransactionDefinition definition = getTransactionDefinitionByMethod(method);
    		TransactionStatus txStatus = transactionManager.getTransaction(definition);
    		Object result = null;
    		try {
    			result = invocation.proceed();
        }
    		catch(Throwable t) {
    			if(needRollbackOn(t)) {
    				transactionManager.rollback(txStatus);
          }
    			else {
    				transactionManager.commit(txStatus);
          }
    			throw t;
        }
    		transactionManager.commit(txStatus);
    		return result;
      }
      
    protected boolean needRollbackOn(Throwablet) {
    	//TODO ...更多实现细节
    	return false;.
    }
    protected TransactionDefinition getTransactionDefinitionByMethod(Methodmethod) {
    	//TODO ...更多实现细节
    	return null;
    }
    public PlatformTransactionManager getTransactionManager() {
    	return transactionManager;
    }
    public void setTransactionManager(PlatformTransactionManager transactionManager) {
    	this.transactionManager = transactionManager;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在实现这样的一个拦截器的过程中,我们会发现有些逻辑不好确定,如下所述。

    • 针对每个对象业务方法的拦截,需要知道该方法是否需要事务支持。如果需要,针对该事务的TransactionDefinition相关信息又从哪里获得?

    • 如果调用方法过程中抛出异常,如何对这些异常进行处理,哪些异常抛出的情况下需要回滚事务,哪些异常抛出的情况下又不需要?

    我们需要提供某种方式来记载业务方法与对应的事务信息之间的映射关系,拦截器只要查询这种映射关系,就可以知道要不要为当前业务方法创建事务。如果要创建事务,则以业务方法作为标志,到映射关系中查找创建事务所需要的信息,然后创建事务。我们当然可以将这种映射信息写死到拦截器实现类中。不过,将它们放在外部配置文件(例如properties或者XML文件)或者Java源代码的注解中才是当下比较流行的方式。这种映射信息正规来讲,叫做驱动事务的元数据(Metadata)。有了类似于PrototypeTransactionInterceptor这样的AOP装备的支持,系统的事务管理就变成了只需要在元数据中声明相应的事务控制信息。

    Spring提供了用于声明式事务管理的一切设施(使用org.springframework.transaction.interceptor.TransactionInterceptor实现替代PrototypeTransactionInterceptor),对于我们来说,所要做的只是决定要使用XML元数据驱动,还是使用注解元数据驱动的声明式事务管理。

    提示:什么?我还没有公布第二个问题是如何解决的?还记得在TransactionDefinition部分提到的用于声明式事务的TransactionAttribute定义吗?其实仅需要将“在什么异常类型抛出的情况下需要回滚事务”的信息补充到默认的TransactionDefintion定义中,用于事务处理的拦截器根据获得的TransactionDefinition提供的该信息,就能够决定出现异常的情况下应该如何结束事务。而TransactionAttribute就是增加了异常回滚信息的TransactionDefinition定义。用TransactionAttribute实现类代替默认的TransactionDefintion实现类创建事务不就结了?

    20.2.2 XML元数据驱动的声明式事务

    Spring允许我们在IoC容器的配置文件中直接指定事务相关的元数据,从1.x版本发展到2.x版本,不管配置形式如何变化,所要达到的目的以及底层的机制却是不变的,配置形式的演化只是为了能够更加简洁方便。从Spring1.x到2.x,大体上来说,我们可以使用以下4种配置方式在IoC容器的配置文件中指定事务需要的元数据。

    • 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor。

    • 使用“一站式”的TransactionProxyFactoryBean。

    • 使用BeanNameAutoProxyCreator。

    • 使用Spring2.x的声明事务配置方式。

    我们将对这4种配置方式给出详细的实例和相关内容的讲解。不过在此之前,我们需要先声明一个事务加之于上的模型。总不能将所有的配置都构建成“空中楼阁”不是?

    总是FooService或者BarService,可能大家都看烦了,所以,我们这回打算换个口味。假设我们要搭建一个QuoteService,先暂且不管它是应用于证券系统还是外汇系统,总之,通过它,我们能够查询报价信息,并且必要的话,也可以更新底层数据内容。现在的QuoteService不是一个远程服务,它的目的也很简单,基本上就是实现一个Quote信息的基本管理功能(第33章将介绍如何通过Spring将QuoteService以远程服务的方式公开出来)。

    首先定义的是对应QuoteService的服务接口,面向接口编程为管理问题域提供了很好的抽象方式,接口定义如下所示:

    public interface IQuoteService {
    	Quote getQuate();
    	Quote getQuateByDateTime(DateTime dateTime);
    	void saveQuote(Quote quote);
    	void updateQuote(Quote quote);
    	void deleteQuote(Quote quote);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    QuoteService实现了IQuoteService接口,其定义见下方代码清单。

    public class QuoteService implements IQuoteService {
    	private JdbcTemplate jdbcTemplate;
      
    	public Quote getQuate() {
    		return(Quote)getJdbcTemplate().queryForObject("",new RowMapper() {
    			public Object mapRow(ResultSet rs, int row) throws SQLException {
    				Quote quote = new Quote();
    				// ...
    				return quote;
          }});
      }
    	public Quote getQuateByDateTime(DateTime dateTime) {
    		throw new NotImplementedException();
      }
    	public void saveQuote(Quote quote) {
    		throw new NotImplementedException();
      }
    	public void updateQuote(Quote quote) {
    		throw new NotImplementedException();
      }
    	public void deleteQuote(Quote quote) {
    		throw new NotImplementedException();
      }
    	public JdbcTemplate getJdbcTemplate() {
    		return jdbcTemplate;
      }
      public void setJdbcTemplate(jdbcTemplate jdbcTemplate) {
    		this.jdbcTemplate = jdbcTemplate;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    这里只是为了说明针对QuoteService的事务管理,所以,暂且直接使用JdbcTemplate进行数据访问。如果系统需要进一步的抽象,可以将数据访问逻辑提取到DAO层,屏蔽数据访问技术造成的差异。我们仅给出一个方法的实现,其余的先抛出NotImplementedException。当系统运行时,这可以告诉我们还有工作没有完成。

    在有了QuoteService这个简单的模型之后,我们开始进入正题。

    1. 使用ProxyFactory(ProxyFactoryBean)+TransactionInterceptor

    XML元数据驱动的声明式事务,本质上来说,是为TransactionInterceptor提供需要的TransactionDefiniton(具体来说,是TransactionAttribute)信息。而至于说将TransactionInterceptor给出的事务管理相关的横切逻辑加到相应的业务对象上的过程,则纯粹就是一个AOP的配置过程。既然如此,最基本的方式当然就是,直接通过ProxyFactoryBean(或者ProxyFactory)完成事务管理这一横切关注点到系统的织入工作。

    所以,使用ProxyFactoryBean和TransactionInterceptor为我们的QuoteService添加声明式事务,看起来就是下方代码清单所示的样子。

    <beans>
    	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    		<property name="url" value="jdbc:mysql://localhost/databaseName"/>
    		<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    		<property name="username" value="..."/>
    		<property name="password" value="..."/>
    	</bean>
        
    	<bean id="jdbcTemplate" c1ass="org.springframework.jdbc.core.JdbcTemplate">
    		<propertyname="dataSource" ref="dataSource"/>
    	</bean>
        
    	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    		<property name="dataSource" ref="dataSource"/>
    	</bean>
        
      <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="transactionAttributes">
          <props>
            <propkey="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop>
            <propkey="saveQuote">PROPAGATION_REQUIRED</prop>
            <propkey="updateQuote">PROPAGATION_REQUIRED</prop>
            <propkey="deleteQuote">PROPAGATION_REQUIRED</prop>
          </props>
        </property>
      </bean>
    
      <bean id="quoteServiceTarget" class="...QuoteService">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
      </bean>
    
      <bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="quoteServiceTarget"/>
        <property name="proxyInterfaces" value="...IQuoteService"/>
        <property name="interceptorNames">
          <list>
            <value>transactionInterceptor</value>  
          </list>
        </property>
      </bean>
    
      <bean id="client" class="...QuoteSerivceClient">
        <property name="quoteService" ref="quoteService"/>
      </bean>
    </beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    一般意义上,QuoteSerivceClient是使用Quoteservice的应用类,当我们通过它调用QuoteService的业务方法的时候,除了getQuote()方法之外,其他方法,比如getQuoteService().saveQuote(quote),因为抛出了unchecked的NotImplementedException,从而导致事务自动回滚。这说明我们的声明式事务生效了,不是吗?如下所示:

    image-20220620174404678

    现在让我们回头探究一下,在使用ProxyFactoryBean和TransactionInterceptor进行声明式事务的过程中,到底有哪些奥秘和需要我们注意的地方。要使声明式事务起作用,需要几个方面协同工作,这包括事务要管理的具体数据资源类型、采用的数据访问技术、特定的事务管理器实现,以及TransactionInterceptor提供的业务方法拦截功能。

    具体到我们的QuoteService来说,我们要对数据库提供的数据资源进行事务操作,所以,配置中需要dataSource的定义,这是事务操作的基础,也就是如下所示配置内容:

    <bean id="dataSource" class="org.apache.cormons.dbcp.BasicDataSource" destroy-method="close">
      <property name="url" value="jdbc:mysql://localhost/databaseName"/>
      <property name="driverClassName" value="com.mysql.jdbc.Driver"/
      <property name="username" value="..."/> 
      <propertyname="password" value="..."/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    要对数据资源进行访问,QuoteService采用的是JDBC的方式,即直接便用Spring提供的JdbcTemplate,那么,我们需要提供JdbcTemplate以及特定于JDBC的事务管理器DataSourceTransactionManager,如下定义:

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    	<property name="đataSource" ref="dataSource"/>
    </bean>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref="dataSource"/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    数据访问技术与事务管理器的类型是一一对应的,否则就有“驴唇不对马嘴”之嫌,让A公司的老板来管理B公司的事务,B公司的员工当然可以不理他。所以,如果你的QuoteService要用Hibemate进行数据访问,那么请提供HibernateTransactionManager作为事务管理器。

    **TransactionInterceptor是整个声明式事务的主体。**要让它发挥事务管理的职能,需要为它提供一个事务管理器。要让它知道该为哪个方法加诸什么事务,是否要加诸事务,需要为它提供必要的映射信息,如下方代码清单所示。

    <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
    	<property name="transactionManager" ref="transactionManager"/>
    	<property name="transactionAttributes">
    		<props>
    			<prop key="getQuate*">PROPAGATION_SUPPORTS,readOnly,timeout_20</prop>
    			<prop key="saveQuote">PROPAGATION_REQUIRED</prop>
    			<prop key="updateQuote">PROPAGATION_REQUIRED</prop>
    			<prop key="deleteQuote">PROPAGATION_REQUIRED</prop>
    		</props>
    	</property>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    TransactionInterceptor所需要的业务对象上的每个业务方法的事务管理信息,通过org.springframework.transaction.interceptor.TransactionAttributeSource接口来获取,该接口定义如下:

    public interface TransactionAttributeSource {
    	TransactionAttribute getTransactionAttribute(Method method, Class targetClass);
    }
    
    • 1
    • 2
    • 3

    通常,TransactionAttributeSource的具体实现类以不同的形式存储了从不同位置获取的事务管理元数据信息,比如NameMatchTransactionAttributeSource将以方法名作为映射信息的Key,对相应的元数据进行存储;MethodMapTransactionAttributeSource则直接以对应每个方法的Method实例作为Key,来存储元数据对应的映射信息;而AnnotationTransactionAttributeSource则是直接从源代码中的注解中获取对应每个业务方法的事务管理信息。

    我们通常通过如下两个属性为TransactionInterceptor设置需要的TransactionAttributeSource。

    • transactionAttributes属性。我们刚才就是使用transactionAttributes属性为TransactionInterceptor设置的映射信息,transactionAttributes是Properties类型,TransactionInterceptor内部将通过transactionAttributes提供的信息构造一个NameMatchTransactionAttributeSource类型的TransactionAttributeSource使用。
    • transactionAttributeSource属性。transactionAttributeSource属性就是直接可用的TransactionAttributeSource类型,但是我们在IoC容器的配置文件中指定的是String形式。所以,容器将通过org.springframework.transaction.interceptor.TransactionAttributeSourceEditor对String形式的值进行一个转换,再设置给TransactionInterceptor,转换后的具体TransactionAttributeSource实现类为MethodMapTransactionAttributeSource。同样的元数据,通过transactionAttributeSource设置如下:
    <property name="transactionAttributeSource">
    	<value>
    		org.spring21.package.IQuoteService.getQuate*=PROPAGATION_SUPPORTS,readOnly,timeout_20
    		org.spring21.package.IQuoteService.saveQuote=PROPAGATION_REQUIRED
    		org.spring21.package.IQuoteService.updateQuote=PROPAGATION_REQUIRED
    		org.spring21.package.IQuoteService.deleteQuote=PROPAGATION_REQUIRED
    	</value>
    </property>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    唯一需要注意的就是,现在需要指定全限定的类名和方法名。

    注意也可以通过transactionAttributeSources属性指定元数据信息,它是transactionAttributeSource的复数形式。

    在TransactionInterceptor准备就绪之后,剩下的就是通过SpringAOP将业务对象与其绑定,见下方代码清单。

    <bean id="quoteServiceTarget" class="...QuoteService">
    	<property name="jdbcTemplate" ref="jabcTemplate"/>
    </bean>
    
    <bean id="quoteService" class="org.springframework.aop.framework.ProxyFactoryBean">
    	<property name="target" ref="quoteServiceTarget"/>
    	<property name="proxyInterfaces" value="...IQuoteService"/>
    	<property name="interceptorNames">
    		<list>
    			<value>transactionInterceptor</value>
    		</list>
    	</property>
    </bean>
    
    <bean id="c1ient" class="...QuoteSerivceClient">
    	<property name="quoteService" ref="quoteService"/>
    </bean>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    只不过,一定不要把quoteServiceTarget当作quoteService而注入给需要的对象,否则,事务管理不生效那就怨不得别人了。

    元数据中事务属性指定规则:

    以String形式给出的事务属性是有一定规则的,这个规则由org.springframework.transaction.interceptor.TransactionAttributeEditor类的逻辑来界定,TransactionAttributeEditor负责将String形式给出的事务属性转换为具体的TransactionAttribute实例(RuleBasedTransactionAttribute)。

    String形式的事务属性规则如下:

    PROPAGATIONNAME,[ISOLATION_NAME],[readOnly],[timeout_NNNN],[+Exception1],[-Exception2]
    
    • 1

    除了PROPAGATION_NAME是必须的之外,其他规则可以根据需要决定是否需要指定,各个区段之间以逗号分隔。

    • PROPAGATION_NAME。对应org.springframework.transaction.TransactionDefinistion中定义的与传播行为相关的常量名,即PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_SUPPORTS、PROPAGATION_MANDATORY、PROPAGATION_NESTED、PROPAGATION_NOT_SUPPORTED和PROPAGATION_NEVER。
    • ISOLATION_NAME。对应org.springframework.transaction.TransactionDefinition中定义的与隔离级别相关的常量名,即ISOLATION_DEFAULT、ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ和年SOLATION_SERIALIZABLE。
    • readonly。如果需要将当前事务标明为只读事务,那么追加该值即可。
    • timeout_NNNN,指定事务的超时时间,需要以timeout_为前缀,后面加数字的形式,如timeout_20限定超时时间为20秒。
    • [+Exception1],[-Exception2]。自定义异常回滚规则。前缀加号(+)后跟具体的异常类型表示,即使业务方法抛出该异常也同样提交事务。前缀减号(-)后跟具体的异常类型表示,业务方法抛出该异常的时候回滚事务。比如,当业务方法抛出QuoteException的时候,我们想要事务回滚,则可以如下这样指定:
    serviceMethod=PROPAGATIONREQUIRED,-QuoteException 
    
    • 1

    因为“unchecked exception”默认情况下会自动回滚,所以,通过自定义异常回滚规则主要是指定“checked exception”类型的应用异常。

    使用ProxyFactoryBean和TransactionInterceptor实现声明式事务,可以从最低层次上理解Spring提供的声明式事务是如何运作的。不过,为了减少配置量,进一步提高开发效率,我们会探索更加便捷的方式,TransactionProxyFactoryBean就向这一目标更迈进了一步。

  • 相关阅读:
    9.2 Plotting with pandas and seaborn(用pandas和seaborn绘图)
    广告牌安全传感器怎么用?为城市能起到什么效果?
    MySQL数据库——日志管理、备份与恢复
    ES6——ES6中对象、字符串、数组的扩展方法
    使用 GPT4V+AI Agent 做自动 UI 测试的探索
    ITIL 4Foundation课本概念常考点
    Java集合常见面试题汇总
    Canvas绘图
    opencv:实现超像素分割​(附完整源码)
    springboot大学生拼车管理系统毕业设计源码201507
  • 原文地址:https://blog.csdn.net/qq_34626094/article/details/125484498