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帮我阅读源码