Spring boot transaction execution whole process analysis
In the last article Source code analysis of spring boot transaction creation process In, we mainly talked about the process of creating springboot transactions. This time, let's take a look at the specific execution process of transactions.
Here, several names are agreed in advance:
-
com.springboot.transaction.service.impl.UserServiceImpl this class is called the original class, and its object is called the original object
-
The subclass of com.springboot.transaction.service.impl.UserServiceImpl generated by springboot through aop is called proxy class, and its object is called proxy object
In the main method, there will be this sentence System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,p); p here is the local storage location of the set springboot dynamic generation code. In the generation agent class, we can find the class files of all corresponding dynamic agent classes here. We can also learn more about it through decompilation
In order to avoid the interference of other frameworks on transaction execution, the database in this article uses the native JDBC template. Other frameworks, such as mybatis, will also insert their own framework code during transactions.
The source code path of this blog: https://github.com/wbo112/blogdemo/tree/main/springbootdemo/springboot-jdbc-transaction
1. Preparation before implementation of transaction method
After the service starts, we enter in the browser http://localhost:8080/addUser To request backstage. The addUsers method of com.springboot.transaction.jdbc.controller.UserController will be requested, and the specific implementation will be returned to userService. Adduser(); In this code, the userService here is the proxy class mentioned in the previous article, which will call the addUser method in the proxy class.
In the last article, we also posted the decompiled userService proxy class, which will be transferred to the following code
//Methods in proxy classes public final boolean addUser() { //This. Cglib $callback here_ The 0 is the assignment of the setCallbacks method after the final proxy class is mentioned in the previous article. //This. Cglib $callback here_ 0 is cglibaopproxy $dynamicadisposedinterceptor MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if (var10000 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; } //CGLIB$addUserCglib $addUser $0 $method $Method is the method of the original class public boolean com.springboot.transaction.jdbc.service.impl.UserServiceImpl.addUser() //CGLIB$addUserCglib $addUser $0 $proxy $Proxy is the object of MethodProxy class, and the final call is completed through MethodProxy.invoke if (var10000 != null) { Object var1 = var10000.intercept(this, CGLIB$addUser$0$Method, CGLIB$emptyArgs, CGLIB$addUser$0$Proxy); return var1 == null ? false : (Boolean)var1; } else { return super.addUser(); } }
The above code will call the intercept method of cglibaopproxy $dynamicadisadvised interceptor. Let's continue to look at this
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; TargetSource targetSource = this.advised.getTargetSource(); try { if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool... //Here we will get our original object and original class target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); //Here we will get the list of interceptors that need to perform interception. We only have an object of org.springframework.transaction.interceptor.TransactionInterceptor, which was also mentioned in the previous article and loaded as a bean object //this.advised here is the object of org.springframework.aop.framework.ProxyFactory. This class has been described in the previous article, so it will not be mentioned here //Let's go to this method first to see how the interceptor list is obtained List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; // Check whether we only have one InvokerInterceptor: that is, // no real advice, but just reflective invocation of the target. if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) { // We can skip creating a MethodInvocation: just invoke the target directly. // Note that the final invoker must be an InvokerInterceptor, so we know // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = methodProxy.invoke(target, argsToUse); } else { // We need to create a method invocation... //Here, we will generate the CglibMethodInvocation object and complete the processing of the whole transaction process by calling process. retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } //Here, we will make a simple judgment on the returned result, because the original type cannot be null. Here, we will judge whether the return type of the method is the original type and whether the current return value is null. If both are null, an exception will be thrown retVal = processReturnType(proxy, target, method, retVal); return retVal; } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } }
Let's take a look at this. Advised. Getinterceptors and dynamic interception advice (method, targetclass); The specific implementation of this code
//This method is in the AdvisedSupport class public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) { //The interceptor list of the method will be cached here //At present, there is no in the cache, so it will go to the if branch MethodCacheKey cacheKey = new MethodCacheKey(method); List<Object> cached = this.methodCache.get(cacheKey); if (cached == null) { //Let's go into this branch //this.advisorChainFactory here is a member variable, which is initialized at the time of declaration. After that, it does not call the set method to re assign values // advisorChainFactory = new DefaultAdvisorChainFactory(); cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice( this, method, targetClass); //After the corresponding interceptor list is obtained, it is added to the cache and can be obtained directly next time this.methodCache.put(cacheKey, cached); } return cached; }
Take a look at the execution process of this. Advisor chainfactory. Getinterceptors and dynamic interception advice (this, method, targetclass)
//This method is defaultadvisor chainfactory @Override public List<Object> getInterceptorsAndDynamicInterceptionAdvice( Advised config, Method method, @Nullable Class<?> targetClass) { // This is somewhat tricky... We have to process introductions first, // but we need to preserve order in the ultimate list. AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); //There is only one object in the advisors array, BeanFactoryTransactionAttributeSourceAdvisor. This class was mentioned in the previous article, and it will not be mentioned here Advisor[] advisors = config.getAdvisors(); List<Object> interceptorList = new ArrayList<>(advisors.length); Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); Boolean hasIntroductions = null; for (Advisor advisor : advisors) { if (advisor instanceof PointcutAdvisor) { // Add it conditionally. PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { ......//Here is to judge whether the method matches with the advisors, which is basically the same as the matching steps in the previous article, which will be omitted here if (match) { //If it can match, it will get the interceptor list of the method from the advisor, add it to the list, and finally return //getInterceptors is also a relatively simple method, that is, to obtain interceptors directly through advisor.getAdvice() //However, there are some other knowledge points in this method, so we'd better go in and have a look MethodInterceptor[] interceptors = registry.getInterceptors(advisor); //The following code can't go, so I'll skip it directly ...... return interceptorList; }
//Methods in defaultadvisor AdapterRegistry @Override public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { List<MethodInterceptor> interceptors = new ArrayList<>(3); //The advice here is the transaction interceptor, which was also mentioned in the last article //Advice corresponds to our Chinese term, which seems to be called notice. It feels awkward 😂 Advice advice = advisor.getAdvice();  //TransactionInterceptor implements MethodInterceptor, so it will go to this if branch if (advice instanceof MethodInterceptor) { interceptors.add((MethodInterceptor) advice); } //The adapters here have three objects: new MethodBeoreAdviceAdapter(), new AfterReturningAdviceAdapter(), and new ThrowsAdviceAdapter() //It can also be seen here that aop interception (Advice notification) is mainly divided into four types //MethodInterceptor is a surround notification, that is, it will be intercepted before and after method execution //MethodBeforeAdviceAdapter pre notification is to intercept first and then execute the original method; This mainly deals with the parameters executed by the original method //After returning advice adapter post notification is to execute the original method first and then intercept; This mainly deals with the execution results of the original method //ThrowsAdviceAdapter is used to handle exceptions thrown by the original method execution //These four intercepts have no inheritance relationship, so they will not enter the for if branch for (AdvisorAdapter adapter : this.adapters) { if (adapter.supportsAdvice(advice)) { interceptors.add(adapter.getInterceptor(advisor)); } } if (interceptors.isEmpty()) { throw new UnknownAdviceTypeException(advisor.getAdvice()); } return interceptors.toArray(new MethodInterceptor[0]); }
Next, let's go back to the intercept method of CglibAopProxy class again and see retval = new CglibMethodInvocation (proxy, target, method, args, targetclass, chain, methodproxy). Processed(); First, we will create the CglibMethodInvocation object. We won't look at this. It's a simple assignment operation. Let's focus on the execution of the processed method
//proceed will be directly through super.proceed(); Call the method of the parent class. Let's look directly at the method of its parent class public Object proceed() throws Throwable { // We start with an index of -1 and increment early. //Initialize currentInterceptorIndex=-1; this.interceptorsAndDynamicMethodMatchers.size()=1 //Therefore, interceptors will be obtained from interceptors anddynamicmethodmatchers in turn for execution. After all interceptors are executed, invokejoinpoints will be executed if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } //Acquire interceptors in turn //We have only one TransactionInterceptor in our interceptor list Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); //I won't go to this branch if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { ....... } else { // It's an interceptor, so we just invoke it: The pointcut will have // been evaluated statically before this object was constructed. //You will go here to the TransactionInterceptor.invoke method return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); } }
@Override @Nullable 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. //You'll get the original class here Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); // Adapt to TransactionAspectSupport's invokeWithinTransaction... //You can see from the name of this method, "call within a transaction" //The invocation here is the above CglibMethodInvocation. You can see that the proceedWithInvocation method will be transferred to the proceedmethod again, which is the recursive call we mentioned above //Let's continue to look at invokeWithinTransaction. The invokeWithinTransaction method is in its parent class TransactionAspectSupport return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() { @Override @Nullable public Object proceedWithInvocation() throws Throwable { return invocation.proceed(); } @Override public Object getTarget() { return invocation.getThis(); } @Override public Object[] getArguments() { return invocation.getArguments(); } }); }
invokeWithinTransaction has a lot of contents. It can be said that the internal transaction processing is completed in this method
//This method is in TransactionAspectSupport protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. //This attribute is assigned by calling the set method when creating the TransactionInterceptor bean, as mentioned in the previous article TransactionAttributeSource tas = getTransactionAttributeSource(); //This is the attribute information of the transaction of the corresponding method, which has been obtained when the transaction is created, so it can be obtained directly from the cache. This was also mentioned in the last article final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); //This is obtained from the transaction manager. It is not available for the first time. It will be obtained from beanfactory and added to the cache. The object of JdbcTransactionManager obtained here //Specifically, it is created in DataSourceTransactionManagerAutoConfiguration. The specific creation process is basically the same as that of other bean s mentioned in the previous article final TransactionManager tm = determineTransactionManager(txAttr); //If the following branching conditions are not tenable and will not enter, we will not see it, and this part of the code will be directly omitted if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) { ...... } //This is mainly the judgment type. If it is the PlatformTransactionManager type, it is forced to turn; If not, the bubble is abnormal. //JdbcTransactionManager indirectly implements platform transactionmanager, which can be forcibly transferred here PlatformTransactionManager ptm = asPlatformTransactionManager(tm); //This is the identification of the connection point: specifically, the full class name +. + method name is spliced into a string. //Our transaction annotations are all for methods. This is mainly used to code the name of the transaction attribute of the corresponding method if the transaction name in the corresponding transaction attribute is empty. Different transactions can be distinguished through this //This is com.springboot.transaction.service.impl.UserServiceImpl.findAll final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); //The following conditions are true and will go into the if branch if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. //From the name of this method, you can see that transactions are created according to needs. Which scenarios need to create transactions, and which scenarios do not? //For those who do not understand the concept of springboot transaction, you can take a look at some brief introductions about the concept of springboot transaction written earlier https://www.cnblogs.com/wbo112/p/15427078.html //Let's take a look at this method TransactionInfo txInfo = createTransactionIfNecessary(ptm, 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. //After the TransactionInfo is created, proceedWithInvocation will be called here, continue to be returned to proceed recursively, and finally transferred to invokeJoinpoint method, where it will eventually be called to this.methodProxy.invoke(this.target, this.arguments). Yes. We will analyze the method proxy at the end of the article //Our @ Transactional annotated methods will eventually be executed here retVal = invocation.proceedWithInvocation(); //The following codes are executed according to the method annotated by @ Transactional to determine whether to throw an exception and whether to commit or rollback the subsequent transaction. We will talk about this part of the code separately later
//Method of TransactionAspectSupport protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { // If no name specified, apply method identification as transaction name. //By default, the transaction here has no name. The transaction attribute will be wrapped here, and the joinpointIdentification generated in the previous step will be used as the name of the transaction 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) { //Here, the transaction will be created specifically. This method is in the abstractplatform transaction manager. Let's go in and have a look status = tm.getTransaction(txAttr); } else { if (logger.isDebugEnabled()) { logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured"); } } } //Here, a TransactionInfo object will be created and bound to the current thread through ThreadLocal return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); }
//Method of AbstractPlatformTransactionManager @Override public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { // Use defaults if no transaction definition given. //The definition here has been wrapped into a subclass of DelegatingTransactionAttribute through anonymous inner classes in the previous step TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); //Here, a transaction object of datasource will be created and the database connection of the current context will be obtained. If there is no connection, it will be null. Let's go into this method and have a look Object transaction = doGetTransaction(); boolean debugEnabled = logger.isDebugEnabled(); //Here, you will judge whether the ConnectionHolder is null. If the ConnectionHolder is not null, you will also judge whether the transaction has been started. If the transaction has been started, subsequent execution will be carried out in the started transaction //The ConnectionHolder here is null and will not enter this branch //Note: the ConnectionHolder here distinguishes different datasources. Only the current ConnectionHolder of the same DataSource can go here if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(def, transaction, debugEnabled); } // Check definition settings for new transaction. //Here is the timeout time to judge the transaction. The default is - 1, which means no timeout. If < - 1, throw an exception if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout()); } //Later, it is processed according to the propagation behavior of the transaction. This time, we are the default propagation behavior, that is, Propagation.REQUIRED; So it will go to the else if branch later // No existing transaction found -> check propagation behavior to find out how to proceed. if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { //For the same DataSource, if there is currently an open transaction, it will go in from the isExistingTransaction above and will not go here. Therefore, this is for different datasources. We currently have only one DataSource. There will be no execution in it, so we won't go in SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def); } try { //We'll start business here. Let's go in and have a look return startTransaction(def, transaction, debugEnabled, suspendedResources); } catch (RuntimeException | Error ex) { resume(null, suspendedResources); throw ex; } } else { // Create "empty" transaction: no actual transaction, but potentially synchronization. if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { logger.warn("Custom isolation level specified but no actual transaction initiated; " + "isolation level will effectively be ignored: " + def); } boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null); } }
//Methods in DataSourceTransactionManager @Override protected Object doGetTransaction() { //First, create a DataSourceTransactionObject. This class mainly holds a connection for the class DataSourceTransactionObject txObject = new DataSourceTransactionObject(); //Sets whether nesting is allowed txObject.setSavepointAllowed(isNestedTransactionAllowed()); //What obtainDataSource() obtains is the underlying DataSource. Don't pay attention to what it is; This is passed in as a bean parameter when creating the JdbcTransactionManager. The default database connection pool in springboot is Hikari, so this DataSource is HikariDataSource by default //Let's use TransactionSynchronizationManager.getResource to see how to get the ConnectionHolder //The conHolder obtained here is null ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); //The obtained connection will be assigned to the DataSourceTransactionObject //The second parameter indicates whether it is a new connection. We only get the previous one here (we only get it here, regardless of whether it is null) and do not create a new one, so it is false here txObject.setConnectionHolder(conHolder, false); return txObject; }
//Method of TransactionSynchronizationManager @Nullable public static Object getResource(Object key) { //Here, first, if the passed in key is packaged, remove the packaging; We don't have packaging here, so the actual key is the key Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); //Here, the connection of the current thread will be obtained through ThreadLocal. Here is a map and the key is the datasource. Therefore, there can be multiple datasources at the same time without conflict. //This is the first time we come in. The map is also null, and the value here is null. Object value = doGetResource(actualKey); if (value != null && logger.isTraceEnabled()) { logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); } //null is returned here return value; }
//Method of AbstractPlatformTransactionManager private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) { //First, judge the current transaction propagation attribute, SYNCHRONIZATION_NEVER needs to start a transaction boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); //Here is to create a DefaultTransactionStatus object DefaultTransactionStatus status = newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); //Here, you will judge whether automatic submission is enabled. If automatic submission is enabled, it will be changed to manual submission, which requires manual submission after all SQL is executed //Let's go in and have a look doBegin(transaction, definition); //Here, the properties will be set in the transaction synchronization manager to prepare for the start of the transaction prepareSynchronization(status, definition); return status; }
//Method of DataSourceTransactionManager protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; Connection con = null; try { //First, it will judge whether the ConnectionHolder is null. If not, it will continue to judge whether it is already in the transaction //The ConnectionHolder here is null and will enter the if branch if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { //Here is a Connection to get the database directly through the DataSource Connection newCon = obtainDataSource().getConnection(); if (logger.isDebugEnabled()) { logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); } //Here, the Connection will be wrapped as a ConnectionHolder and set to the DataSourceTransactionObject //Meanwhile, here is the newly created ConnectionHolder, and the second parameter is true txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } //Start transaction is set here txObject.getConnectionHolder().setSynchronizedWithTransaction(true); con = txObject.getConnectionHolder().getConnection(); //Here is the isolation level of the database //If the isolation level defined in the transaction attribute is the default, it means that we use the current setting of the data. At this time, we don't need to modify it. null is returned here //If what we defined in the transaction attribute is not the default, first obtain the current isolation level of the database, set the isolation level of the database to what we defined, and return to the previous isolation level of the database Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); txObject.setReadOnly(definition.isReadOnly()); // 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). //The above notes basically explain that manual submission is enabled here if (con.getAutoCommit()) { //This means that we want to revert to auto commit after executing the transaction txObject.setMustRestoreAutoCommit(true); if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } //Through this sentence, you can set manual submission con.setAutoCommit(false); } //If we define a read-only transaction, the read-only transaction setting will be executed here prepareTransactionalConnection(con, definition); //Set transaction on flag txObject.getConnectionHolder().setTransactionActive(true); //Get the timeout time. If it is not - 1, it indicates that the timeout time needs to be set. The illegal timeout has been judged earlier int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } //Here, you can judge how to create a newly created ConnectionHolder and bind it to the DataSource through ThreadLocal. Subsequently, if it is in the same transaction (the same transaction is only in the same DataSource and the same connection), it can be obtained directly // Bind the connection holder to the thread. if (txObject.isNewConnectionHolder()) { 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); } }
2. Calling of business code
1. Creation of methodproxy object
In the above source code analysis, we can see that this.methodProxy.invoke(this.target, this.arguments) is finally called by calling the invokeJoinpoint method. Let's see how the methodProxy is created here
It is created in the CGLIB$STATICHOOK9() method of our agent, and the relevant code is as follows
Class var0 = Class.forName("com.springboot.transaction.jdbc.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$3f0b65e9"); Class var1 = Class.forName("com.springboot.transaction.jdbc.service.impl.UserServiceImpl")ï¼› //Here you can see that it will be created by calling the static method of MethodProxy //The first two parameters are the original class and the proxy class. The third parameter is the signature of the method corresponding to our transaction data // The fourth parameter is the corresponding method name in the original class, and the fifth parameter is the corresponding method name in the generated proxy class //Now let's go in and look at the calling process inside it CGLIB$addUser$0$Proxy = MethodProxy.create(var1, var0, "()Z", "addUser", "CGLIB$addUser$0");
//Methods in MethodProxy public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) { MethodProxy proxy = new MethodProxy(); proxy.sig1 = new Signature(name1, desc); proxy.sig2 = new Signature(name2, desc); //The first few lines are relatively simple, creating objects and assigning attributes. Let's look at the following line. It will create a createInfo object and assign it to the corresponding property of methodproxy proxy.createInfo = new CreateInfo(c1, c2); return proxy; }
public CreateInfo(Class c1, Class c2) { //Here c1=om.springboot.transaction.service.impl.UserServiceImpl.class //c2=com.springboot.transaction.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$f0a9d143.class //c2 is our newly generated proxy class this.c1 = c1; this.c2 = c2; //AbstractClassGenerator will be obtained through ThreadLocal below, mainly to obtain the following three properties to ensure that the proxy class is consistent with the class related settings of the quick execution method to be generated here //Let's see how we got this AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent(); if (fromEnhancer != null) { namingPolicy = fromEnhancer.getNamingPolicy(); strategy = fromEnhancer.getStrategy(); attemptLoad = fromEnhancer.getAttemptLoad(); } }
First of all, the above method call is to call the above method after we finish executing the proxy class that creates the transaction. After loading the proxy class, there will be a static method in our proxy class. After loading the execution class, the static method will be executed
static { CGLIB$STATICHOOK9(); }
The above CGLIB$STATICHOOK9 method will obtain the methods of our original class through the reflected method, and will also generate various methodproxies.
Therefore, the AbstractClassGenerator obtained here by AbstractClassGenerator.getCurrent() is actually the Enhancer object we use when creating the proxy class. You can confirm it by looking at the method call stack
Looking at the above figure, you can see that the construction method of calling CreateInfo at the moment is invoked at the generate of AbstractClassGenerator. At the beginning of generate in abstractclassgenerator, there are the following sentences
protected Class generate(ClassLoaderData data) { Class gen; Object save = CURRENT.get(); //Here, this is assigned to CURRENT, and this is the Enhancer used to create the proxy class CURRENT.set(this); ...... }
- Let's take a look at the execution of this.methodProxy.invoke(this.target, this.arguments)
MethodProxy is mainly used to execute method calls in the original class and proxy class.
There are two main methods:
- invokeSuper method, which calls the method in the proxy class
- invoke method, which calls the method of the original class
//MethodProxy method public Object invoke(Object obj, Object[] args) throws Throwable { try { //In the init method, you will judge whether the fastClassInfo object exists. If it does not exist, you will generate the corresponding object. //Two proxy classes will also be generated in init. Let's take a look at the init method init(); FastClassInfo fci = fastClassInfo; return fci.f1.invoke(fci.i1, obj, args); } catch (InvocationTargetException ex) { throw ex.getTargetException(); } catch (IllegalArgumentException ex) { if (fastClassInfo.i1 < 0) throw new IllegalArgumentException("Protected method: " + sig1); throw ex; } }
//MethodProxy method private void init() { /* * Using a volatile invariant allows us to initialize the FastClass and * method index pairs atomically. * * Double-checked locking is safe with volatile in Java 5. Before 1.5 this * code could allow fastClassInfo to be instantiated more than once, which * appears to be benign. */ if (fastClassInfo == null) { synchronized (initLock) { if (fastClassInfo == null) { CreateInfo ci = createInfo; //First create a FastClassInfo object //FastClassInfo mainly has four attributes, which will be assigned values below FastClassInfo fci = new FastClassInfo(); //The ci here is the createInfo above, //ci.c1 is our original class com.springboot.transaction.service.impl.UserServiceImpl.class //Next, call the helper method respectively to generate the FastClass class that quickly calls the original class and proxy class methods fci.f1 = helper(ci, ci.c1); fci.f2 = helper(ci, ci.c2); //Here, an integer will be returned in FastClass according to the method signature. Finally, when the invoke method is called, this integer will be passed in. In FastClass, the corresponding method will be called quickly through switch case fci.i1 = fci.f1.getIndex(sig1); fci.i2 = fci.f2.getIndex(sig2); fastClassInfo = fci; createInfo = null; } } } }
The helper method will also dynamically generate a class called FastClass. The function of this class is to quickly call the methods of some classes through digital subscripts. It is mainly the method of proxy class.
Personal understanding
For example, the method CGLIB$findAll cglib $findall $0 in our proxy class is dynamically generated.
For another example, although our method is not dynamically generated, the springboot framework does not know our code and cannot directly tune our code.
In the above two cases, the platform needs to call our code methods, so we need to generate calls dynamically according to our methods through FastClass
The following two lines of code will generate FastClass corresponding to our original class and proxy class respectively
//The helper method also creates FastClass dynamically by creating the FastClass.Generator object and calling its create fci.f1 = helper(ci, ci.c1); fci.f2 = helper(ci, ci.c2);
3. FastClass generated by FastClass.generator. Since the structure is consistent, only the FastClass corresponding to the CGLIB$findAll cglib $findall $0 method of the proxy class is displayed here
It mainly corresponds to two methods
- getIndex returns an integer according to the passed in Signature or method name and class name, and the index of the code in invoke
- invoke calls the methods of the proxy class or the original class according to the passed in integer and the parameters of the corresponding original class or proxy class
Dynamically generate the class file according to the System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,p) specified in our main method; The location generates the. Class here. Decompile to see
3. Post processing of transaction method implementation
We have seen that the transaction execution is executed in the invokewintransaction method of the TransactionInterceptor class. Now let's take a look at how the springboot performs transaction submission and subsequent work after our code is executed
//Methods in TransactionAspectSupport try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. //As mentioned earlier, the method of our @ Transactional annotation will be executed here retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception //If you throw an exception, you will go here, roll back and other operations according to the situation, and then continue to throw an exception //Note: Throwable is captured here, which is the top-level interface of all exceptions, so it can certainly be captured completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { //Here, continue to store the information before TransactionInfo into the corresponding ThreadLocal variable //Note: since it is stored in ThreadLocal variable, there will be no concurrency problem cleanupTransactionInfo(txInfo); } //If the transaction fails to execute, an exception will be thrown in the front, so when you go to the following code, the execution must be successful, and you need to commit the transaction later //There will be no io.vavr.control.Try.class here, so we won't go to this branch if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { // Set rollback-only in case of Vavr failure matching our rollback rules... TransactionStatus status = txInfo.getTransactionStatus(); if (status != null && txAttr != null) { retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status); } } //Transaction submission will be carried out here. Let's take a look here commitTransactionAfterReturning(txInfo); return retVal; } else { ...... }
//Methods in TransactionAspectSupport protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { if (txInfo != null && txInfo.getTransactionStatus() != null) { if (logger.isTraceEnabled()) { logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); } //Commit the transaction through the transaction manager. Let's continue to look at the code inside txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); } }
//Methods in AbstractPlatformTransactionManager public final void commit(TransactionStatus status) throws TransactionException { //First, judge whether the current transaction has been completed. The completed flag is set to true after the transaction is completed. Here, it is still false 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; 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; } //Commit the transaction here processCommit(defStatus); } /** * Process an actual commit. * Rollback-only flags have already been checked and applied. * @param status object representing the transaction * @throws TransactionException in case of commit failure */ private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { boolean unexpectedRollback = false; //The following is an empty method, which is left to the subclass to inherit prepareForCommit(status); //Here are some pre submission processing //It mainly obtains all transactionsynchronizations through TransactionSynchronizationManager.getSynchronizations() //The TransactionSynchronization interface serves the third-party framework and implements some internal work in the framework during the transaction submission phase //If the database uses the mybatis framework, SqlSessionUtils$SqlSessionSynchronization will be obtained here. This is mainly used by mybatis and has little to do with the logic of our transaction framework //If our database interaction uses the native JdbcTemplate, it is empty here, so neither triggerBeforeCommit nor triggerBeforeCompletion will perform any operation triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; //If there is a save point before, it needs to be released here, because we will submit the whole operation later //We didn't set a save point and won't come here if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Releasing transaction savepoint"); } unexpectedRollback = status.isGlobalRollbackOnly(); status.releaseHeldSavepoint(); } //We are starting a new business here and will come here else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } //For SmartTransactionObject, there may be rollback. We are not SmartTransactionObject here, and the return value here is false unexpectedRollback = status.isGlobalRollbackOnly(); //The real submission is implemented here. It's relatively simple, so I don't go in and see it. The transaction is manually committed by calling the commit method of the connection of the underlying jdbc 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 triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); throw ex; } catch (TransactionException ex) { // can only be caused by doCommit if (isRollbackOnCommitFailure()) { doRollbackOnCommitException(status, ex); } else { 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 { //This is also an empty operation triggerAfterCommit(status); } finally { //This is also an empty operation triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { //Some cleaning work will be done here. The resources previously set by the threadlocal variable will be modified back to the original, and the connection will be returned to the connection pool cleanupAfterCompletion(status); } }
4. The difference between transaction code and non transaction code
The above is about the transaction processing of spring boot. For our business code, there are some differences in whether to use transactions or not.
- The main differences are:
- If it is executed in a transaction, the actual execution database connection we obtained is the one obtained when the transaction was created, that is, the one that turns off automatic submission and changes to manual submission.
- If it is not executed in a transaction, it will be obtained directly from the database connection pool
-
Let's look at this part of the key code
We perform database operations to obtain database connections through this code = DataSourceUtils.getConnection(obtainDataSource());
So we just need to look at the internal logic of this code to understand
public static Connection doGetConnection(DataSource dataSource) throws SQLException { Assert.notNull(dataSource, "No DataSource specified"); //If the transaction is started before, the ConnectionHolder here will not be empty by default. The following if condition is also true, and the connection to the database will be obtained from here ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } // Else we either got no holder or an empty thread-bound holder here. logger.debug("Fetching JDBC Connection from DataSource"); //If it is not in the transaction or null is obtained from the transaction, a new one will be obtained from the database connection pool Connection con = fetchConnection(dataSource); //It will be judged here that if you can go to this if branch in the current transaction, it means that there is no connection in the current transaction, then the connection method obtained this time will be transferred to the transaction manager. In the same transaction, you will perform multiple database operations, only once at most, and the subsequent database operations will be returned from the above if branch if (TransactionSynchronizationManager.isSynchronizationActive()) { try { // Use same Connection for further JDBC actions within the transaction. // Thread-bound object will get removed by synchronization at transaction completion. ConnectionHolder holderToUse = conHolder; if (holderToUse == null) { holderToUse = new ConnectionHolder(con); } else { holderToUse.setConnection(con); } holderToUse.requested(); TransactionSynchronizationManager.registerSynchronization( new ConnectionSynchronization(holderToUse, dataSource)); holderToUse.setSynchronizedWithTransaction(true); if (holderToUse != conHolder) { TransactionSynchronizationManager.bindResource(dataSource, holderToUse); } } catch (RuntimeException ex) { // Unexpected exception from external delegation call -> close Connection and rethrow. releaseConnection(con, dataSource); throw ex; } } return con; }
5. Summary
As can be seen from the above process, the general transaction process is as follows:
-
Call the intercept method of interceptor org.springframework.aop.framework.cglibaopproxy $dynamicadisadvisedinterceptor by dynamically generating proxy classes.
-
In the intercept method, obtain the corresponding interceptor list and call the invoke method for processing. We only have one org.springframework.transaction.interceptor.TransactionInterceptor here. Then call its invokeWithinTransaction method
-
In the invokeWithinTransaction method of the TransactionInterceptor, a series of transaction preparations are performed
-
Get transaction attribute information and transaction manager
-
Create a transaction resource object, obtain the database connection, bind the connection with the current transaction, set the isolation level and propagation behavior according to the attributes defined by the current transaction, turn off automatic submission and set it to manual submission.
-
Set the current transaction as on, and bind the current transaction resource to the current thread
-
Call it through invocation.proceedWithInvocation(), and finally call it to our business code through methodproxy.invoke
-
The subsequent processing is performed according to whether the business code throws an exception or not
- If an exception is thrown, the execution of the current transaction is considered to have failed, rollback and other fallback operations are performed, and finally the exception continues to be thrown out
- If no exception is thrown, it is considered that the current transaction is successfully executed, the transaction is committed, and the resources are cleaned up at the same time. Restore the transaction isolation level, propagation behavior, etc. to the previous state.
-
Finally, the result is returned to the caller
-
This is the general flow of the whole transaction processing.
Simultaneous comparison Analysis of annotation @ Configuration source code in Springboot , you will also find that the dynamically generated classes in springboot are basically the same. It's all the same template.
​