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();
    }
}
其中DemoService的startXxxTask下的代码逻辑如下所示:
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实现类时的执行顺序 - 
关于
ApplicationRunner和CommandLineRunner被调用的时机:- 
//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()方法。在这个方法中会取出容器中的所有的ApplicationRunner和CommandLineRunner的实现类并进行排序,使用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); } } - 
由此可见,容器中的
ApplicationRunner和CommandLineRunner的实现类是在SpringBoot容器初始化完毕之后,由主线程按照@Order注解的值顺序执行的,因此在ApplicationRunner和CommandLineRunner的实现类的run()中尽量不要写耗时较长的操作,以免造成阻塞。 
 - 
 
5.Others
- 在测试类中加上
@RunWith(SpringJUnit4ClassRunner.class)注解时,JUnit测试将使用SpringJUnit4ClassRunner来运行SpringJUnit4ClassRunner继承自BlockJUnit4ClassRunner,它是JUnit提供的标准Runner之一。在SpringJUnit4ClassRunner运行测试时,它会执行以下步骤:- 加载测试类并创建测试实例。
 - 执行@BeforeClass方法(如果存在)
 - 创建一个Spring应用程序上下文
 - 执行@Before方法(如果存在)
 - 执行测试方法。
 - 执行@After方法(如果存在)
 - 销毁Spring应用程序上下文
 - 执行@AfterClass方法(如果存在)
 
 - 挖个坑,具体SpringJUnit4ClassRunner源码解析会在日后补上
 - 感谢chatGpt帮我阅读源码
 

                
            
        
浙公网安备 33010602011771号