WildFly中JUnit5 TestEngine可通过Class.forName加载但无法被ServiceLoader识别的问题
问题描述
我在WildFly上部署了一个WAR包,它依赖一个包含JUnit 5 JAR的模块,但一直遇到以下异常:
[Cannot create Launcher without at least one TestEngine; consider
adding an engine implementation JAR to the classpath]:
org.junit.platform.commons.PreconditionViolationException: Cannot
create Launcher without at least one TestEngine; consider adding an
engine implementation JAR to the classpath
我用这段代码来排查问题:
@RestController @RequestMapping("/Paolo") public class HelloController { Logger logger = LoggerFactory.getLogger(HelloController.class); @GetMapping("/hello") String hello() throws ClassNotFoundException { Class.forName("org.junit.jupiter.engine.JupiterTestEngine"); // 没有抛出异常 Iterable<TestEngine> testEngines = ServiceLoader.load(TestEngine.class, ClassLoaderUtils.getDefaultClassLoader()); // JUnit用来查找TestEngine的方法,JupiterTestEngine是唯一的实际实现 logger.info("testEngines: {}", testEngines.iterator().hasNext()); // 输出false return "Hi"; } }
如果我把JUnit 5的JAR放到WAR的WEB-INF/lib目录下,一切都能正常工作。我了解到模块会被加载并添加到WAR的类路径中,但为什么放到模块里就不行呢?
问题原因与解决方案
核心原因:类加载器隔离性与SPI资源访问限制
你遇到的问题本质是WildFly模块类加载器的隔离特性和Java SPI(服务提供者接口)的工作逻辑共同导致的:
- Class.forName能成功的原因:因为你的WAR明确依赖了这个JUnit模块,WildFly的类加载机制会允许WAR的类加载器访问模块中的类,所以能成功加载
JupiterTestEngine类。 - ServiceLoader找不到实现的原因:JUnit的
TestEngine是通过SPI机制加载的,它需要读取JAR中META-INF/services/org.junit.platform.engine.TestEngine这个配置文件,文件里指定了JupiterTestEngine作为实现类。
当JUnit JAR在WildFly模块中时:
- WildFly的模块默认是资源隔离的,即使模块的类对WAR可见,模块JAR中的
META-INF/services资源可能没有被WAR的类加载器正确访问到——WAR的默认类加载器无法遍历到模块JAR内的SPI配置文件,所以ServiceLoader找不到任何TestEngine实现。 - 你代码中用的
ClassLoaderUtils.getDefaultClassLoader()大概率是WAR自身的类加载器,它没有权限读取模块内部的SPI配置,自然拿不到实现类。
而当JAR放在WEB-INF/lib时,所有JAR都在WAR的类加载器管辖范围内,ServiceLoader可以轻松读取每个JAR的META-INF/services文件,所以能正常找到JupiterTestEngine。
解决方案
针对这个问题,你可以尝试以下几种方案:
1. 调整WildFly模块配置,开放资源访问
在你的JUnit模块的module.xml文件中,确保模块的SPI配置资源能被依赖它的WAR访问:
<module name="com.your.junit.module" xmlns="urn:jboss:module:1.5"> <resources> <resource-root path="junit-jupiter-api-5.x.x.jar"/> <resource-root path="junit-jupiter-engine-5.x.x.jar"/> <!-- 其他JUnit相关依赖JAR --> </resources> <dependencies> <!-- 模块所需的其他依赖,比如JDK模块等 --> </dependencies> <exports> <!-- 明确导出SPI配置所在的资源路径 --> <export path="META-INF/services"/> <!-- 或者直接导出模块所有内容,适合简单场景 --> <!-- <export-all/> --> </exports> </module>
2. 手动指定模块类加载器给ServiceLoader
你可以通过已加载的JupiterTestEngine类获取到模块的类加载器,再传给ServiceLoader,这样就能读取到模块内的SPI配置:
// 通过已加载的类获取模块类加载器 ClassLoader moduleClassLoader = Class.forName("org.junit.jupiter.engine.JupiterTestEngine").getClassLoader(); // 用模块类加载器初始化ServiceLoader Iterable<TestEngine> testEngines = ServiceLoader.load(TestEngine.class, moduleClassLoader);
3. (推荐,若场景允许)将JUnit JAR保留在WEB-INF/lib
如果你的场景不是必须把JUnit放在模块中,直接将JUnit JAR放在WEB-INF/lib是最省心的方案——既不需要调整模块配置,也能避免类加载器隔离带来的各种问题,这也是Web应用依赖第三方库的标准做法。
备注:内容来源于stack exchange,提问作者Pp88

