SpringBoot测试类运行后无反应问题排查

SpringBoot测试类运行后无反应问题排查

1.情景复现

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Application.class)
public class DemoTest {

    @Test
    public void demoTest(){
        System.out.println("test");
    }
}

在上面这段代码运行后,SpirngBoot启动后,没有运行该测试类,并且进程不结束

2.问题排查

首先排查了Junit版本,尝试手动引入Junit代替spring-boot-starter-test,结果是一样的

因为在之前版本,测试类是能正常执行的,于是检查最近版本代码的提交,最后定位到问题出现在以下代码中:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class InitRun implements ApplicationRunner {

    @Autowired
    private DemoService demoService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        demoProcessService.startXxxTask();
    }
}

其中DemoServicestartXxxTask下的代码逻辑如下所示:

private static final ArrayBlockingQueue<DemoEntity> queue = new ArrayBlockingQueue<>(50);

public void startXxxTask() throws InterruptedException {
        logger.info("xxx task start.");
        while (true) {
            DemoEntity demoEntity = queue.take();
            ......
            try {
                excuteXxxTask(demoEntity);
            } catch (Exception e) {
                logger.error("xxx error, demoEntity: " + gson.toJson(demoEntity));
            }
        }
    }

经过查阅相关资料以及阅读源码之后,了解到ApplicationRunner实现类的run()方法会在SpringBoot容器启动完成后,由主线程执行该类的run()方法(具体原理下面会介绍),如果run()方法中存在死循环,会阻塞后面流程的执行(会影响容器中的其他ApplicationRunner实现类的run()方法执行),测试类的执行是在run()方法都执行完毕之后才会执行,所以会出现上面描述的运行测试类后,SpringBoot正常启动,并且无异常抛出但不执行测试类并且进程不结束的情况

3.改进方案

可以在ApplicationRunner实现类的run()方法中新开启一个线程来执行耗时较长或者会造成阻塞的代码,具体实现如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class InitRun implements ApplicationRunner {

    @Autowired
    private DemoService demoService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        new Thread(() -> {
            try {
                demoProcessService.startXxxTask();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();
    }
}

4.关于SpringBoot的ApplicationRunner类

  • ApplicationRunner接口效果类似还有CommandLineRunner接口,两者的不同点在于run()方法传入的参数是不一样的,ApplicationRunner接口的run()方法接收的参数是字符串,而CommandLineRunner接口的run()方法接收的参数是字符串数组。这里的参数可以在Idea的配置中配置Program arguments的值来进行配置,也可以使用java -jar xxx.jar arg1 arg2 ...命令来传入

  • 可以通过@Order注解来指定在容器中存在多个ApplicationRunner或者CommandLineRunner实现类时的执行顺序

  • 关于ApplicationRunnerCommandLineRunner被调用的时机:

    • //SpringApplication的run()方法
      public ConfigurableApplicationContext run(String... args) {
      
          StopWatch stopWatch = new StopWatch();
      
          stopWatch.start();
          ConfigurableApplicationContext context = null;
          Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
          configureHeadlessProperty();
          SpringApplicationRunListeners listeners = getRunListeners(args);
          listeners.starting();
          try {
              ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                  args);
              ConfigurableEnvironment environment = prepareEnvironment(listeners,
                                                                       applicationArguments);
              configureIgnoreBeanInfo(environment);
              Banner printedBanner = printBanner(environment);
              context = createApplicationContext();
              exceptionReporters = getSpringFactoriesInstances(
                  SpringBootExceptionReporter.class,
                  new Class[] { ConfigurableApplicationContext.class }, context);
              prepareContext(context, environment, listeners, applicationArguments,
                             printedBanner);
              refreshContext(context);
              afterRefresh(context, applicationArguments);
              stopWatch.stop();
              if (this.logStartupInfo) {
                  new StartupInfoLogger(this.mainApplicationClass)
                      .logStarted(getApplicationLog(), stopWatch);
              }
              listeners.started(context);
              //就是在这里进行调用
              callRunners(context, applicationArguments);
          }
          catch (Throwable ex) {
              handleRunFailure(context, ex, exceptionReporters, listeners);
              throw new IllegalStateException(ex);
          }
      
          try {
              listeners.running(context);
          }
          catch (Throwable ex) {
              handleRunFailure(context, ex, exceptionReporters, null);
              throw new IllegalStateException(ex);
          }
          return context;
      }
      
    • //SpringApplication的callRunners()方法
      private void callRunners(ApplicationContext context, ApplicationArguments args) {
          List<Object> runners = new ArrayList<>();
          runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
          runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
          //根据@Order注解进行排序
          AnnotationAwareOrderComparator.sort(runners);
          for (Object runner : new LinkedHashSet<>(runners)) {
              if (runner instanceof ApplicationRunner) {
                  callRunner((ApplicationRunner) runner, args);
              }
              if (runner instanceof CommandLineRunner) {
                  callRunner((CommandLineRunner) runner, args);
              }
          }
      }
      
    • 如上述源码所示,在SpringBoot容器初始化完毕后,会调用callRunners()方法。在这个方法中会取出容器中的所有的ApplicationRunnerCommandLineRunner的实现类并进行排序,使用callRunner()执行每个实现类的run()方法,其中callRunner()方法源码如下:

    • private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
          try {
              (runner).run(args);
          }
          catch (Exception ex) {
              throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
          }
      }
      
    • 由此可见,容器中的ApplicationRunnerCommandLineRunner的实现类是在SpringBoot容器初始化完毕之后,由主线程按照@Order注解的值顺序执行的,因此在ApplicationRunnerCommandLineRunner的实现类的run()中尽量不要写耗时较长的操作,以免造成阻塞。

5.Others

  • 在测试类中加上@RunWith(SpringJUnit4ClassRunner.class)注解时,JUnit测试将使用SpringJUnit4ClassRunner来运行SpringJUnit4ClassRunner继承自BlockJUnit4ClassRunner,它是JUnit提供的标准Runner之一。在SpringJUnit4ClassRunner运行测试时,它会执行以下步骤:
    1. 加载测试类并创建测试实例。
    2. 执行@BeforeClass方法(如果存在)
    3. 创建一个Spring应用程序上下文
    4. 执行@Before方法(如果存在)
    5. 执行测试方法。
    6. 执行@After方法(如果存在)
    7. 销毁Spring应用程序上下文
    8. 执行@AfterClass方法(如果存在)
  • 挖个坑,具体SpringJUnit4ClassRunner源码解析会在日后补上
  • 感谢chatGpt帮我阅读源码
posted @ 2023-03-22 00:11  人之为言  阅读(997)  评论(0编辑  收藏  举报