Spring事务管理---中

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring事务管理---中相关的知识,希望对你有一定的参考价值。

Spring事务管理---中


本系列文章:

Spring事务管理—上

Spring事务王国概览


使用Spring 2.x的声明事务配置方式

上面我们介绍完了三种XML元数据驱动的声明式事务的使用方式,下面我们介绍最后一种基于Spring 2.x的声明事务配置方式。

Spring 2.x后提供的基于XML Schema的配置方式,专门为事务管理提供了一个单独的命名空间用于简化配置,结合新的TX命名空间,现在的声明式事务管理看起来如下:

具体使用方式有以下几个步骤:

  • 引入相关aop和tx命名空间
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-beans.xsd
       http://www.springframework.org/schema/tx  http://www.springframework.org/schema/tx/spring-beans.xsd">
  • dataSource,transactioManager,业务对象准备
<!--数据源准备-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    ...
</bean>
<!--  JdbcTemplate准备  -->
<bean id="jt" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--    事务管理器准备    -->
<bean id="tm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--  业务对象准备  -->
<bean id="testService" class="org.transaction.TestService">
    <constructor-arg ref="jt"/>
</bean>
  • 使用专有的advice命名空间声明相关advice,aop命名空间配置自动代理创建器
    <aop:config>
        <aop:pointcut id="pointCut" expression="execution(* org.transaction.TestService.update())"/>
        <aop:advisor advice-ref="txAdivce" pointcut-ref="pointCut"/>
    </aop:config>
    
    <tx:advice id="txAdivce" transaction-manager="tm">
        <tx:attributes>
            <tx:method name="update" propagation="REQUIRED" read-only="true" timeout="20"/>
        </tx:attributes>
    </tx:advice>

< tx:advice >是专门为声明事务Adivce而设置的配置元素,底层还是TransactionInterceptor,其transaction-manager指明拦截器需要使用的事务管理器是哪个,如果容器中事务管理器的beanName恰好就是transactionManager,那么可以不明确指定。

< tx:attributes >提供声明式事务所需要的元数据映射信息,对应着拦截器中之前配置的TransactionAttributeSource。

< tx:method >的name属性必须指定,其他事务定义相关属性,如果不指定,采用默认的配置,即DefaultTransactionDefioiton。

tx:method可设置属性列表如下:

< tx:adivce > 指定的是拦截器的配置,那么需要有AOP的支持才能织入到具体的业务对象中去,所以剩下的工作实际上是AOP的配置。

和之前编码过程一样,还是通过自动代理创建器来创建代理对象,而aop:config底层就是依赖自动代理机制的,但是具体解析源码我就不带大家看了。

    <aop:config>
        <aop:pointcut id="pointCut" expression="execution(* org.transaction.TestService.update())"/>
        <aop:advisor advice-ref="txAdivce" pointcut-ref="pointCut"/>
    </aop:config>

aop:config标签底层会由aop命名空间解析器进行解析,然后自动代理实现类会根据该元素内部对应的point,advisor及aspect的子元素取得必要的织入信息,然后为容器内注册的bean进行自动代理。

如果各位对aop命名空间开头的标签解析过程感兴趣的话,可以去看看AopNamespaceHandler类的源码,因为Spirng对于非默认命令空间的解析,对应都会去寻找对应的命名空间解析器进行解析。

public class AopNamespaceHandler extends NamespaceHandlerSupport 

	@Override
	public void init() 
		// In 2.0 XSD as well as in 2.5+ XSDs
		//config标签由ConfigBeanDefinitionParser负责解析--感兴趣可以自行研究一下,这里篇幅有限,不展开看了
		registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
		//通过标签方式开启aop自动代理的方式--对应AspectJAutoProxyBeanDefinitionParser解析器
		registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
		registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());

		// Only in 2.0 XSD: moved to context namespace in 2.5+
		registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
	



注解元数据驱动的声明式事务

