1. 程式人生 > 程式設計 >SpringBoot自動裝配&事務傳播策略

SpringBoot自動裝配&事務傳播策略

SpringBoot自動裝配

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}
複製程式碼

為何使用SpringBoot後,我們只需上述幾行程式碼即可搭建一個web伺服器,比之前使用SpringMVC不要簡潔太多。

這其中奧妙在於@SpringBootApplication註解之中:

@Target(ElementType.TYPE)
@Retention
(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM,classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM,classes = AutoConfigurationExcludeFilter.class) }) public @interface
SpringBootApplication { 複製程式碼

其又繼承了@EnableAutoConfiguration註解:

/** 
 * ...
 * Auto-configuration classes are regular Spring {@link Configuration} beans. They are
 * located using the {@link SpringFactoriesLoader} ..
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { ... } 複製程式碼

檢視註釋可知,自動配置類也是以常規的Spring Bean的形式存在。它們被SpringFactoriesLoader定位:

public final class SpringFactoriesLoader {
    private static Map<String,List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String,String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();

                ...
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]",var13);
            }
        }
    }
}
複製程式碼

該類會從類路徑中的"META-INF/spring.factories"中讀取自動裝配的類列表,其中spring-boot-autoconfigure.jar中的就包含了內嵌Tomcat、SpringMVC、事務等功能的配置類:

org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
複製程式碼

事務傳播策略

前言

方法為維度

由於Spring事務是基於AOP的,所以事務以方法為維度存在於Java程式碼中。而事務的傳播是基於多事務之間相互影響的,所以在程式碼中表現為一個事務方法呼叫另一個事務方法(如下列程式碼中savePersons方法中呼叫saveChildren):

@Service
public class PersonService {

    @Autowired
    private PersonMapper personMapper;
    
    @Transactional
    public void savePersons() {
        Person person = new Person();
        person.setUsername("parent");
        person.setPassword("123");
        personMapper.insertSelective(person);
        
        saveChildren();
    }

    @Transactional
    public void saveChildren() {
        saveChild1();
        saveChild2();
        int i = 1 / 0;
    }

    public void saveChild1() {
        Person person = new Person();
        person.setUsername("child1");
        person.setPassword("456");
        personMapper.insertSelective(person);
    }

    public void saveChild2() {
        Person person = new Person();
        person.setUsername("child2");
        person.setPassword("789");
        personMapper.insertSelective(person);
    }

}
複製程式碼

bean為入口

但是,SpringAOP是基於bean增強的,也就是說當你呼叫一個bean的事務方法(被事務註解修飾的方法)時,該事務註解是可以正常生效的。但如果你呼叫本類中的事務方法,那就相當於將該方法中的程式碼內嵌到當前方法中,即該方法的事務註解會被忽略。

例如:

@Service
public class PersonService {

    @Autowired
    private PersonMapper personMapper;

    public void savePersons() {
        Person person = new Person();
        person.setUsername("parent");
        person.setPassword("123");
        personMapper.insertSelective(person);
        
        saveChildren();
    }

    @Transactional
    public void saveChildren() {
        saveChild1();
        saveChild2();
        int i = 1 / 0;
    }
}
複製程式碼

上列程式碼等效於下列程式碼(saveChildren方法事務註解被忽略掉了):

@Service
public class PersonService {

    @Autowired
    private PersonMapper personMapper;

    public void savePersons() {
        Person person = new Person();
        person.setUsername("parent");
        person.setPassword("123");
        personMapper.insertSelective(person);
        
        saveChild1();
        saveChild2();
        int i = 1 / 0;
    }
}
複製程式碼

因此我們接下來討論的一個事務/非事務方法呼叫另一個事務/非事務方法,這兩個方法被呼叫的方式都是基於bean作為方法引用的,而非通過this呼叫本類中的方法。因此我們不妨將兩個寫庫方法saveChildrensavePersons移入兩個bean中進行測試:

@Service
public class PersonService2 {
    @Autowired
    private PersonMapper personMapper;

    public void saveChildren() {
        saveChild1();
        saveChild2();
    }

    public void saveChild1() {
        Person person = new Person();
        person.setUsername("child1");
        person.setPassword("456");
        personMapper.insertSelective(person);
    }

    public void saveChild2() {
        Person person = new Person();
        person.setUsername("child2");
        person.setPassword("789");
        personMapper.insertSelective(person);
    }
}
複製程式碼
@Service
public class PersonService {

    @Autowired
    private PersonMapper personMapper;

    @Autowired
    private PersonService2 personService2;

    public void savePersons() {
        Person person = new Person();
        person.setUsername("parent");
        person.setPassword("123");
        personMapper.insertSelective(person);

        personService2.saveChildren();
    }	
}
複製程式碼
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class TransTest {

    @Autowired
    private PersonService personService;

    @Test
    public void test() {
        personService.savePersons();
    }
}
複製程式碼

當前事務&父方法事務

本文中說父方法是否建立事務,不僅僅是指呼叫本方法的呼叫方,而是泛指方法呼叫鏈的上游方法,只要上游方法中的任意一個方法開啟了事務,那麼當前方法的執行就處於事務之中,也即執行當前方法時存在事務。

策略列舉

Spring事務傳播策略相關的列舉類如下:

package org.springframework.transaction.annotation;

public enum Propagation {
    REQUIRED(0),SUPPORTS(1),MANDATORY(2),REQUIRES_NEW(3),NOT_SUPPORTED(4),NEVER(5),NESTED(6);

    private final int value;

    private Propagation(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}
複製程式碼

REQUIRED——有飯就吃,沒飯自己買

@Transactional註解預設的傳播策略就是REQUIRED

public @interface Transactional {
    Propagation propagation() default Propagation.REQUIRED;
}
複製程式碼
/**
	 * Support a current transaction,create a new one if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>This is the default setting of a transaction annotation.
	 */
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),複製程式碼

