1. Transaction usage example in JDBC mode
1) Create data table structure
CREATE TABLE user ( id int(11) NOT NULL auto increment, name varchar (255) default NULL , age int ( 11 ) default NULL , sex varchar (255 ) default NULL , PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2) Create corresponding PO
public class User { private int id; private String name; private int age; private String sex; // Omit getter and setter methods }
3) Create mapping between tables and entities
public class UserRowMapper implements RowMapper<User> { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User person = new User(rs.getInt("id"), rs.getString("name"), rs.getInt("age"), rs.getString("sex")); return person; } }
4) Create data operation interface
public interface UserService { void save(User user); }
5) Create data operation interface implementation class
public class UserServiceImpl implements UserService { private JdbcTemplate jdbcTemplate; /** * set up data sources */ public void setDataSource(DataSource dataSource) { jdbcTemplate = new JdbcTemplate(dataSource); } @Override public void save(User user) { dbcTemplate.update( "INSERT INTO `user` (`name`, age, sex) VALUES (?, ? ,?)", new Object[]{user.getName(), user.getAge(), user.getSex()}, new int[]{Types.VARCHAR, Types.INTEGER, Types.VARCHAR}); // Transaction test, plus this code, the data will not be saved to the database throw new RuntimeException("aa"); } }
6) Create Spring configuration file
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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/tx http://www.springframework.org/schema/tool/spring-tx.xsd"> <tx:annotation-driven transaction-manager="transactionManager" /> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- Configure data source--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="com.mysql.jdbc.Driver" /> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/study" /> <property name="user" value="root" /> <property name="password" value="123456" /> <!-- Initial value of connection pool startup--> <property name="initialPoolSize" value="1" /> <!-- Connection pool maximum--> <property name="maxPoolSize" value="300" /> <!-- Connection pool minimum--> <property name="minPoolSize" value="1" /> </bean> <!-- Allocation industry bean--> <bean id="userService" class="org.springframework.test.tx.UserServiceImpl"> <property name="dataSource" ref="dataSource" /> </bean> </beans>
7) Testing
public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext factory = new ClassPathXmlApplicationContext("test/tx/bean.xml"); UserService userService = (UserService) factory.getBean("userService"); User user = new User(); user.setName("Li Si"); user.setAge(20); user.setSex("male"); userService.save(user); } }
By default, transactions in Spring only roll back the RuntimeException method. If an exception is thrown, the saved code will not take effect.
2 transaction custom label
The < TX: annotation driven / > configuration in the Spring configuration file is the switch of transactions. If it is not configured here, there will be no transaction function in Spring, which needs to be analyzed from this configuration.
The key is the TxNamespaceHandler#init method.
public void init() { registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); }
According to the usage rules of the custom tag and the above code, Spring will use the parse method of the AnnotationDrivenBeanDefinitionParser class to parse the configuration starting with < TX: annotation driven / >.
public BeanDefinition parse(Element element, ParserContext parserContext) { registerTransactionalEventListenerFactory(parserContext); String mode = element.getAttribute("mode"); // If AspectJ is used if ("aspectj".equals(mode)) { // mode="aspectj" registerTransactionAspect(element, parserContext); if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { registerJtaTransactionAspect(element, parserContext); } } else { // mode="proxy" AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); } return null; }
There is a judgment on the mode attribute in the parsing. According to the needs of the code, the AspectJ method is used for transaction entry (the book house in Spring is based on AOP). Then you can use this configuration:
<tx:annotation-driven transaction-manager="transactionManager" mode="aspectj"/>
2.1 register infrastructure advisor autoproxycreator
Take the default configuration as an example for analysis, and enter the method of aopautoproxyconfigurator #configureautoproxycreator for analysis:
public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { // Register infrastructure advisor autoproxycreator AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); // The name of the transaction enhancer is org springframework. transaction. config. internalTransactionAdvisor String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { Object eleSource = parserContext.extractSource(element); // Create the TransactionAttributeSource definition. // Create a bean for TransactionAttributeSource RootBeanDefinition sourceDef = new RootBeanDefinition( "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); sourceDef.setSource(eleSource); sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); // Create the TransactionInterceptor definition. // Create bean of TransactionInterceptor 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); // Create the TransactionAttributeSourceAdvisor definition. // Create bean of BeanFactoryTransactionAttributeSourceAdvisor RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); advisorDef.setSource(eleSource); advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // Inject the bean of sourceName into the transactionAttributeSource attribute of advisorDef advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); // Inject the bean of interceptorName into the adviceBeanName attribute of advisorDef advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); // If the order attribute is configured if (element.hasAttribute("order")) { advisorDef.getPropertyValues().add("order", element.getAttribute("order")); } //Register beanDefinition parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); // Create 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); } }
The above code registers three beans, TransactionAttributeSource, TransactionInterceptor and BeanFactoryTransactionAttributeSourceAdvisor. These three beans support the whole transaction function, so how are these three beans organized?
TransactionAttributeSource and TransactionInterceptor are injected into a bean named advisorDef as attributes, which uses BeanFactoryTransactionAttributeSourceAdvisor as its class attribute. That is, BeanFactoryTransactionAttributeSourceAdvisor represents the current bean, as shown in the following figure
Also, the first sentence of the above method registers a bean of type infrastructureasuggestorautoproxycreator.
AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element);
Enter the function.
/** * Register infrastructure advisor autoproxycreator */ public static void registerAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( parserContext.getRegistry(), parserContext.extractSource(sourceElement)); useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); registerComponentIfNecessary(beanDefinition, parserContext); } public static BeanDefinition registerAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); }
View the class hierarchy of infrastructure advisor autoproxycreator.
It can be seen from this structure that it indirectly implements the BeanPostProcessor interface. All bean instantiations will call its postProcessAfterInitialization method and implement it in its parent class AbstractAutoProxyCreator.
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { // Build a key according to the class and name of a given bean, in the form of bean classname_ beanName Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyProxyReferences.remove(cacheKey) != bean) { // If it is suitable to be proxied, it needs to encapsulate the specified bean return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; }
The main purpose of this implementation is to encapsulate the specified bean, and delegate the work of detection and encapsulation to the wrapIfNecessary method.
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // If it has been handled if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; } // No enhancement required if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } // Whether the given infrastructure bean class does not need to be configured automatically, or whether the given infrastructure bean class does not need to be configured automatically if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } //EmbeddedValueResolverAware // Create proxy if we have advice. // If there is an enhancement method, create a proxy // Advisor @Before(), etc Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); // If you get the enhancement, you need to create a proxy for the enhancement class if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // Create proxy Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; }
The main logic of the above method is:
- Find the enhancer corresponding to the specified bean.
- Create an agent based on the found enhancer.
2.2 obtain the enhancer of the corresponding class/method
Get the corresponding enhancer, that is, in the getAdvicesAndAdvisorsForBean method, you not only need to find the enhancer, but also need to judge whether the enhancer meets the requirements.
// AbstractAdvisorAutoProxyCreator.java protected Object[] getAdvicesAndAdvisorsForBean( Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) { // Find the right enhancer List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { // Get all candidate intensifiers List<Advisor> candidateAdvisors = findCandidateAdvisors(); // Filter out the appropriate advisors for the current bean List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); extendAdvisors(eligibleAdvisors); if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; }
In the above method, Spring divides the task into two function points: obtaining all the enhancers and whether the enhancers match.
1) Looking for candidate intensifiers
The method findCandidateAdvisors completes this function.
// AbstractAdvisorAutoProxyCreator.java protected List<Advisor> findCandidateAdvisors() { Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); return this.advisorRetrievalHelper.findAdvisorBeans(); } // BeanFactoryAdvisorRetrievalHelper.java public List<Advisor> findAdvisorBeans() { // Determine list of advisor bean names, if not cached already. String[] advisorNames = this.cachedAdvisorBeanNames; if (advisorNames == null) { // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the auto-proxy creator apply to them! // Get all corresponding advisor Class, because the previously registered BeanFactoryTransactionAttributeSourceAdvisor // The Advisor interface is also implemented. This bean will also be extracted when obtaining all the enhancers, and will be woven together with other enhancers in the subsequent steps advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Advisor.class, true, false); this.cachedAdvisorBeanNames = advisorNames; } if (advisorNames.length == 0) { return new ArrayList<>(); } List<Advisor> advisors = new ArrayList<>(); for (String name : advisorNames) { if (isEligibleBean(name)) { if (this.beanFactory.isCurrentlyInCreation(name)) { if (logger.isTraceEnabled()) { logger.trace("Skipping currently created advisor '" + name + "'"); } } else { try { advisors.add(this.beanFactory.getBean(name, Advisor.class)); } catch (BeanCreationException ex) { Throwable rootCause = ex.getMostSpecificCause(); if (rootCause instanceof BeanCurrentlyInCreationException) { BeanCreationException bce = (BeanCreationException) rootCause; String bceBeanName = bce.getBeanName(); if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { if (logger.isTraceEnabled()) { logger.trace("Skipping advisor '" + name + "' with dependency on currently created bean: " + ex.getMessage()); } // Ignore: indicates a reference back to the bean we're trying to advise. // We want to find advisors other than the currently created bean itself. continue; } } throw ex; } } } } return advisors; }
The above method first obtains all corresponding advisors through beanfactoryutils#beannamesfortypeincludingagents method bean name of class.
public static String[] beanNamesForTypeIncludingAncestors( ListableBeanFactory lbf, Class<?> type, boolean includeNonSingletons, boolean allowEagerInit);
When the beanName of the intensifier in the container is known, obtain the corresponding instance through BeanFactory#getBean.
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
At the beginning, when Spring parses the < TX: annotation driven / > tag, it registers a bean of BeanFactoryTransactionAttributeSourceAdvisor, and injects two other beans into this bean. At this time, the bean will be used. Because it also implements the Advisor interface, whether this bean will be extracted naturally after obtaining all the enhancers and woven into the agent in the subsequent steps together with other enhancers.
2) The candidate intensifier found a match
After finding the corresponding intensifier, you need to find out whether the class/method matches.
protected List<Advisor> findAdvisorsThatCanApply( List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) { ProxyCreationContext.setCurrentProxiedBeanName(beanName); try { // Filter the obtained advisors return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); } finally { ProxyCreationContext.setCurrentProxiedBeanName(null); } } // AopUtils.java public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { if (candidateAdvisors.isEmpty()) { return candidateAdvisors; } List<Advisor> eligibleAdvisors = new ArrayList<>(); // First, deal with the DeclareParents advisor that introduces the enhanced DeclareParents annotation ID for (Advisor candidate : candidateAdvisors) { if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { eligibleAdvisors.add(candidate); } } boolean hasIntroductions = !eligibleAdvisors.isEmpty(); for (Advisor candidate : candidateAdvisors) { // Referral enhancement has been processed if (candidate instanceof IntroductionAdvisor) { // already processed continue; } // Processing of ordinary bean s if (canApply(candidate, clazz, hasIntroductions)) { eligibleAdvisors.add(candidate); } } return eligibleAdvisors; } public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { // DeclareParent if (advisor instanceof IntroductionAdvisor) { // classFilter = TypePatternClassFilter return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); } // General advisor else if (advisor instanceof PointcutAdvisor) { PointcutAdvisor pca = (PointcutAdvisor) advisor; return canApply(pca.getPointcut(), targetClass, hasIntroductions); } else { // It doesn't have a pointcut so we assume it applies. return true; } }
When analyzing whether this enhancement method is applicable to UserService, the current advisor is the BeanFactoryTransactionAttributeSourceAdvisor found earlier.
From the class hierarchy, we can see that BeanFactoryTransactionAttributeSourceAdvisor indirectly implements PointcutAdvisor, so it will pass the judgment in the second if judgment. The parameter returned by the getPointcut() method in BeanFactoryTransactionAttributeSourceAdvisor will continue to call the canApply method, which returns an instance of TransactionAttributeSourcePointcut. For the transactionAttributeSource attribute, it is the annotation transactionAttributeSource injected when parsing the custom tag.
// BeanFactoryTransactionAttributeSourceAdvisor.java private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { @Override @Nullable protected TransactionAttributeSource getTransactionAttributeSource() { return transactionAttributeSource; } }; public Pointcut getPointcut() { return this.pointcut; }
Continue to track canApply using an instance of transactionAttributeSource type as a function parameter.
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { Assert.notNull(pc, "Pointcut must not be null"); if (!pc.getClassFilter().matches(targetClass)) { return false; } // pc = TransactionAttributeSourcePointcut // Return is itself MethodMatcher methodMatcher = pc.getMethodMatcher(); if (methodMatcher == MethodMatcher.TRUE) { // No need to iterate the methods if we're matching any method anyway... return true; } IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; if (methodMatcher instanceof IntroductionAwareMethodMatcher) { introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; } Set<Class<?>> classes = new LinkedHashSet<>(); if (!Proxy.isProxyClass(targetClass)) { classes.add(ClassUtils.getUserClass(targetClass)); } // Get all the interfaces implemented by the target class classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); for (Class<?> clazz : classes) { Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); for (Method method : methods) { // If there is a method matching, it returns true if (introductionAwareMethodMatcher != null ? introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : methodMatcher.matches(method, targetClass)) { return true; } } } return false; }
First, all interfaces of the corresponding class are obtained and traversed together with the same class itself. In the traversal process, the methods in the class are traversed again. Once the matching is successful, it is considered that this class is suitable for the current enhancer.
Call methodmatcher Matches (method, targetclass) this method will call the matches method of TransactionAttributeSourcePointcut class.
// TransactionAttributeSourcePointcut.java public boolean matches(Method method, Class<?> targetClass) { if (TransactionalProxy.class.isAssignableFrom(targetClass) || PlatformTransactionManager.class.isAssignableFrom(targetClass) || PersistenceExceptionTranslator.class.isAssignableFrom(targetClass)) { return false; } // Annotation transaction attributesource is injected during custom label parsing TransactionAttributeSource tas = getTransactionAttributeSource(); return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); }
At this time, tas represents the AnnotationTransactionAttributeSource type.
// AbstractFallbackTransactionAttributeSource.java public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) { if (method.getDeclaringClass() == Object.class) { return null; } // First, see if we have a cached value. // Trying to get from cache Object cacheKey = getCacheKey(method, targetClass); TransactionAttribute cached = this.attributeCache.get(cacheKey); if (cached != null) { // Value will either be canonical value indicating there is no transaction attribute, // or an actual transaction attribute. if (cached == NULL_TRANSACTION_ATTRIBUTE) { return null; } else { return cached; } } else { // We need to work it out. // Extract transaction tag TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); // Put it in the cache. if (txAttr == null) { this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); } else { String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); if (txAttr instanceof DefaultTransactionAttribute) { ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); } if (logger.isTraceEnabled()) { logger.trace("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); } this.attributeCache.put(cacheKey, txAttr); } return txAttr; } }
The above method first loads from the cache at this time. If it is not found, it delegates the computeTransactionAttribute method to obtain the transaction tag.
3) Extract transaction tag
// AbstractFallbackTransactionAttributeSource.java protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. // Method represents the method in the interface, and specificMethod represents the method in the implementation class Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); // First try is the method in the target class. // Check whether there is a transaction declaration in the method TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } // Second try is the transaction attribute on the target class. // Find whether there is a transaction declaration in the class where the method is located txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } // If there is an interface, look for it in the interface if (specificMethod != method) { // Fallback is to look at the original method. // Method of finding interface txAttr = findTransactionAttribute(method); if (txAttr != null) { return txAttr; } // Last fallback is the class of the original method. // Look in the class in the interface txAttr = findTransactionAttribute(method.getDeclaringClass()); if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { return txAttr; } } return null; }
Acquisition rules of transaction attributes:
- If there are transaction attributes in the method, the attributes on the method are used.
- Otherwise, use the property on the class where the method is located.
- If there is no method on the class where the method is located, search the method in the interface.
- Finally, try to search the properties on the class of the interface.
The real search transaction attribute task is delegated to the findTransactionAttribute method.
// AnnotationTransactionAttributeSource.java protected TransactionAttribute findTransactionAttribute(Method method) { return determineTransactionAttribute(method); } protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) { // this.annotationParsers is initialized when the current class AnnotationTransactionAttributeSource is initialized // The value is added to the spring transaction annotation parser for (TransactionAnnotationParser annotationParser : this.annotationParsers) { TransactionAttribute attr = annotationParser.parseTransactionAnnotation(element); if (attr != null) { return attr; } } return null; }
this.annotationParsers are initialized when the current class AnnotationTransactionAttributeSource is initialized, and the value is added to the spring transactionannotationparser, that is, the spring transactionannotationparser #parsetransactionannotation method is used for attribute acquisition.
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) { // Get Transactional annotation information AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( element, Transactional.class, false, false); if (attributes != null) { return parseTransactionAnnotation(attributes); } else { return null; } }
The above method will first judge whether the current class has Transactional annotation, which is the basis of the transaction. If so, continue to call the parseTransactionAnnotation method to resolve the detailed attributes.
protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); // Parsing propagation Propagation propagation = attributes.getEnum("propagation"); rbta.setPropagationBehavior(propagation.value()); // Parsing isolation Isolation isolation = attributes.getEnum("isolation"); rbta.setIsolationLevel(isolation.value()); // Parsing timeout rbta.setTimeout(attributes.getNumber("timeout").intValue()); // Parse readOnly rbta.setReadOnly(attributes.getBoolean("readOnly")); // Parse value rbta.setQualifier(attributes.getString("value")); List<RollbackRuleAttribute> rollbackRules = new ArrayList<>(); // Resolve rollbackFor for (Class<?> rbRule : attributes.getClassArray("rollbackFor")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } // Resolve rollbackForClassName for (String rbRule : attributes.getStringArray("rollbackForClassName")) { rollbackRules.add(new RollbackRuleAttribute(rbRule)); } // Parse noRollbackFor for (Class<?> rbRule : attributes.getClassArray("noRollbackFor")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } // Resolve noRollbackForClassName for (String rbRule : attributes.getStringArray("noRollbackForClassName")) { rollbackRules.add(new NoRollbackRuleAttribute(rbRule)); } rbta.setRollbackRules(rollbackRules); return rbta; }
The above method implements the transaction attribute parsing of the corresponding class or method. You will see any transaction attribute extraction you use or don't use in this class.
So far, the resolution of the transaction tag has been completed. At the beginning, our task is to find out whether an enhancer is suitable for the corresponding class. The key to whether it matches or not lies in who finds the corresponding transaction attribute from the specified class or the method in the class. Taking UserServicemImpl as an example, the transaction attribute has been found in its interface UserService. Therefore, it matches the transaction enhancer, That is, it will be decorated by transaction function.
So far, the initialization of the transaction function has been completed. When it is judged that a bean is suitable for transaction enhancement, that is, it is suitable for the enhancer BeanFactoryTractionAttributeSourceAdvisor.
When the proxy class is called, the enhanced method of this class will be called, which is the Advise of the BeanFactoryTransactionAttributeSourceAdvisor instance. Because the TransactionInterceptor is injected into the BeanFactoryTransactionAttributeSourceAdvisor, the TransactionInterceptor will be executed first for enhancement when calling the proxy class of the transaction intensifier, At the same time, the logic of the whole transaction is completed in the invoke method in the TransactionInterceptor instance.
3 transaction enhancer
The TransactionInterceptor class supports the architecture of the whole transaction function. TransactionInterceptor implements MethodInterceptor, so calling this class starts with the invoke method.
public Object invoke(MethodInvocation invocation) throws Throwable { // Work out the target class: may be {@code null}. // The TransactionAttributeSource should be passed the target class // as well as the method, which may be from an interface. Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); } // TransactionAspectSupport.java protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); // Get the corresponding transaction attribute final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); // Get transactionManager in beanFactory final PlatformTransactionManager tm = determineTransactionManager(txAttr); // Unique identifier of construction method (class. Method, such as service.UserServiceImpl.save) final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); // Declarative transaction if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. // Create transaction information TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. // Execute enhanced method retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception // Abnormal rollback completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // Clear information cleanupTransactionInfo(txInfo); } // Commit transaction commitTransactionAfterReturning(txInfo); return retVal; } // Programming transaction else { final ThrowableHolder throwableHolder = new ThrowableHolder(); // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in. try { Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> { TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); try { return invocation.proceedWithInvocation(); } catch (Throwable ex) { if (txAttr.rollbackOn(ex)) { // A RuntimeException: will lead to a rollback. if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else { throw new ThrowableHolderException(ex); } } else { // A normal return value: will lead to a commit. throwableHolder.throwable = ex; return null; } } finally { cleanupTransactionInfo(txInfo); } }); // Check result state: It might indicate a Throwable to rethrow. if (throwableHolder.throwable != null) { throw throwableHolder.throwable; } return result; } catch (ThrowableHolderException ex) { throw ex.getCause(); } catch (TransactionSystemException ex2) { if (throwableHolder.throwable != null) { logger.error("Application exception overridden by commit exception", throwableHolder.throwable); ex2.initApplicationException(throwableHolder.throwable); } throw ex2; } catch (Throwable ex2) { if (throwableHolder.throwable != null) { logger.error("Application exception overridden by commit exception", throwableHolder.throwable); } throw ex2; } } }
Spring supports declarative transactions and programmatic transactions. Take declarative transactions as an example. The main logic is as follows.
- Get the transaction attribute transactionattribute (@ transactional)
- Load TransactionManager in configuration file
- Different transaction processing methods use different logic
The difference between declarative transactions and programmatic transactions is that, first, programmatic transactions do not require transaction attributes. Second, the difference is the TransactionManager. CallbackPreferringPlatformTransactionManager inherits the PlatformTransactionManager interface and exposes a method used to execute callbacks in transaction processing. - Obtain transactions and collect transaction information before the target method is executed
Transaction attributes and transaction information are different, that is, TransactionInfo and TransactionAttribute are different. TransactionInfo contains TransactionAttribute, platform transactionmanager, TransactionStatus and other information. - Execution target method
- An exception occurred. Try exception handling
By default, only RuntimeException exceptions are rolled back. - Transaction information cleanup before committing a transaction
- Commit transaction
3.1 create transaction
The creation of a transaction is implemented by the createTransactionIfNecessary method.
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { // If no name specified, apply method identification as transaction name. // If there is no name, the method is uniquely identified and txAttr is encapsulated with DelegatingTransactionAttribute if (txAttr != null && txAttr.getName() == null) { txAttr = new DelegatingTransactionAttribute(txAttr) { @Override public String getName() { return joinpointIdentification; } }; } TransactionStatus status = null; if (txAttr != null) { if (tm != null) { // Get TransactionStatus status = tm.getTransaction(txAttr); } else { if (logger.isDebugEnabled()) { logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured"); } } } //Prepare a TransactionInfo in status according to the specified attribute return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }
The main logic of the above method:
- Encapsulate TransactionAttribute with DelegatingTransactionAttribute
The current actual type is RuleBasedTransactionAttribute. - Get transactions, core tasks
- Build transaction information
3.1.1 get transaction
Use the getTransaction method to handle the preparation of the transaction.
// AbstractPlatformTransactionManager.java public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { // Transaction acquisition Object transaction = doGetTransaction(); // Cache debug flag to avoid repeated checks. boolean debugEnabled = logger.isDebugEnabled(); if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } // Judge whether there is a transaction in the current thread. The judgment basis is that the connection recorded by the current thread is not empty and the transactionActive attribute in the connection holder is not empty if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. // The current thread already has a transaction return handleExistingTransaction(definition, transaction, debugEnabled); } // Check definition settings for new transaction. // Transaction timeout verification if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); } // No existing transaction found -> check propagation behavior to find out how to proceed. // If there is no thread in the current thread, then propagation is declared as PROPAGATION_MANDATORY threw an exception if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { // New transaction // Empty pending SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); // Construct transaction, including setting ConnectionHolder, isolation level and timeout. If it is a new connection, bind to the current thread doBegin(transaction, definition); // The setting of the new synchronization transaction, which refers to the setting of the current thread prepareSynchronization(status, definition); return status; } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } else { // Create "empty" transaction: no actual transaction, but potentially synchronization. if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + definition); } boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); } }
The main logic of the above method:
- Get transaction
Create a corresponding transaction instance. Here, the doGetTransaction method in DataSourceTransactionManager is used to create a JDBC based transaction instance. If there is a connection about dataSource in the current thread, it can be used directly. Here is a setting for savepoints. Whether to enable allow savepoints depends on whether to allow embedded transactions is set.
// DataSourceTransactionManager.java protected Object doGetTransaction() { DataSourceTransactionObject txObject = new DataSourceTransactionObject(); txObject.setSavepointAllowed(isNestedTransactionAllowed()); // If the current thread has recorded the database, the original connection is used ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); // false indicates a non new connection txObject.setConnectionHolder(conHolder, false); return txObject; }
- If there is a transaction in the current thread, turn to the processing of nested transactions
- Transaction timeout setting validation
- Verification of setting of transaction propagationBehavior property
- Build DefaultTransactionStatus
- Improve the transaction, including setting ConnectionHolder, isolation level and timeout. If it is a new connection, it will be bound to the current thread
For some isolation levels, the setting of timeout and other functions is not completed by Spring, but entrusted to the underlying database connection, and the setting of database connection is handled in the doBegin function.
protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection(); // Set isolation level Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); // Switch to manual commit if necessary. This is very expensive in some JDBC drivers, // so we don't want to do it unnecessarily (for example if we've explicitly // configured the connection pool to set it already). //Change the automatic submission settings, and Spring controls the submission if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } con.setAutoCommit(false); } // Set the basis for judging whether the current thread has transactions prepareTransactionalConnection(con, definition); txObject.getConnectionHolder().setTransactionActive(true); int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } // Bind the connection holder to the thread. if (txObject.isNewConnectionHolder()) { // Bind the currently acquired connection to the current thread TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { if (txObject.isNewConnectionHolder()) { DataSourceUtils.releaseConnection(con, obtainDataSource()); txObject.setConnectionHolder(null, false); } throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); } }
It can be said that the transaction starts from this method, because i in this method has begun to try to obtain the database connection. While obtaining the database connection, you need to set some synchronization settings.
- Trying to get a connection
If the connectionHolder already exists in the current thread, it is not necessary to obtain it again, or if the transaction synchronization indication is set to true, it is necessary to obtain the connection again. - Set isolation level and read-only identification
Some core operations in the implementation of read-only transaction are set on the read-only attribute. The control of isolation level is also controlled by connection. - Change the default submission settings and delegate the submission operation to Spring
- Set the flag bit to indicate that the current connection has been activated by the transaction
- Set expiration time
- Bind connectionHolder to current thread
The prepareConnectionForTransaction method that sets the isolation level is used to set the connection to the underlying database. It only contains the settings of read-only ID and isolation level.
// DataSourceUtils.java public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException { Assert.notNull(con, "No Connection specified"); // Set read-only flag. // Set database read-only identity if (definition != null && definition.isReadOnly()) { try { if (logger.isDebugEnabled()) { logger.debug("Setting JDBC Connection [" + con + "] read-only"); } con.setReadOnly(true); } catch (SQLException | RuntimeException ex) { Throwable exToCheck = ex; while (exToCheck != null) { if (exToCheck.getClass().getSimpleName().contains("Timeout")) { // Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0 throw ex; } exToCheck = exToCheck.getCause(); } // "read-only not supported" SQLException -> ignore, it's just a hint anyway logger.debug("Could not set JDBC Connection read-only", ex); } } // Apply specific isolation level, if any. // Set the isolation level of the database connection Integer previousIsolationLevel = null; if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { if (logger.isDebugEnabled()) { logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + definition.getIsolationLevel()); } int currentIsolation = con.getTransactionIsolation(); if (currentIsolation != definition.getIsolationLevel()) { previousIsolationLevel = currentIsolation; con.setTransactionIsolation(definition.getIsolationLevel()); } } return previousIsolationLevel; }
- Record transaction information in the current thread
// AbstractPlatformTransactionManager.java protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) { if (status.isNewSynchronization()) { TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction()); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel( definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? definition.getIsolationLevel() : null); TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); TransactionSynchronizationManager.setCurrentTransactionName(definition.getName()); TransactionSynchronizationManager.initSynchronization(); } }
3.1.2 processing existing transactions
Spring supports a variety of transaction propagation methods, such as PROPAGATION_NESTED,PROPAGATION_REQUIRES_NEW, etc. These are further processed on the basis of existing transactions. How are existing transactions handled? Judge in getTransaction method that if a transaction already exists (judged by isExistingTransaction method), handleExistingTransaction method will be called to process the existing transaction.
private TransactionStatus handleExistingTransaction( TransactionDefinition definition, Object transaction, boolean debugEnabled) throws TransactionException { if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { throw new IllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'"); } // Unsupported transaction propagation type if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { if (debugEnabled) { logger.debug("Suspending current transaction"); } Object suspendedResources = suspend(transaction); boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus( definition, null, false, newSynchronization, debugEnabled, suspendedResources); } // New transaction processing if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]"); } SuspendedResourcesHolder suspendedResources = suspend(transaction); try { boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } catch (RuntimeException | Error beginEx) { resumeAfterBeginException(transaction, suspendedResources, beginEx); throw beginEx; } } // Embedded transaction if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { if (!isNestedTransactionAllowed()) { throw new NestedTransactionNotSupportedException( "Transaction manager does not allow nested transactions by default - " + "specify 'nestedTransactionAllowed' property with value 'true'"); } if (debugEnabled) { logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); } if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction, // through the SavepointManager API implemented by TransactionStatus. // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. // If the transaction rollback is not controlled by using the savepoint method, the savepoint is established at the beginning of the establishment of the embedded transaction DefaultTransactionStatus status = prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); return status; } else { // Nested transaction through nested begin and commit/rollback calls. // Usually only for JTA: Spring synchronization might get activated here // in case of a pre-existing JTA transaction. // In some cases, if you cannot use savepoint operations, such as JTA, create a new transaction boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, null); doBegin(transaction, definition); prepareSynchronization(status, definition); return status; } } // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. if (debugEnabled) { logger.debug("Participating in existing transaction"); } if (isValidateExistingTransaction()) { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) { Constants isoConstants = DefaultTransactionDefinition.constants; throw new IllegalTransactionStateException("Participating transaction with definition [" + definition + "] specifies isolation level which is incompatible with existing transaction: " + (currentIsolationLevel != null ? isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) : "(unknown)")); } } if (!definition.isReadOnly()) { if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { throw new IllegalTransactionStateException("Participating transaction with definition [" + definition + "] is not marked as read-only but existing transaction is"); } } } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); }
The above method considers two situations for existing transactions:
- PROPAGATION_REQUIRES_NEW means that the current transaction must run in its own transaction, and a new transaction will be started. If a transaction is allowed, it will be suspended during the allowed operation of this method. The difference between the processing of this propagation method and the establishment of a new transaction in Spring is that the suspend method is used to suspend the original transaction and restore the original transaction after the current transaction is processed.
- PROPAGATION_NESTED means that if a transaction is currently running, the method should run in a nested transaction. The nested transaction can be committed or rolled back independently of the encapsulated transaction. If the encapsulated transaction does not exist, the behavior is like deployment_ REQUIRES_ NEW . Embedded transactions mainly consider two ways.
- When embedded transactions are allowed in Spring, it is preferred to set the savepoint as the rollback of exception handling.
- For other methods, such as JTA cannot use savepoints, the processing method is the same as that of promotion_ REQUIRES_ New is the same. Once an exception occurs, the transaction exception handling mechanism of Spring will complete the subsequent operations.
For the pending operation, the status of the original transaction is mainly recorded to facilitate the recovery of the transaction by subsequent operations.
protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { if (TransactionSynchronizationManager.isSynchronizationActive()) { List<TransactionSynchronization> suspendedSynchronizations = doSuspendSynchronization(); try { Object suspendedResources = null; if (transaction != null) { suspendedResources = doSuspend(transaction); } String name = TransactionSynchronizationManager.getCurrentTransactionName(); TransactionSynchronizationManager.setCurrentTransactionName(null); boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); TransactionSynchronizationManager.setActualTransactionActive(false); return new SuspendedResourcesHolder( suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); } catch (RuntimeException | Error ex) { // doSuspend failed - original transaction is still active... doResumeSynchronization(suspendedSynchronizations); throw ex; } } else if (transaction != null) { // Transaction active but no synchronization active. Object suspendedResources = doSuspend(transaction); return new SuspendedResourcesHolder(suspendedResources); } else { // Neither transaction nor synchronization active. return null; } }
3.1.3 prepare transaction information
After the transaction connection has been established and the extraction of transaction information has been completed, all transaction information needs to be uniformly recorded in the TransactionInfo instance, which contains all the state information before the start of the target method. Once the transaction fails, Spring will roll back and other subsequent operations through the information in the TransactionInfo instance.
// TransactionAspectSupport.java protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, String joinpointIdentification, @Nullable TransactionStatus status) { TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification); if (txAttr != null) { // We need a transaction for this method... if (logger.isTraceEnabled()) { logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]"); } // The transaction manager will flag an error if an incompatible tx already exists. // Record transaction status txInfo.newTransactionStatus(status); } else { // The TransactionInfo.hasTransaction() method will return false. We created it only // to preserve the integrity of the ThreadLocal stack maintained in this class. if (logger.isTraceEnabled()) { logger.trace("No need to create transaction for [" + joinpointIdentification + "]: This method is not transactional."); } } // We always bind the TransactionInfo to the thread, even if we didn't create // a new transaction here. This guarantees that the TransactionInfo stack // will be managed correctly even if no transaction was created by this aspect. txInfo.bindToThread(); return txInfo; }
3.2 rollback processing
When an error occurs, Spring will roll back.
// TransactionAspectSupport.java protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { // When throwing an exception, first judge whether there is a transaction if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "] after exception: " + ex); } // The default basis for judging whether to roll back is whether the thrown exception is a RuntimeException or an Error type // transactionAttribute is RuleBasedTransactionAttribute if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { try { // Rollback according to transaction information txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by rollback exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by rollback exception", ex); throw ex2; } } else { // We don't roll back on this exception. // Will still roll back if TransactionStatus.isRollbackOnly() is true. // When the rollback conditions are not met, the exception thrown in time will also be submitted try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } catch (TransactionSystemException ex2) { logger.error("Application exception overridden by commit exception", ex); ex2.initApplicationException(ex); throw ex2; } catch (RuntimeException | Error ex2) { logger.error("Application exception overridden by commit exception", ex); throw ex2; } } } }
For the execution target method, once Throwables appear, they will be guided to this method for processing. At that time, it does not mean that all Throwables will be rolled back. By default, only RuntimeException and Error are processed. The key judgment is txinfo transactionAttribute. rollbackOn(ex).
1) Rollback condition
// DefaultTransactionAttribute.java public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }
By default, the transaction exception handling mechanism in Spring only handles RuntimeException and Error. It can be processed through the extension class, but the most commonly used is to use the attribute settings provided by the transaction, using the annotation method, for example:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
2) Rollback processing
Once the rollback conditions are met, Spring will boot the program into the rollback function.
// AbstractPlatformTransactionManager.java public final void rollback(TransactionStatus status) throws TransactionException { // If the transaction has completed, rolling back again will throw an exception if (status.isCompleted()) { throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction"); } DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; processRollback(defStatus, false); } private void processRollback(DefaultTransactionStatus status, boolean unexpected) { try { boolean unexpectedRollback = unexpected; try { // Activate the corresponding methods in all TransactionSynchronization triggerBeforeCompletion(status); if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Rolling back transaction to savepoint"); } // If there is a savepoint, that is, the current transaction is a separate thread, it will retreat to the savepoint status.rollbackToHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction rollback"); } // If it is a new transaction, roll it back directly doRollback(status); } else { // Participating in larger transaction if (status.hasTransaction()) { if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { if (status.isDebug()) { logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); } // If the current transaction is not an independent transaction, you can only mark the status and roll back after the transaction is executed doSetRollbackOnly(status); } else { if (status.isDebug()) { logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); } } } else { logger.debug("Should roll back transaction but cannot - no transaction available"); } // Unexpected rollback only matters here if we're asked to fail early if (!isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback = false; } } } catch (RuntimeException | Error ex) { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw ex; } // Activate the corresponding methods in all TransactionSynchronization triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); // Raise UnexpectedRollbackException if we had a global rollback-only marker if (unexpectedRollback) { throw new UnexpectedRollbackException( "Transaction rolled back because it has been marked as rollback-only"); } } finally { // Clear recorded resources and restore suspended resources cleanupAfterCompletion(status); } }
The main logic of the above method:
- The calling of custom triggers, including those before and after rollback, is registered through the TransactionSynchronizationManager#registerSynchronization method.
public static void registerSynchronization(TransactionSynchronization synchronization)
- Processing of rollback logic
- When there is savepoint information in the previously saved transaction information, the savepoint information is used for rollback. It is commonly used in embedded transactions. For the processing of embedded transactions, embedded transaction exceptions will not cause the rollback of external transactions. The implementation of rollback based on savepoint is actually based on the underlying database connection.
// AbstractTransactionStatus.java public void rollbackToHeldSavepoint() throws TransactionException { Object savepoint = getSavepoint(); if (savepoint == null) { throw new TransactionUsageException( "Cannot roll back to savepoint - no savepoint associated with current transaction"); } // Rollback to savepoint getSavepointManager().rollbackToSavepoint(savepoint); getSavepointManager().releaseSavepoint(savepoint); setSavepoint(null); }
The JDBC database connection is used here, and the getSavepointManager method returns JdbcTransactionObjectSupport, that is, the above method will call the JdbcTransactionObjectSupport#rollbackToSavepoint method.
// JdbcTransactionObjectSupport.java public void rollbackToSavepoint(Object savepoint) throws TransactionException { ConnectionHolder conHolder = getConnectionHolderForSavepoint(); try { // Use Jdbc to rollback conHolder.getConnection().rollback((Savepoint) savepoint); conHolder.resetRollbackOnly(); } catch (Throwable ex) { throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); } }
- When the transaction in the previously saved transaction information is a new transaction, it is rolled back directly. It is often used for the processing of individual transactions. For rollback without savepoint, Spring also uses the API provided by the underlying database connection to operate. Use the DataSourceTransactionManager#doRollback method.
protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); } try { // Jdbc con.rollback(); } catch (SQLException ex) { throw new TransactionSystemException("Could not roll back JDBC transaction", ex); } }
- The current transaction information indicates that there is a transaction, but it does not belong to the above two situations. Generally, it is JTA, which only makes the rollback identification, and does not commit when it is submitted.
- Information clearing after rollback
After the rollback logic is executed, the cleanup operation after the end of the transaction will be performed regardless of whether the rollback is successful or not.
private void cleanupAfterCompletion(DefaultTransactionStatus status) { // Set completion status status.setCompleted(); // If it is a new synchronization state, you need to clear the transaction information bound to the current thread if (status.isNewSynchronization()) { TransactionSynchronizationManager.clear(); } // If it is a new transaction, the resource needs to be cleared if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); } if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction after completion of inner transaction"); } Object transaction = (status.hasTransaction() ? status.getTransaction() : null); // Pending state of transaction before end resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); } }
The main logic of the above method:
- Set completion status to avoid repeated calls
- If it is a new synchronization state, you need to clear the transaction information bound to the current thread
- If it is a new transaction, some resources need to be cleared
// DataSourceTransactionManager.java protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // Remove the connection holder from the thread, if exposed. if (txObject.isNewConnectionHolder()) { // Unbind the database connection from the current thread TransactionSynchronizationManager.unbindResource(obtainDataSource()); } // Reset connection. // Release link Connection con = txObject.getConnectionHolder().getConnection(); try { if (txObject.isMustRestoreAutoCommit()) { // Restore database auto commit properties con.setAutoCommit(true); } // Reset database connection DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); } catch (Throwable ex) { logger.debug("Could not reset JDBC Connection after transaction", ex); } if (txObject.isNewConnectionHolder()) { if (logger.isDebugEnabled()) { logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); } // If the current transaction is an independent new transaction, release the database connection when the transaction completes DataSourceUtils.releaseConnection(con, this.dataSource); } txObject.getConnectionHolder().clear(); }
- If a transaction is suspended before the transaction is executed, the suspended transaction needs to be resumed after the current transaction ends
// AbstractPlatformTransactionManager.java protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) throws TransactionException { if (resourcesHolder != null) { Object suspendedResources = resourcesHolder.suspendedResources; if (suspendedResources != null) { doResume(transaction, suspendedResources); } List<TransactionSynchronization> suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; if (suspendedSynchronizations != null) { TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); doResumeSynchronization(suspendedSynchronizations); } } } // DataSourceTransactionManager.java @Override protected void doResume(@Nullable Object transaction, Object suspendedResources) { TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); }
3.3 transaction submission
When there is no exception in transaction execution, the transaction can be committed.
// TransactionAspectSupport.java protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); } // DataSourceTransactionManager txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } }
Before the transaction is actually committed, you need to make a judgment. During exception transaction processing, when a transaction has no savepoint and is not a new transaction, Spring will set a rollback ID. Main application scenario: if a transaction is another embedded transaction, but these transactions are not under the management of Spring, or the savepoint cannot be set, the commit will be prohibited by setting the rollback ID. When an external transaction is committed, it will judge whether the current transaction flow has a rollback ID set, and the external transaction will roll back the overall transaction uniformly. Therefore, when the transaction is not caught by an exception, it does not mean that the commit will be executed.
// AbstractPlatformTransactionManager.java public final void commit(TransactionStatus status) throws TransactionException { if (status.isCompleted()) { throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction"); } DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; // The rollback ID has been identified in the current transaction chain. Roll back directly if (defStatus.isLocalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Transactional code has requested rollback"); } processRollback(defStatus, false); return; } if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); } processRollback(defStatus, true); return; } // Submit processCommit(defStatus); }
When the transaction is running normally.
// AbstractPlatformTransactionManager.java private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { boolean unexpectedRollback = false; // reserve prepareForCommit(status); // Call to method in TransactionSynchronization triggerBeforeCommit(status); // Call to method in TransactionSynchronization triggerBeforeCompletion(status); beforeCompletionInvoked = true; if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Releasing transaction savepoint"); } unexpectedRollback = status.isGlobalRollbackOnly(); // Clear if saved by savepoint information status.releaseHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } unexpectedRollback = status.isGlobalRollbackOnly(); // Independent new transactions are directly submitted to DataSourceTransactionManager doCommit(status); } else if (isFailEarlyOnGlobalRollbackOnly()) { unexpectedRollback = status.isGlobalRollbackOnly(); } // Throw UnexpectedRollbackException if we have a global rollback-only // marker but still didn't get a corresponding exception from commit. if (unexpectedRollback) { throw new UnexpectedRollbackException( "Transaction silently rolled back because it has been marked as rollback-only"); } } catch (UnexpectedRollbackException ex) { // can only be caused by doCommit // Call to method in TransactionSynchronization triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); throw ex; } catch (TransactionException ex) { // can only be caused by doCommit if (isRollbackOnCommitFailure()) { // Abnormal rollback occurred doRollbackOnCommitException(status, ex); } else { // Call to method in TransactionSynchronization triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); } throw ex; } catch (RuntimeException | Error ex) { if (!beforeCompletionInvoked) { triggerBeforeCompletion(status); } doRollbackOnCommitException(status, ex); throw ex; } // Trigger afterCommit callbacks, with an exception thrown there // propagated to callers but the transaction still considered as committed. try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } }
Will be considered in the submission.
- When there is savepoint information in the transaction state, the transaction will not be committed
- When the transaction is not new, the transaction will not be committed
This condition mainly considers the situation of embedded transactions. For embedded transactions, savepoints will be set before the start of embedded transactions in Spring processing, and exceptions will be rolled back according to the savepoints. However, when there are no exceptions, the embedded transactions will not be submitted separately, and the outermost transaction is responsible for submitting according to the transaction flow. Therefore, the savepoint timestamp indicates that it is not the outermost transaction, Therefore, only the savepoint information is cleared, and the judgment of whether it is a new transaction is also based on this consideration.
Finally, it will be submitted by the database connection API.
// DataSourceTransactionManager.java protected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); Connection con = txObject.getConnectionHolder().getConnection(); if (status.isDebug()) { logger.debug("Committing JDBC transaction on Connection [" + con + "]"); } try { // Submit con.commit(); } catch (SQLException ex) { throw new TransactionSystemException("Could not commit JDBC transaction", ex); } }