上面,我们介绍完了XML元数据驱动的声明式事务,下面来看看注解元数据驱动的声明式事务

注解元数据驱动,我相信大家都猜到了,就是我们最常使用的Transactional注解,注解元数据驱动的声明式事务基本原理就是,将业务方法的事务元数据,直接通过注解标注到业务方法或者业务方法所在的对象上,然后再业务方法执行期间,通过反射读取标注在业务方法上的注解所包含的元数据信息,最终根据读取的信息为业务方法构建事务管理的支持。

Transactional注解用于标注业务方法所对应的事务元数据信息,通过该注解可以指定与< te:method />标签几乎相同的信息。

当然,现在不需要指定方法名称了,因为注解直接标注到了业务方法或者业务方法所在的对象定义上。

@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional 
	@AliasFor("transactionManager")
	String value() default "";
	@AliasFor("value")
	String transactionManager() default "";
	String[] label() default ;
	Propagation propagation() default Propagation.REQUIRED;
	Isolation isolation() default Isolation.DEFAULT;
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
	String timeoutString() default "";
	boolean readOnly() default false;
	Class<? extends Throwable>[] rollbackFor() default ;
	String[] rollbackForClassName() default ;
	Class<? extends Throwable>[] noRollbackFor() default ;
	String[] noRollbackForClassName() default ;


基于注解的驱动式事务基本使用如下:

@Transactional
@Component
@RequiredArgsConstructor
public class TestService 
    private final JdbcTemplate jt;

    @Transactional(propagation = Propagation.SUPPORTS,readOnly = true,timeout = 20)
    public void update() throws Exception 
        throw new RuntimeException("我是来捣乱的");
    

有下面几点需要注意:

  • @Transactional标注为对象级别的话,该注解标注的事务管理信息会应用到该类所有方法上。通过将相同的事务管理行为提取到对象级别的@Transactional,可以有效减少标注的数量。
  • 如果只标注在了指定方法上,那么只会应用在指定方法上。
  • 如果不为@Transactional注解指定一些自定义事务配置,那么它会像< tx:method />一样采用与DefaultTransactionDefinition一样的事务定义内容。

如果仅仅只通过@Transactional标注业务对象和对象中的业务方法,并不会给相应的业务方法执行提供任何事务管理信息的支持,该注解的作用类似TransactionAttributeSource中保存的一条映射关系,即通过该注解我们只是知道了当前方法需要什么样的事务支持,但是我们只有在方法执行时通过反射读取这些事务信息,才能去构建事务,从而使得事务生效。


模拟解析注解

首先,我们需要弄清楚解析步骤:

  • 判断哪些bean上标注了@Transactional注解,然后解析得到TransactionAttribute
  • 将当前方法和TransactionAttribute的映射关系加入TransactionAttributeSource中
  • 加入TransactionAttributeSource的前提是我们已经准备好了一个TransactionAttributeSource,当然还有TransactionInterceptor和TransactionManager,还有最重要的自动代理创建器
  • 但是事务并不是任何情况下都需要的,即TI,TM,TAS和自动代理创建器并不是什么任何情况下都需要创建的
  • 因此,我们需要准备一个开关,需要的时候打开开关,创建上面四个组件,别的时候就不管

分析完上面的需求后,我们大概知道了具体需要怎么做,那么到底怎么完成上面的需求呢?

  • 判断哪些bean上标注了@Transactional注解—>需要对每一个bean都进行检查,这不就想到了bean的后置处理器吗,而且还需要为每一个需要事务支持的bean生成代理对象,那么同时兼具这两个功能的,不就是自动代理创建器吗?
  • 在什么时候获取当前方法和对应TransactionAttribute的映射关系呢? —>利用自动代理创建器,这个具体下面会讲
  • 需要一个开关–>这个比较简单,我们可以自定义一个事务开启的注解,当需要开启事务的时候,我们就将上面四个组件导入到容器中。

