Service the method in this class calls another method, and the transaction does not take effect & the AOP log service does not generate problems

Posted by btfans on Tue, 25 Jan 2022 08:32:46 +0100

[question]

I don't think I need to introduce Spring's declarative transactions more. In a word, "since using Spring AOP, transaction management is really easy, really easy; transaction management code is gone, brain is not sour, hands are not painful, and transactions are fully equipped at one time; lightweight, easy to test, hey!". From any perspective, lightweight declarative transactions are a great thing to liberate productivity. So we "always use it".

However, in a recent project, there was a problem in transaction management: there was a service class that declared a transaction method, which did three SQL insert operations. However, when an error rolled back later, it was found that the previous insert was successful. That is to say, this declared transaction method did not actually start the transaction! What's going on? Does Spring's declarative transaction fail?

[exploring]

In fact, some people have said that the transaction configuration of Spring does not work, but according to the first reaction and past experience, I always tell him that there must be a problem with your configuration; So this time, I think there is no exception. It is probably that the transaction annotation is assigned to the interface rather than the implementation method, or if it is declared in XML, it is likely that the expression of the pointcut is not paired.

However, after checking their configuration, it is found that there is no configuration problem. In the implementation method of the transaction, the @ Transactional transaction annotation declaration is used, and the XML is also equipped with annotation driven < TX: annotation driven... / >. The configuration is very correct. How can it not work?

I was puzzled, so I went on to ask:

Q1: is this the case with other methods?

Answer 1: No.

Q2: is there anything special about this method (hereinafter referred to as method B)?

Answer 2: there are three records inserted in the background, nothing special.

Q3: is this method called directly from the Web layer?

Answer 3: No, it was transferred from another method of this Service class (hereinafter referred to as ServiceA) (hereinafter referred to as method A).

Q4: Oh, is the method calling it equipped with a transaction (the problem may be here)?

Answer 4: No.

Q5: the Action of the WEB layer (using struts 2) calls method A that does not declare A transaction, and method A calls method B that declares A transaction?

Answer 5: Yes.

Q6: you can add A transaction declaration directly to method A

Answer 6: OK...

It seems that the problem may be found, so @ Transactional is added to method A and the project test is started. The result is that the transaction takes effect normally, and method A and method B are in the same transaction.

OK, now summarize the phenomenon:

1. ServiceA class is the Action service of the Web layer

2. Action calls method A of ServiceA, but method A does not declare A transaction (because method A itself is time-consuming and does not need A transaction)

3. Method A of ServiceA calls its own method B, and method B declares A transaction, but the transaction declaration of method B fails in this case.

4. If A transaction is also declared on method A, the transaction will take effect when Action calls method A, and method B will automatically participate in the transaction.

I asked him to add A to the transaction statement first, and decided to come back and test it by himself.

On the surface, this problem is the invalidation of transaction declaration. In essence, it is likely to be the implementation perspective of Spring's AOP mechanism. I think of a phenomenon discovered when studying the AOP implementation of Spring a long time ago: for the AOP target class enhanced by Cglib, two objects will be created, one is the Bean instance itself, and the other is the Cglib enhanced proxy object, not just the latter. I once wondered about this, but I didn't explore it any more.

As we know, there are two ways to implement Spring's AOP: 1. Java proxy; 2. Cglib dynamic enhancement mode, which can be seamlessly and freely switched in Spring. The advantage of Java proxy is that it does not rely on third-party jar packages. The disadvantage is that it can not proxy classes, but only proxy interfaces.

Spring abstracts these two implementations through the AopProxy interface and implements a consistent AOP method:

Now it seems that this abstraction also has a defect, that is, it obliterates the ability of Cglib to directly create enhanced subclasses of ordinary classes. Spring is equivalent to using the subclasses dynamically generated by Cglib as ordinary proxy classes, which is why two objects are created. The following figure shows the actual calling process of spring's AOP proxy class:

Therefore, it can be seen from the above analysis that methodB is not notified by AopProxy, resulting in the final result: for a class enhanced by Spring's AOP, when the internal method of the same class is called, the enhanced notification on the called method will not work.

What impact will this result have

1: When called internally, the transaction declaration of the called method will not work

2: In other words, when you declare that a method needs transactions, if there are other developers in this class, you can't guarantee that the method will really be in the transaction environment

3: In other words, Spring's transaction propagation strategy will not work when calling internal methods. Whether you want a method to require separate transactions, RequiresNew, Nested transactions, Nested, etc., it doesn't work.

4: Not just transaction notifications, but all AOP notifications you implement using Spring will be subject to the same restrictions....

[solution]

The cause of the problem has been found. In fact, my ideal AOP implementation should be as follows:

As long as a Cglib enhanced object is good, for the Java proxy method, my choice is to abandon it without hesitation.

As for the previous transaction problem, as long as we avoid the limitations of Spring's current AOP implementation, either declare transactions, or separate them into two classes, or directly use programmatic transactions in methods, then everything is OK.

resolvent

//Get an instance from the spring context and call similar methods
	@Override
    public void testTransaction() {
        SpringContextUtil.getBean(this.getClass()).transactionTest();

    }

    @Transactional(rollbackFor = Exception.class)
    @OpLog(desc = "Test whether the transaction is effective", operationType = OperationType.NOTIFY, referer = Referer.THIRD_PARTY)
    public void transactionTest(){
        InsInfoCompany company = new InsInfoCompany();
        company.setId(2);
        company.setContactName("Zhang San");
        companyMapper.updateById(company);
        int i = 1/0;
    }
package com.innothings.utils.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.util.Objects;


@Slf4j
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    /**
     * Is it a production environment
     */
    public static Boolean isPro;

    @EventListener(ApplicationReadyEvent.class)
    public void initActiveProfile() {
        isPro = Objects.equals(SpringContextUtil.getActiveProfile(), "pro");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        if (SpringContextUtil.applicationContext == null) {
            SpringContextUtil.applicationContext = applicationContext;
        }

    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //According to name
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    //According to type
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

    public static String getActiveProfile() {
        return applicationContext.getEnvironment().getActiveProfiles()[0];
    }
}

Topics: Java AOP