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
呼叫本類中的方法。因此我們不妨將兩個寫庫方法saveChildren
和savePersons
移入兩個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:老闆沒飯吃,自己買飯吃,想吃啥吃啥;老闆有飯吃,跟著老闆吃,吃啥得看老闆心情