Help us understand the problem. What is going on with this article?

Gradle および IntelliJ でモジュールを使ったときにリソースが取得できない問題

More than 1 year has passed since last update.

環境

Java

openjdk 11 2018-09-25

Gradle

Gradle 4.10.2

IntelliJ

IntelliJ IDEA 2018.2.4 (Community Edition)
Build #IC-182.4505.22, built on September 18, 2018

OS

Windows 10

発生する問題

プロジェクト構成

|-build.gradle
`-src/main/
  |-java/
  | |-module-info.java
  | `-foo/
  |   `-Foo.java
  `-resources/
    `-resource.txt

コード

build.gradle
apply plugin: "application"

sourceCompatibility = 11
targetCompatibility = 11

compileJava.options.encoding = "UTF-8"
mainClassName = "foo/foo.Foo"

compileJava {
    doFirst {
        options.compilerArgs = [
            "--module-path", classpath.asPath
        ]
        classpath = files()
    }
}

run {
    doFirst {
        jvmArgs = [
            "--module-path", classpath.asPath,
            "--module", mainClassName
        ]
        classpath = files()
    }
}
module-info.java
module foo {
    exports foo;
}
Foo.java
package foo;

import java.net.URL;

public class Foo {

    public static void main(String[] args) throws Exception {
        URL resource = Foo.class.getResource("/resource.txt");
        System.out.println("resource=" + resource);
    }
}

実行結果

> gradle run
resource=null
  • /resource.txt が取得できずに null になる
  • IntelliJ に取り込んでも同じ結果になる
  • ちなみに、 Eclipse (Photon) だと問題なく取得できる

原因(と思われるもの)

  • Gradle と IntelliJ では、 src/main/javasrc/main/resources のビルド結果が、それぞれ別のディレクトリに出力される
gradleのビルド結果
|-build
: |-classes/java/main/
: | `-foo/
: |   `-Foo.class
: |
: |-resources/main/
: : `-resource.txt
IntelliJのビルド結果
|-out/production/
: |-classes/
: | `-foo/
: |   `-Foo.class
: |
: `-resources/
:   `-resource.txt
  • このとき、モジュールパスがどのように設定されるか確認する
Foo.java
package foo;

import java.net.URL;
import java.util.stream.Stream;

public class Foo {

    public static void main(String[] args) throws Exception {
        System.out.println("[jdk.module.path]");
        Stream.of(System.getProperty("jdk.module.path")
                .split(";"))
                .map(p -> "  " + p)
                .forEach(System.out::println);

        URL resource = Foo.class.getResource("/resource.txt");
        System.out.println("resource=" + resource);
    }
}
Gradleの実行結果
[jdk.module.path]
  ...\build\classes\java\main
  ...\build\resources\main
resource=null
IntelliJの実行結果
[jdk.module.path]
  ...\out\production\classes
  ...\out\production\resources
resource=null
  • それぞれがモジュールパスに設定されている
  • つまり、 resources ディレクトリは foo モジュールとは別のモジュール扱いになっている気がする
    • module-info.class が無いディレクトリをモジュールパスに入れた場合は、無名モジュール扱いになる???
    • jar ファイルなら、ファイル名からモジュール名が解決されて自動モジュール扱いになるが、ディレクトリの場合はどうなるのかよくわかってない
  • そして、 Class.getResource(String) の JavaDoc を確認すると、次のように説明されている

このクラスが名前付きModuleにある場合、このメソッドはモジュール内のリソースを検索しようとします
(中略)
リソースが呼び出し元モジュールに対して開かれていないパッケージ内の非".class"リソースである場合、このメソッドはnullを返します

getResource(String) | Class | Java 10

  • つまり、 Class.getResource(String) は、そのクラスが属するモジュール内のリソースだけを検索しようとする
  • もしリソースが他のモジュールだった場合、そのパッケージが開かれている(opens 指定されている?)必要がある
  • もし resources が無名モジュールとして扱われているのであれば、名前付きモジュールから無名モジュールにはアクセスできない気がする
    • 少なくとも、クラスはアクセスできない
    • リソースもアクセスできないかは、正確なところはわかってない
  • ちなみに、 Eclipse の場合は src/main/javasrc/main/resources も、どちらも bin/main の下に出力されている

話を整理

  • Gradle, IntelliJ の場合、 src/main/resources ディレクトリ以下のファイルは、 src/main/java 以下のコンパイル結果とは違うディレクトリに出力される
  • src/main/java, src/main/resources の出力先は、別々のディレクトリとしてモジュールパスに指定されることになる
  • src/main/resourcessrc/main/java とは異なるモジュール扱いになる(おそらく無名モジュール?)
  • Class.getResource(String) は、基本的に同じモジュール内か、開かれているモジュール内のリソースにしかアクセスできないっぽい
  • したがって、 src/main/resources 以下に配置したファイルを、リソースとして読み取ることができなくなる

あくまで予想。

解決方法

Can't access resource with Java 10 – IDEs Support (IntelliJ Platform) | JetBrains

同じようなことで困っている人がいるっぽい。

ここで最終的に説明されている方法だと、 src/main/resources の出力先を src/main/java の出力先と同じにすればいいとのこと。

build.gradle
apply plugin: "application"
+ apply plugin: 'idea'

sourceCompatibility = 11
targetCompatibility = 11

compileJava.options.encoding = "UTF-8"
mainClassName = "foo/foo.Foo"

+ sourceSets {
+     main {
+         output.resourcesDir = java.outputDir
+     }
+ }
+ 
+ idea.module.outputDir file("out/production/classes")

compileJava {
    doFirst {
        options.compilerArgs = [
            "--module-path", classpath.asPath
        ]
        classpath = files()
    }
}

run {
    doFirst {
        jvmArgs = [
            "--module-path", classpath.asPath,
            "--module", mainClassName
        ]
        classpath = files()
    }
}
  • IntelliJ の出力先と、 Gradle の出力先をそれぞれ調整している

実行結果

Gradleの実行結果
> gradle run
[jdk.module.path]
  ...\build\classes\java\main
resource=file:/.../build/classes/java/main/resource.txt
IntelliJの実行結果
[jdk.module.path]
  ...\out\production\classes
resource=file:/.../out/production/classes/resource.txt
  • src/main/resources の出力先を揃えることで、リソースにアクセスできるようになった
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした