天天看點

Spring事務用法示例與實作原理

關于事務,簡單來說,就是為了保證資料完整性而存在的一種工具,其主要有四大特性:原子性,一緻性,隔離性和持久性。對于Spring事務,其最終還是在資料庫層面實作的,而Spring隻是以一種比較優雅的方式對其進行封裝支援。本文首先會通過一個簡單的示例來講解Spring事務是如何使用的,然後會講解Spring是如何解析xml中的标簽,并對事務進行支援的。

1. 使用示例

       關于事務最簡單的示例,就是其一緻性,比如在整個事務執行過程中,如果任何一個位置報錯了,那麼都會導緻事務復原,復原之後資料的狀态将和事務執行之前完全一緻。這裡我們以使用者資料為例,在插入使用者資料的時候,如果程式報錯了,那麼插入的動作就會復原。如下是使用者的實體:

public class User {
  private long id;
  private String name;
  private int age;
  
  // getter, setter...
}           

       如下是模拟插入使用者資料的業務代碼:

public interface UserService {
  void insert(User user);
}

@Service
@Transactional
public class UserServiceImpl implements UserService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Override
  public void insert(User user) {
    jdbcTemplate.update("insert into user (name, age) value (?, ?)", 
        user.getName(), user.getAge());
  }
}           

       在進行事務支援時,Spring隻需要使用者在需要事務支援的bean上使用@Transactional注解即可,如果需要修改事務的隔離級别和傳播特性的屬性,則使用該注解中的屬性進行指定。這裡預設的隔離級别與各個資料庫一緻,比如MySQL是Repeatable Read,而傳播特性預設則為Propagation.REQUIRED,即隻需要目前操作具有事務即可。如下是xml檔案的配置:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="url" value="jdbc:mysql://localhost/test?useUnicode=true"/>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="username" value="****"/>
    <property name="password" value="******"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

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

<context:component-scan base-package="com.transaction"/>
<tx:annotation-driven/>           

       上述資料庫配置使用者按照各自的設定進行配置即可。可以看到,這裡對于資料庫的配置,主要包括四個方面:

  • DataSource配置:設定目前應用所需要連接配接的資料庫,包括連結,使用者名,密碼等;
  • JdbcTemplate聲明:封裝了用戶端調用資料庫的方式,使用者可以使用其他的方式,比如JpaRepository,Mybatis等等;
  • TransactionManager配置:指定了事務的管理方式,這裡使用的是DataSourceTransactionManager,對于不同的連結方式,也可以進行不同的配置,比如對于JpaRepository使用JpaTransactionManager,對于Hibernate,使用HibernateTransactionManager;
  • tx:annotation-driven:主要用于事務驅動,其會通過AOP的方式聲明一個為事務支援的Advisor,通過該Advisor和事務的相關配置進行事務相關操作。

       按照上述配置,我們的事務功能即配置完成,如下是我們的驅動類程式:

public class TransactionApp {
  @Test
  public void testTransaction() {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    UserService userService = ac.getBean(UserService.class);
    User user = getUser();
    userService.insert(user);
  }

  private User getUser() {
    User user = new User();
    user.setName("Mary");
    user.setAge(27);
    return user;
  }
}           

       運作上述程式之後,可以看到資料庫中成功新增了一條資料。這裡如果我們将業務代碼的插入語句之後手動抛出一個異常,那麼,理論上插入語句是會復原的。如下是修改後的service代碼:

@Service
@Transactional
public class UserServiceImpl implements UserService {
  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Override
  public void insert(User user) {
    jdbcTemplate.update("insert into user (name, age) value (?, ?)", 
        user.getName(), user.getAge());
    throw new RuntimeException();
  }
}           

       這裡我們手動抛出了一個RuntimeException,再次運作上述程式之後我們發現資料庫中是沒有新增的資料的,這說明我們的事務在程式出錯後是能夠保證資料一緻性的。

2. 标簽解析

       關于事務的實作原理,我們首先講解Spring是如何解析标簽,并且封裝相關bean的,後面我們會深入講解Spring是如何封裝資料庫事務的。

       根據上面的示例,我們發現,其主要有三個部分:DataSource,TransactionManager和tx:annotation-driven标簽。這裡前面兩個部分主要是聲明了兩個bean,分别用于資料庫連接配接的管理和事務的管理,而tx:annotation-driven才是Spring事務的驅動。根據本人前面對Spring自定義标簽的講解(

Spring自定義标簽解析與實作

),可以知道,這裡tx:annotation-driven是一個自定義标簽,我們根據其命名空間(www.springframework.org/schema/tx)在全局範圍内搜尋,可以找到其處理器指定檔案spring.handlers,該檔案内容如下:

http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler           

       這裡也就是說tx:annotation-driven标簽的解析在TxNamespaceHandler中,我們繼續打開該檔案可以看到起init()方法如下:

@Override
public void init() {
    registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
    registerBeanDefinitionParser("annotation-driven", 
        new AnnotationDrivenBeanDefinitionParser());
    registerBeanDefinitionParser("jta-transaction-manager", 
        new JtaTransactionManagerBeanDefinitionParser());
}           

       可以看到,這裡的annotation-driven是在AnnotationDrivenBeanDefinitionParser中進行處理的,其parse()方法就是解析标簽,并且注冊相關bean的方法,如下是該方法的實作:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 注冊事務相關的監聽器,如果某個方法标注了TransactionalEventListener注解,
    // 那麼該方法就是一個事務事件觸發方法,即發生某種事務事件後,将會根據該注解的設定,回調指定
    // 類型的方法。常見的事務事件有:事務執行前和事務完成(包括報錯後的完成)後的事件。
    registerTransactionalEventListenerFactory(parserContext);
    String mode = element.getAttribute("mode");
    // 擷取目前事務驅動程式的模式,如果使用了aspectj模式,則會注冊一個AnnotationTransactionAspect
    // 類型的bean,使用者可以以aspectj的方式使用該bean對事務進行更多的配置
    if ("aspectj".equals(mode)) {
        registerTransactionAspect(element, parserContext);
    } else {
        // 一般使用的是目前這種方式,這種方式将會在Spring中注冊三個bean,分别是
        // AnnotationTransactionAttributeSource,TransactionInterceptor
        // 和BeanFactoryTransactionAttributeSourceAdvisor,并通過Aop的方式實作事務
        AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext);
    }
    return null;
}           

       可以看到,對于事務的驅動,這裡主要做了兩件事:①注冊事務相關的事件觸發器,這些觸發器由使用者自行提供,在事務進行送出或事務完成時會觸發相應的方法;②判斷目前事務驅動的模式,有預設模式和aspectj模式,對于aspectj模式,Spring會注冊一個AnnotationTransactionAspect類型的bean,用于使用者使用更親近于aspectj的方式進行事務處理;對于預設模式,這裡主要是聲明了三個bean,最終通過Aop的方式進行事務切入。下面我們看一下Spring是如何注冊這三個bean的,如下是AopAutoProxyConfigurer.configureAutoProxyCreator的源碼:

public static void configureAutoProxyCreator(Element element, 
        ParserContext parserContext) {
    // 這個方法主要是在目前BeanFactory中注冊InfrastructureAdvisorAutoProxyCreator這個
    // bean,這個bean繼承了AbstractAdvisorAutoProxyCreator,也就是其實作原理與我們前面
    // 講解的Spring Aop的實作原理幾乎一緻
    AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);

    // 這裡的txAdvisorBeanName就是我們最終要注冊的bean,其類型就是下面注冊的
    // BeanFactoryTransactionAttributeSourceAdvisor,可以看到,其本質是一個
    // Advisor類型的對象,因而Spring Aop會将其作為一個切面織入到指定的bean中
    String txAdvisorBeanName = TransactionManagementConfigUtils
        .TRANSACTION_ADVISOR_BEAN_NAME;
    // 如果目前BeanFactory中已經存在了目标bean,則不進行注冊
    if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) {
        Object eleSource = parserContext.extractSource(element);
        // 注冊AnnotationTransactionAttributeSource,這個bean的主要作用是封裝
        // @Transactional注解中聲明的各個屬性
        RootBeanDefinition sourceDef = new RootBeanDefinition(
       "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource");
        sourceDef.setSource(eleSource);
        sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        String sourceName = parserContext.getReaderContext()
            .registerWithGeneratedName(sourceDef);

        // 注冊TransactionInterceptor類型的bean,并且将上面的封裝屬性的bean設定為其一個屬性。
        // 這個bean本質上是一個Advice(可檢視其繼承結構),Spring Aop使用Advisor封裝實作切面
        // 邏輯織入所需的所有屬性,但真正的切面邏輯卻是儲存在其Advice屬性中的,也就是說這裡的
        // TransactionInterceptor才是真正封裝了事務切面邏輯的bean
        RootBeanDefinition interceptorDef = 
            new RootBeanDefinition(TransactionInterceptor.class);
        interceptorDef.setSource(eleSource);
        interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registerTransactionManager(element, interceptorDef);
        interceptorDef.getPropertyValues().add("transactionAttributeSource", 
            new RuntimeBeanReference(sourceName));
        String interceptorName = parserContext.getReaderContext()
            .registerWithGeneratedName(interceptorDef);

        // 注冊BeanFactoryTransactionAttributeSourceAdvisor類型的bean,這個bean實作了
        // Advisor接口,實際上就是封裝了目前需要織入的切面的所有所需的屬性
        RootBeanDefinition advisorDef = 
            new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class);
        advisorDef.setSource(eleSource);
        advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        advisorDef.getPropertyValues().add("transactionAttributeSource", 
            new RuntimeBeanReference(sourceName));
        advisorDef.getPropertyValues().add("adviceBeanName", interceptorName);
        if (element.hasAttribute("order")) {
            advisorDef.getPropertyValues().add("order", element.getAttribute("order"));
        }
        parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef);

        // 将需要注冊的bean封裝到CompositeComponentDefinition中,并且進行注冊
        CompositeComponentDefinition compositeDef = 
            new CompositeComponentDefinition(element.getTagName(), eleSource);
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(sourceDef, sourceName));
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(interceptorDef, interceptorName));
        compositeDef.addNestedComponent(
            new BeanComponentDefinition(advisorDef, txAdvisorBeanName));
        parserContext.registerComponent(compositeDef);
    }
}           

       如此,Spring事務相關的标簽即解析完成,這裡主要是聲明了一個BeanFactoryTransactionAttributeSourceAdvisor類型的bean到BeanFactory中,其實際為Advisor類型,這也是Spring事務能夠通過Aop實作事務的根本原因。

3. 實作原理

       關于Spring事務的實作原理,這裡需要抓住的就是,其是使用Aop實作的,我們知道,Aop在進行解析的時候,最終會生成一個Adivsor對象,這個Advisor對象中封裝了切面織入所需要的所有資訊,其中就包括最重要的兩個部分就是Pointcut和Adivce屬性。這裡Pointcut用于判斷目标bean是否需要織入目前切面邏輯;Advice則封裝了需要織入的切面邏輯。如下是這三個部分的簡要關系圖:

Spring事務用法示例與實作原理

       同樣的,對于Spring事務,其既然是使用Spring Aop實作的,那麼也同樣會有這三個成員。我們這裡我們隻看到了注冊的Advisor和Advice(即BeanFactoryTransactionAttributeSourceAdvisor和TransactionInterceptor),那麼Pointcut在哪裡呢?這裡我們檢視BeanFactoryTransactionAttributeSourceAdvisor的源碼可以發現,其内部聲明了一個TransactionAttributeSourcePointcut類型的屬性,并且直接在内部進行了實作,這就是我們需要找的Pointcut。這裡這三個對象對應的關系如下:

Spring事務用法示例與實作原理

       這樣,用于實作Spring事務的Advisor,Pointcut以及Advice都已經找到了。關于這三個類的具體作用,我們這裡進行整體的上的講解,後面我們将會深入其内部講解其是如何進行bean的過濾以及事務邏輯的織入的。

  • BeanFactoryTransactionAttributeSourceAdvisor:封裝了實作事務所需的所有屬性,包括Pointcut,Advice,TransactionManager以及一些其他的在Transactional注解中聲明的屬性;
  • TransactionAttributeSourcePointcut:用于判斷哪些bean需要織入目前的事務邏輯。這裡可想而知,其判斷的基本邏輯就是判斷其方法或類聲明上有沒有使用@Transactional注解,如果使用了就是需要織入事務邏輯的bean;
  • TransactionInterceptor:這個bean本質上是一個Advice,其封裝了目前需要織入目标bean的切面邏輯,也就是Spring事務是如果借助于資料庫事務來實作對目标方法的環繞的。

4. 小結

       本文首先通過一個簡單的例子講解了Spring事務是如何使用的,然後講解了Spring事務進行标簽解析的時候做了哪些工作,最後講解了Spring事務是如何與Spring Aop進行一一對應的,并且是如何通過Spring Aop實作将事務邏輯織入目标bean的。

本文來自雲栖社群合作夥伴“開源中國”

本文作者:王練

原文連結