被該註解表示:如果當前方法有事務,則支援當前事務;如果當前方法沒有事務,則新建事務供自己使用。

父無,子自力更生

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}
複製程式碼
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1/0;
}
複製程式碼

由於父方法沒有註解,執行到第7行時,呼叫了傳播策略為REQUIRED的事務方法,其自己新建事務供自己使用,因此child1,child2因為1/0異常不會被插入,異常拋至父方法,父方法因為沒有事務所以不會回滾之前插入的parent,執行結果如下:

----+----------+----------+
| id | username | password |
+----+----------+----------+
| 23 | parent   | 123      |
+----+----------+----------+

複製程式碼

父有,子繼承

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1 / 0;
}

複製程式碼
@Autowired
private PersonService personService;

@Test
public void test() {
    personService.savePersons();
}

複製程式碼

由於test呼叫bean personService的事務方法savePersons且其傳播策略為REQUIRED,於是其新建一個事務給自己用,當呼叫bean personService2的REQUIRED事務方法時,發現當前有事務因此支援當前事務,因此parent、child1、child2的插入由於在同一個事務中,因此在1/0異常丟擲後都被回滾:

mysql> select * from person;
Empty set (0.00 sec)

複製程式碼

俚語

REQUIRED,老闆(父方法)有飯吃(有事務),我(子方法)跟著老闆吃(支援當前事務);老闆沒飯吃,我自己買飯吃

SUPPORTS——有飯就吃,沒飯餓肚子

/**
	 * Support a current transaction,execute non-transactionally if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p>Note: For transaction managers with transaction synchronization,* {@code SUPPORTS} is slightly different from no transaction at all,* as it defines a transaction scope that synchronization will apply for.
	 * As a consequence,the same resources (JDBC Connection,Hibernate Session,etc)
	 * will be shared for the entire specified scope. Note that this depends on
	 * the actual synchronization configuration of the transaction manager.
	 * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
	 */
	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),複製程式碼

如果當前有事務,則支援當前事務,否則以非事務的方式執行當前方法

父有子支援

當父方法會建立事務時,子方法用SUPPORTS和用REQUIRED效果是一樣的

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.SUPPORTS)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1 / 0;
}

複製程式碼
mysql> select * from person;
Empty set (0.00 sec)

複製程式碼

父無子無

當父方法沒有建立事務,那麼子方法也不會自作主張去新建事務:

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.SUPPORTS)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1 / 0;
}

複製程式碼
mysql> select * from person;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 29 | parent   | 123      |
| 30 | child1   | 456      |
| 31 | child2   | 789      |
+----+----------+----------+

複製程式碼

俚語

SUPPORTS:老闆有飯吃,我跟著老闆吃;老闆沒飯吃,我就只能餓肚子了。

MANDATORY——必須要有飯吃

