Open7

Spring の Classpath 配下の Resource を扱う

com4dccom4dc

Spring Boot における Resources 配下のファイルを扱いたい

以下のような構成

│       │   └── resources
│       │       ├── application.yaml
│       │       ├── hoge.json
│       │       └── logback-spring.xml

この hoge.json をコード内で読み込んで利用したい

com4dccom4dc

IntelliJ を使って手元での実行では問題ないが、 Fat jar に Bundle するとエラーになるコード

val json = Files.readString(Paths.get(ClassLoader.getSystemResource("hoge.json").toURI()))

Fat jar にした Bundle で実行すると以下のエラーが発生する

java.lang.NullPointerException: null

debug すると以下が null であることがわかる

ClassLoader.getSystemResource("hoge.json")
com4dccom4dc

なぜエラーとなるか

https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/ClassLoader.html#getSystemResource(java.lang.String)

気になるのはこのあたり

このメソッドは、パッケージが無条件でopenedの場合にのみ、名前付きモジュールのパッケージ内のリソースを検索します。

resources 配下に配置された任意のリソースは、実際には BOOT-INF/classes の Root に配置されるため、このメソッドを利用したリソースの特定はそもそも不適格なのでは?

com4dccom4dc

ClassPathResource

Resource IF の実装がいくつかあるのでどれを使えば良いかの使い分けはあまりはっきりわかっていない。
このクラスが良さそうに思えるのだが、 Javadoc を見ると気になる記載がある。

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/ClassPathResource.html

クラスパスリソースがファイルシステムにある場合は java.io.File として解決をサポートしますが、JAR 内のリソースはサポートしません。常に URL としての解決をサポートします。

JAR 内のリソースはサポートしない?と読める。さらに Spring Boot に気になる Issue が未だ Close されていない

https://github.com/spring-projects/spring-boot/issues/7161

com4dccom4dc

ResourceLoader を Injection

Spring 由来の ResourceLoader がある。

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/ResourceLoader.html

SpringBoot では様々なリソース(例えば application.yaml 等)も resources に配置されこれらを読んでいるため、 Spring 由来の ResourceLoader は必ず利用しているはず。
Bean として登録されているため、Injection されることを期待したコードを書いておくと実際正常に動作する

class Hoge(
    private val resourceLoader: ResourceLoader,
) {

...

fun hoge(): String {
    return Files.readString(resourceLoader.getResource("hoge.json").file.toPath())
}
com4dccom4dc

Resource 配下のファイルにアクセスする際には Fixed File としてではなく InputStream として読む

https://stackoverflow.com/questions/43060265/spring-boot-classpathresource-in-a-jar

https://ito-u-oti.com/post-250/

どうも InputStream として読むことでうまくいくようだ。理由についてはまだ判然としていないので要調査。

結果として以下のコードで手元でもBundleした JAR ファイルでも正しく動くことを確認した。

    val inputStream = ClassPathResource("hoge.json").inputStream
    val json = String(inputStream.readAllBytes(), StandardCharsets.UTF_8)

わかったようで何もわかっていない Classpath, ResourceLoader 周り