Several scenarios and causes of spring transaction failure

Posted by ShadowX on Tue, 01 Mar 2022 02:46:06 +0100

preface

You may have seen the spring transaction failure scenario in many articles, so let's take a look at the water today to see if you can harvest something different. Go straight to the topic

spring transaction failure scenarios and reasons

1. Scenario 1: the service is not hosted to spring

public class TranInvalidCaseWithoutInjectSpring {

    private UserService userService;

    public TranInvalidCaseWithoutInjectSpring(UserService userService) {
        this.userService = userService;
    }

    @Transactional
    public boolean add(User user){
        boolean isSuccess = userService.save(user);
        int i = 1 % 0;
        return isSuccess;
    }
}
    @Test
    public void testServiceWithoutInjectSpring(){
        boolean randomBoolean = new Random().nextBoolean();
        TranInvalidCaseWithoutInjectSpring tranInvalidCaseWithoutInjectSpring;
        if(randomBoolean){
            tranInvalidCaseWithoutInjectSpring = applicationContext.getBean(TranInvalidCaseWithoutInjectSpring.class);
            System.out.println("service Has been spring trusteeship");
        }else{
            tranInvalidCaseWithoutInjectSpring = new TranInvalidCaseWithoutInjectSpring(userService);
            System.out.println("service Not by spring trusteeship");
        }

        boolean isSuccess = tranInvalidCaseWithoutInjectSpring.add(user);
        Assert.assertTrue(isSuccess);

    }

Failure reason: the precondition for spring transaction to take effect is that the service must be a bean object
Solution: inject the service into spring

2. Scenario 2: throw the detected exception

@Service
public class TranInvalidCaseByThrowCheckException {

    @Autowired
    private UserService userService;


    @Transactional
    public boolean add(User user) throws FileNotFoundException {
        boolean isSuccess = userService.save(user);
        new FileInputStream("1.txt");
        return isSuccess;
    }
    }
 @Test
    public void testThrowCheckException() throws Exception{
        boolean randomBoolean = new Random().nextBoolean();
        boolean isSuccess = false;
        TranInvalidCaseByThrowCheckException tranInvalidCaseByThrowCheckException = applicationContext.getBean(TranInvalidCaseByThrowCheckException.class);
        if(randomBoolean){
            System.out.println("to configure@Transactional(rollbackFor = Exception.class)");
            isSuccess = tranInvalidCaseByThrowCheckException.save(user);
        }else{
            System.out.println("to configure@Transactional");
            tranInvalidCaseByThrowCheckException.add(user);
        }

        Assert.assertTrue(isSuccess);

    }

Failure reason: spring will roll back only non check exceptions and error exceptions by default
Solution: configure rollback for

3. Scenario 3: the business catches exceptions

 @Transactional
    public boolean add(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {

        }
        return isSuccess;
    }
  @Test
    public void testCatchExecption() throws Exception{
        boolean randomBoolean = new Random().nextBoolean();
        boolean isSuccess = false;
        TranInvalidCaseWithCatchException tranInvalidCaseByThrowCheckException = applicationContext.getBean(TranInvalidCaseWithCatchException.class);
        if(randomBoolean){
            randomBoolean = new Random().nextBoolean();
            if(randomBoolean){
                System.out.println("Throw the exception as it is");
                tranInvalidCaseByThrowCheckException.save(user);
            }else{
                System.out.println("set up TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();");
                tranInvalidCaseByThrowCheckException.addWithRollBack(user);
            }
        }else{
            System.out.println("The business caught the exception itself");
            tranInvalidCaseByThrowCheckException.add(user);
        }

        Assert.assertTrue(isSuccess);

    }

Failure reason: the spring transaction can be processed only after it catches the exception thrown by the business. If the business catches the exception itself, the transaction cannot perceive it
Solution:
1. Throw the exception as it is;
2. Set transactionaspectsupport currentTransactionStatus(). setRollbackOnly();

4. Scenario 4: the sequence of sections leads to

@Service
public class TranInvalidCaseWithAopSort {

    @Autowired
    private UserService userService;

    @Transactional
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }



}
@Aspect
@Component
@Slf4j
public class AopAspect {


    @Around(value = " execution (* com.github.lybgeek.transcase.aopsort..*.*(..))")
    public Object around(ProceedingJoinPoint pjp){

        try {
            System.out.println("This is a section");
           return pjp.proceed();
        } catch (Throwable throwable) {
            log.error("{}",throwable);
        }

        return null;
    }
}

Failure reason: the priority order of spring transaction aspect is the lowest. However, if the customized aspect has the same priority and the customized aspect does not correctly handle exceptions, it will be the same as the scenario in which the business catches exceptions
Solution:
1. Throw the exception as it is in the section;
2. Set transactionaspectsupport. In the aspect currentTransactionStatus(). setRollbackOnly();