/**
	 * Support a current transaction,throw an exception if none exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),複製程式碼

強制以事務的方式執行當前方法:如果當前有事務,那麼支援當前事務,否則丟擲異常

父有子支援

這點REQUIRED,SUPPORTS,MANDATORY是一樣的

父無子罷工

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.MANDATORY)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1 / 0;
}

複製程式碼

當執行到personService2的MANDATORY事務方法時,發現當前沒有事務,於是它直接丟擲一個異常:

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

複製程式碼
mysql> select * from person;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 32 | parent   | 123      |
+----+----------+----------+

複製程式碼

俚語

MANDATORY:老闆有飯吃,跟著老闆吃;老闆沒飯吃,老子不幹了。有飯才幹活

REQUIRES_NEW——自力更生

/**
	 * Create a new transaction,and suspend the current transaction if one exists.
	 * Analogous to the EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},* which requires the {@code javax.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Java EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),複製程式碼

不管當前有沒有事務,自己都會新建一個事務為自己所用,並且如果當前有事務那麼就會掛起當前事務

父無子自強

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChildren() {
    saveChild1();
    saveChild2();
    int i = 1 / 0;
}

複製程式碼

此情景下,REQUIRED_NEW同REQUIRED

mysql> select * from person;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 33 | parent   | 123      |
+----+----------+----------+

複製程式碼

父有子也不稀罕

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
    
    int i = 1 / 0;
}

複製程式碼
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChildren() {
    saveChild1();
    saveChild2();
}

複製程式碼

這次我將1/0移入了父方法中,父方法有事務因此parent的插入會回滾,但是子方法的執行會掛起當前事務另建新事務,因此子方法的插入依然有效(子方法執行結束後父方法的事務又會被自動恢復)

mysql> select * from person;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 40 | child1   | 456      |
| 41 | child2   | 789      |
+----+----------+----------+

複製程式碼

俚語

REQUIRED_NEW:不管老闆有沒有飯吃,我都自己買飯吃,不接受他人的恩惠。

NOT_SUPPORTED——不吃飯只幹活

/**
	 * Execute non-transactionally,suspend the current transaction if one exists.
	 * Analogous to EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},* which requires the {@code javax.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Java EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),複製程式碼

強制以非事務的方式執行當前程式碼,如果當前有事務則將其掛起。

父有子不用

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveChildren() {
    saveChild1();
    int i = 1 / 0;
    saveChild2();
}

複製程式碼

執行子方法時,事務被掛起,因此child1的插入未被回滾,回到父方法後事務被恢復,因此parent的插入被回滾

+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 43 | child1   | 456      |
+----+----------+----------+

複製程式碼

父無遂子意

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveChildren() {
    saveChild1();
    int i = 1 / 0;
    saveChild2();
}

複製程式碼

本來就不想以事務的方式執行此方法,如果當前沒有事務,豈不正合我意,於是parent、child1都沒喲回滾

+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 44 | parent   | 123      |
| 45 | child1   | 456      |
+----+----------+----------+

複製程式碼

俚語

NOT_SUPPORT:不管老闆有沒有飯,我都不吃,我是個只愛幹活的機器

NEVER——一說吃飯就罷工

/**
	 * Execute non-transactionally,throw an exception if a transaction exists.
	 * Analogous to EJB transaction attribute of the same name.
	 */
	NEVER(TransactionDefinition.PROPAGATION_NEVER),複製程式碼

以非事務方式執行此方法,如果當前有事務直接異常

父無子無事

public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.NEVER)
public void saveChildren() {
    saveChild1();
    int i = 1 / 0;
    saveChild2();
}

複製程式碼

此情景和不用事務效果一樣

+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 46 | parent   | 123      |
| 47 | child1   | 456      |
+----+----------+----------+

複製程式碼

父有子罷工

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();
}

複製程式碼
@Transactional(propagation = Propagation.NEVER)
public void saveChildren() {
    saveChild1();
    int i = 1 / 0;
    saveChild2();
}

複製程式碼

呼叫子方法時因為當前有事務,因此子方法直接丟擲異常,parent的插入回滾,子方法沒有執行自然沒有插入資料

mysql> select * from person;
Empty set (0.00 sec)

複製程式碼

俚語

NEVER:老闆一提吃飯,我就不幹了。

NESTED

/**
	 * Execute within a nested transaction if a current transaction exists,* behave like {@code REQUIRED} otherwise. There is no analogous feature in EJB.
	 * <p>Note: Actual creation of a nested transaction will only work on specific
	 * transaction managers. Out of the box,this only applies to the JDBC
	 * DataSourceTransactionManager. Some JTA providers might support nested
	 * transactions as well.
	 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
	 */
	NESTED(TransactionDefinition.PROPAGATION_NESTED);

複製程式碼

如果當前沒有事務,那麼效果同REQUIRED;否則以當前事務巢狀事務的方式執行此方法。外層事務的回滾會導致內層事務回滾(即使內層事務正常執行)。

@Transactional(propagation = Propagation.REQUIRED)
public void savePersons() {
    Person person = new Person();
    person.setUsername("parent");
    person.setPassword("123");
    personMapper.insertSelective(person);

    personService2.saveChildren();

    int i = 1 / 0;
}

複製程式碼
@Transactional(propagation = Propagation.NESTED)
public void saveChildren() {
    saveChild1();
    saveChild2();
}

複製程式碼

雖然子方法以巢狀事務的方式正常執行,但巢狀事務的操作要等當前事務模型中最外層事務的提交一併寫庫,否則會跟隨外層事務一同回滾。這點要和REQUIRED_NEW區分開,REQUIRED_NEW是掛起當前事務另建新事務(兩事務互不影響),而非在當前事務下建巢狀事務(巢狀事務受當前事務的牽制)。

因此,內層事務的提交會和外層事務一同回滾:

mysql> select * from person;
Empty set (0.00 sec)

複製程式碼

俚語

NESTED:老闆沒飯吃,自己買飯吃,想吃啥吃啥;老闆有飯吃,跟著老闆吃,吃啥得看老闆心情