思路也有了,解决办法也有了,下面就开始实战吧:

  • 开关最简单,我们这里先准备好
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
//导入一个类
@Import(MyTransactionManagementConfigurationSelector.class)
public @interface MyEnableTransactionManagement 

    /**
     * 是否默认采用cglib进行代理
     */
    boolean proxyTargetClass() default false;

    /**
     * 代理模式--是jdk,cglib代理还是aspectj代理
     */
    AdviceMode mode() default AdviceMode.PROXY;

  • 开关打开后,我们就需要导入上面说的那些组件了,而导入的工作就交给了MyTransactionManagementConfigurationSelector 来完成
/**
 * 注册TM,拦截器和自动代理创建器
 */
public class MyTransactionManagementConfigurationSelector implements ImportSelector 

    /**
     * 返回的是需要导入的bean的全类名
     * 这里我们需要导入一个配置类和一个自动代理创建器
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) 
        return new String[]
            //注册TM,拦截器等组件
            MyTransactionConfiguration.class.getName(),
                //注册自动代理创建器到容器
                AutoProxyRegistrar.class.getName()
        ;
    

ImportSelector的原理涉及配置类的解析过程,我下面会稍微提一嘴,该接口的作用就是可以向容器中放入某些bean集合

  • 先来看看TM,拦截器的注册,就是MyTransactionConfiguration就是导入一个配置类,但是注意这个配置类必须放在启动类或者自己加的@CompoentScan注解扫描不到的地方
@Configuration
public class MyTransactionConfiguration 
  //注册TM
  @Bean
 //标注角色为基础设施类,该bean被解析后,其BeanDefinition中会标记当前bean的role为ROLE_INFRASTRUCTURE
 //ROLE_INFRASTRUCTURE与我们待会要注入的自动代理创建器有关,暂时先不用深究
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionManager transactionManager(DataSource dataSource)
    return new JdbcTransactionManager(dataSource);
  

//注册transactionAttributeSource---这里放入的是我们自定义的MyTransactionAttributeSource
  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionAttributeSource transactionAttributeSource()
    return new MyTransactionAttributeSource();
  

//注册拦截器
  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionInterceptor transactionInterceptor(TransactionManager transactionManager,TransactionAttributeSource transactionAttributeSource)
    return new TransactionInterceptor(transactionManager,transactionAttributeSource);
  

  /**
   * transactionAttributeSourceAdvisor增强器最大的作用在于
   * 给我们提供了一个TransactionAttributeSourcePointcut
   * 该pointcut可以帮助我们进行类级别的过滤和方法级别的过滤
   * 方法级别的过滤是当前方法在TransactionAttributeSource中能否找到对应的一个TransactionAttribute
   */
  @Bean
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public TransactionAttributeSourceAdvisor transactionAttributeSourceAdvisor(TransactionInterceptor transactionInterceptor)
     return new TransactionAttributeSourceAdvisor(transactionInterceptor);
  

  • 自动代理创建器导入的是InfrastructureAdvisorAutoProxyCreator,通过AutoProxyRegistrar导入的,因为他实现了ImportBeanDefinitionRegistrar接口,该接口实现后,也可以向容器中导入bean,和ImportSelector功能一致
  • 在来看看我们自定义的transactionAttributeSource