5. Scenario 5: non public method

@Service
public class TranInvalidCaseWithAccessPerm {

        @Autowired
        private UserService userService;

        @Transactional
        protected boolean save(User user){
            boolean isSuccess = userService.save(user);
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                throw new RuntimeException();
            }
            return isSuccess;
        }

}
public class TranInvalidCaseWithAccessPermTest {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class);
        TranInvalidCaseWithAccessPerm tranInvalidCaseWithAccessPerm = context.getBean(TranInvalidCaseWithAccessPerm.class);
        boolean isSuccess = tranInvalidCaseWithAccessPerm.save(UserUtils.getUser());

        System.out.println(isSuccess);

    }
}

Failure reason: the default effective method permissions of spring transactions must be public

Solution:
1. Change the method to public;
2. Modify the TansactionAttributeSource and change the publicMethodsOnly to false [the conclusion is drawn from the source code trace]
3. Draw conclusions from AspectJ

The document is as follows

Method visibility and @Transactional
When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

Specific steps:

1. Introduce aspectjrt coordinates and corresponding plug-ins into pom

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.9</version>
</dependency>

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.9</version>
    <configuration>
        <showWeaveInfo>true</showWeaveInfo>
        <aspectLibraries>
            <aspectLibrary>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
             <goals>
              <goal>compile</goal>       <!-- use this goal to weave all your main classes -->
              <goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
            </goals>
        </execution>
    </executions>
</plugin> 

2. Add the following configuration to the startup class

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

Note: if it is running on idea, the following configuration is required

4. Directly use TransactionTemplate

Example:

    @Autowired
    private TransactionTemplate transactionTemplate;

    private void process(){
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                processInTransaction();
            }
        });

    }

6. Scenario 6: parent child container

Failure reason: the scan range of the sub container is too large. Scan the serivce without transaction configuration

Solution:
1. The range of parent-child containers;
2. Without parent-child containers, all bean s are managed by the same container

Note: because the example uses springboot, and there is no parent-child container by default for springboot startup, there is only one container, so this scenario demonstrates the example

7. Scenario 7: the method is modified with final

    @Transactional
    public final boolean add(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

Failure reason: because spring transaction is implemented by dynamic proxy, if the method uses final modification, the proxy class cannot rewrite the target method and implant transaction function

Solution:
1. Methods should not be modified with final

8. Scenario 8: the method is modified with static

  @Transactional
    public static boolean save(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

Failure reason: the reason is the same as final

Solution:
1. Methods should not be modified with static

9. Scenario 9: call this method

   public boolean save(User user) {
        return this.saveUser(user);
    }

    @Transactional
    public boolean saveUser(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

Failure reason: this kind of method cannot be enhanced without agent

Solution:
1. Inject yourself to call;
2. Use @ enableaspectjautoproxy (exposeproxy = true) + aopcontext currentProxy()

10. Scenario 10: multithreaded call

 @Transactional(rollbackFor = Exception.class)
    public boolean save(User user) throws ExecutionException, InterruptedException {

        Future<Boolean> future = executorService.submit(() -> {
            boolean isSuccess = userService.save(user);
            try {
                int i = 1 % 0;
            } catch (Exception e) {
                throw new Exception();
            }
            return isSuccess;
        });
        return future.get();


    }

Failure reason: because the transaction of spring is realized through database connection, and the database connection spring is placed in threadLocal. The same transaction can only be connected with the same database. In the multithreaded scenario, the database connections obtained are different, that is, they belong to different transactions

11. Scenario 11: wrong communication behavior

 @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

Failure reason: the propagation feature used does not support transactions

12. Scenario 12: using a storage engine that does not support transactions

Failure reason: a storage engine that does not support transactions is used. For example, MyISAM in mysql

13. Scenario 13: the data source is not configured with a transaction manager

Note: because of the spring boot, the transaction manager has been started by default. org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration. Therefore, the example is omitted

14. Scenario 14: premature instantiation of the proxy class

@Service
public class TranInvalidCaseInstantiatedTooEarly implements BeanPostProcessor , Ordered {

    @Autowired
    private UserService userService;


    @Transactional
    public boolean save(User user) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

Failure reason: when the instantiation of the proxy class is earlier than the AbstractAutoProxyCreator postprocessor, it cannot be enhanced by the AbstractAutoProxyCreator postprocessor

summary

This article lists 14 scenarios of spring transaction failure. In fact, many of them are caused by the same kind of problems in the final analysis, such as dynamic agent, method qualifier, exception type, etc

demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-transaction-invalid-case

Topics: Spring Transaction