You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WildFly中JUnit5 TestEngine可通过Class.forName加载但无法被ServiceLoader识别的问题

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(服务提供者接口)的工作逻辑共同导致的:

  1. Class.forName能成功的原因:因为你的WAR明确依赖了这个JUnit模块,WildFly的类加载机制会允许WAR的类加载器访问模块中的类,所以能成功加载JupiterTestEngine类。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 19:40:28