如何通过Maven Surefire插件在测试时添加JPMS模块及解析失败原因
场景说明
我使用Maven和maven-surefire-plugin(3.0.0版本)开发Java模块系统(JPMS)项目:
fancythingprovider模块:通过provides ThingProvider with FancyThingProvider声明服务提供者thinguser模块:通过uses ThingProvider声明服务依赖,其中ThingUser类负责通过ServiceLoader加载服务
基础测试已正常运行:JUnit测试类使用ThingUser可以成功找到FancyThingProvider。
现在需要仅在测试阶段引入额外的simplethingprovider模块,让ThingUser同时发现FancyThingProvider和SimpleThingProvider,但不想在thinguser的主module-info.java中添加requires simplethingprovider(因为主模块本身不依赖该模块)。
尝试过的无效方案
- 在
maven-surefire-plugin的argLine中添加--add-modules simplethingprovider:失败,因为simplethingprovider不在Surefire运行时的模块路径上,也未被其他模块引用,无法加入模块图。 - 意识到
simplethingprovider处于测试类路径(属于未命名模块),添加--add-reads thinguser=ALL-UNNAMED:仍未生效。 - 为
simplethingprovider补充了传统SPI配置文件/META-INF/services/ThingProvider(内容为SimpleThingProvider),因为类路径下的模块module-info.java会失效,但--add-reads依旧没有效果。
为什么--add-reads方案不生效?
--add-reads thinguser=ALL-UNNAMED仅能让thinguser模块读取未命名模块中的类,但服务发现的核心逻辑并不受此影响:
- 当模块中的代码调用
ServiceLoader.load(ThingProvider.class)时,默认只会扫描模块路径中已解析的模块(通过provides声明的服务),以及调用者所在模块的类资源。 - 未命名模块中的SPI配置不会被模块上下文的
ServiceLoader自动扫描,因为JPMS的服务发现逻辑在模块和未命名模块之间是隔离的——模块的ServiceLoader不会主动去未命名模块中查找服务,哪怕配置了--add-reads。 - 加上Surefire的测试机制:测试类会被
--patch-module合并到主模块中,测试代码的运行上下文依然是thinguser模块,而非未命名模块,所以服务加载逻辑还是局限在模块路径范围内。
可行解决方案
方案1:将simplethingprovider放到模块路径
修改maven-surefire-plugin配置,确保simplethingprovider被加入模块路径,同时用--add-modules将其引入模块图:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0</version> <configuration> <!-- 开启模块路径模式,让测试依赖进入模块路径 --> <useModulePath>true</useModulePath> <!-- 强制引入simplethingprovider模块 --> <argLine>--add-modules simplethingprovider</argLine> </configuration> </plugin>
开启useModulePath=true后,Surefire会自动将测试依赖(包括simplethingprovider)放到模块路径,此时--add-modules可以成功将其加入模块图,simplethingprovider的module-info.java中的provides声明会被正常识别,无需额外SPI文件。
方案2:测试代码中手动加载未命名模块的服务
如果必须将simplethingprovider留在类路径,可以在测试代码中显式从系统类加载器(未命名模块的类加载器)加载服务并合并:
// 加载模块上下文的服务 ServiceLoader<ThingProvider> moduleProviders = ServiceLoader.load(ThingProvider.class); // 加载未命名模块的服务 ClassLoader unnamedClassLoader = ClassLoader.getSystemClassLoader(); ServiceLoader<ThingProvider> unnamedProviders = ServiceLoader.load(ThingProvider.class, unnamedClassLoader); // 合并两个来源的服务 Set<ThingProvider> allProviders = new HashSet<>(); moduleProviders.forEach(allProviders::add); unnamedProviders.forEach(allProviders::add); // 使用allProviders进行测试断言
方案3:创建测试专属模块描述符
在src/test/java下创建独立的module-info.java,专门用于测试场景:
module thinguser.test { requires thinguser; requires simplethingprovider; requires org.junit.jupiter.api; // 导出测试类给JUnit平台 exports com.example.thinguser.test to org.junit.platform.commons; }
修改Surefire配置,让测试使用这个测试模块运行:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0</version> <configuration> <modulepath>${project.testModulePath}</modulepath> <mainClass>org.junit.platform.console.ConsoleLauncher</mainClass> <arguments> <argument>--scan-classpath</argument> </arguments> </configuration> </plugin>
这种方式完全隔离了主模块和测试模块的依赖,是JPMS测试的标准实践之一。
内容的提问来源于stack exchange,提问作者Laird Nelson

