Java 9模块中Gradle+Jigsaw项目JUnit测试资源文件放置位置问询
我之前也碰到过这个坑,Java 9的模块系统把资源和类一样做了封装,不再是全局可访问的,再加上Gradle的Jigsaw插件对模块打包的特殊处理,确实容易踩资源找不到的问题。下面给你一步步说清楚怎么解决:
核心原因
Java 9模块系统下,资源属于特定模块的一部分,只有当模块明确开放资源所在的路径(或者资源在模块内部被访问)时,代码才能加载到它。Gradle的java-module-info插件会严格遵循模块规则来打包主代码和测试代码的资源,所以光放对位置还不够,得配合module-info配置才行。
资源存放位置(依然可以用熟悉的路径)
你完全可以继续把测试资源放在src/test/resources/目录下——不管是放在根目录(比如src/test/resources/foo.txt)还是某个包路径下(比如src/test/resources/com/example/hello/foo.txt),Gradle都会自动把这些资源打包到测试模块的classpath里。
真正关键的是模块描述文件的配置,分两种场景来说:
场景1:测试代码和主代码在同一个模块
比如你的主模块是com.example.hello,测试代码也在这个模块下:
- 在
src/test/java/下创建测试专用的模块描述文件module-info.test.java(Gradle会自动识别这个文件作为测试模块的配置):
module com.example.hello { // 声明依赖JUnit API requires org.junit.jupiter.api; // 开放测试类所在的包给JUnit,允许它反射调用测试方法 opens com.example.hello.test to org.junit.jupiter.api; // 不需要额外声明资源——模块内部的代码默认可以访问模块内的所有资源 }
- 测试代码中访问资源的正确姿势:
推荐用Java 9新增的ModuleAPI来访问,比传统的ClassLoader更贴合模块系统:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.InputStream; public class ResourceTest { @Test void testLoadResource() throws Exception { // 访问根目录下的foo.txt try (InputStream is = getClass().getModule().getResourceAsStream("foo.txt")) { assertNotNull(is, "资源文件foo.txt未找到"); // 这里可以添加资源读取逻辑 } // 如果是放在com/example/hello/下的bar.txt,路径要写完整 try (InputStream is = getClass().getModule().getResourceAsStream("com/example/hello/bar.txt")) { assertNotNull(is, "资源文件bar.txt未找到"); } } }
如果非要用ClassLoader,记得去掉路径前的斜杠(模块系统中,ClassLoader.getResource("foo.txt")是相对于模块根目录查找,带斜杠的绝对路径在模块环境下会失效):
URL resource = ClassLoader.getSystemClassLoader().getResource("foo.txt"); assertNotNull(resource);
场景2:测试代码在独立的测试模块
如果你的测试代码是一个单独的模块(比如主模块com.example.hello,测试模块com.example.hello.test):
- 主模块的
src/main/java/module-info.java需要开放资源所在的路径给测试模块:
比如主资源放在src/main/resources/foo.txt(根目录),就要开放根目录给测试模块:
module com.example.hello { exports com.example.hello; // 开放根目录的资源给测试模块,允许它访问 opens . to com.example.hello.test; }
如果资源放在某个包下(比如src/main/resources/com/example/hello/foo.txt),就开放对应的包:
module com.example.hello { exports com.example.hello; opens com.example.hello to com.example.hello.test; }
- 测试模块的
src/test/java/module-info.test.java要声明依赖主模块和JUnit:
module com.example.hello.test { requires com.example.hello; requires org.junit.jupiter.api; opens com.example.hello.test to org.junit.jupiter.api; }
- 测试代码中访问主模块的资源,路径规则和场景1一致;测试模块自己的资源(
src/test/resources/下的文件)直接访问即可。
Gradle配置注意事项
确保你的build.gradle正确引入了java-module-info插件,并且配置了JUnit平台:
plugins { id 'java' id 'java-module-info' // 关键:启用Jigsaw模块支持 } repositories { mavenCentral() } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.+' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.+' } test { useJUnitPlatform() // 启用JUnit 5平台 }
常见坑点
- 不要用带前导斜杠的资源路径(比如
/foo.txt):在模块系统中,这种绝对路径会被视为全局路径,但模块资源是隔离的,所以找不到。 - 别忘了给JUnit开放测试包:如果没加
opens your.test.package to org.junit.jupiter.api;,JUnit可能无法反射调用测试方法,甚至间接导致资源加载异常。 - 测试模块的
module-info文件要放在src/test/java/下,命名为module-info.test.java,Gradle才会把它当作测试模块的配置。
内容的提问来源于stack exchange,提问作者michas