public class MyTransactionAttributeSource implements TransactionAttributeSource 
    /**
     * 负责解析@Transactional注解的
     */
    private TransactionAnnotationParser parser = new SpringTransactionAnnotationParser();
    /**
     * 方法名和方法名关联的TransactionAttribute
     */
    private final Map<String, TransactionAttribute> nameMap = new HashMap<>();
    /**
     * 存放已经解析过的方法
     */
    private final Set<String> parsedMethod=new HashSet<>();
    /**
     * 类过滤信息缓存--key是类名,value表示当前类是否需要事务支持: 1表示需要,其他数字表示不需要
     */
    private final Map<String,Integer> classFilter=new HashMap<>();
    
    private final Integer NEED_TRANSACTION=1;

    /**
     * 用于pointcut的类过滤---判断类和类的每个方法上是否标注了@Transacion注解
     */
    @Override
    public boolean isCandidateClass(Class<?> targetClass) 
        //查询缓存
        Integer res = classFilter.get(targetClass.getName());
        if(res!=null)
            return res.equals(NEED_TRANSACTION);
        
        //当前类是否需要事务,即当前类上,方法上是否存在@Transactional注解
        if(parser.isCandidateClass(targetClass))
            classFilter.put(targetClass.getName(),NEED_TRANSACTION);
            return true;
         
        return false;
    


    /**
     * 该方法在TransactionAttributeSourceAdvisor增强器的point中,用于方法级别过滤
     * 如果第一次来,缓存没有会进行解析,第二次来直接走缓存即可
     */
    @Override
    public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) 
        //先去缓存中寻找--保存已经解析过
        String cacheKey = getCacheKey(method, targetClass);
        if(parsedMethod.contains(cacheKey))
            return nameMap.get(cacheKey);
        
        //如果当前类上,类的方法上都没有标注该注解,那么就跳过
        if(!parser.isCandidateClass(targetClass))
            parsedMethod.add(cacheKey);
            return null;
        
        //解析方法上标注的@Transactional注解
        TransactionAttribute attr = parser.parseTransactionAnnotation(method);
        if(attr==null)
            //如果方法上没标注,再尝试解析类上的注解
            attr=parser.parseTransactionAnnotation(targetClass);
        
        nameMap.put(cacheKey,attr);
        parsedMethod.add(cacheKey);
        return attr;
    

    private String getCacheKey(Method method, Class<?> targetClass) 
      return targetClass.getName()+method.getName();
    




  • 测试
//关闭事务的自动配置
@SpringBootApplication(exclude = TransactionAutoConfiguration.class)
//开启我们手动的事务配置
@MyEnableTransactionManagement
public class TransactionMain 
    public static void main(String[] args) throws ClassNotFoundException, SQLException 
        ConfigurableApplicationContext app = SpringApplication.run(TransactionMain.class, args);
        TestService testService = app.getBean(TestService.class);
        try 
            testService.update();
         catch (Exception e) 
            e.printStackTrace();
        
    

SpringBoot会自动开启事务的相关配置,因此我们需要先关闭一下,替换为我们自定义的事务管理实现


模拟流程的原理解析

如果能看懂上面模拟解析的每个步骤,那么说明你对Spring源码研究的还不错,如果没看懂,没关系,下面我一点点带领大家来分析:


import导入配置

就先从最简单的开始吧:

@Import(MyTransactionManagementConfigurationSelector.class)
public class MyTransactionManagementConfigurationSelector implements ImportSelector 

    /**
     * 返回的是需要导入的bean的全类名
     * 这里我们需要导入一个配置类和一个自动代理创建器
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) 
        return new String[]
            MyTransactionConfiguration.class.getName(),
                AutoProxyRegistrar.class.getName()
        ;
    

因为Import注解,ImportSelector相关接口的解析都涉及到了@Configuration标注的配置类解析过程,所以这里先来大致浏览一下配置类解析是个怎么样的流程:

配置类是由ConfigurationClassPostProcessor工厂后置处理器解析的,工厂后置处理器相关接口都会在refresh方法中被调用:

@Override
	public void 以上是关于Spring事务管理---中的主要内容,如果未能解决你的问题,请参考以下文章

Spring循环依赖是如何解决的?

在 ArrayLists () 中放入啥 [关闭]

建议在存储过程中放入啥逻辑?

如何自动在标签中放入 UIElement 的名称

为啥我在android中放入MutableLiveData后会得到Null

在二进制数据文件的标头中放入啥