いけむランド

はてダからやってきました

Java 11 and ForkJoinPool.commonPool() class loading issue を再現する

Java8 では動くけど Java11 では死ぬコードがあって、調査をしたら、原因がこれだったが、再現に手間取ったので結果をメモっておく。

github.com

発生条件は以下のとおりである。

  • fat jar を java -jar で起動すると発生する。
    • gradle からの bootRuntest では発生しない。(fat jar から起動するようなテストを書かない限り)
  • ForkJoinPool.commonPool() で提供されるスレッドで Thread.currentThread().getContextClassLoader()) からリソースをロードする。
    • すると src/main/resources 配下のリソースが参照できなくて null になる。

実際に死んだコードはストリームの parallel()) 内でリソースを参照していた。

再現するコード (かなり雑) は以下のとおりである。これを Spring Initializr で生成したプロジェクトにぶっこんで bootJar をつくる。

gist.github.com

Java11 だとアクセスされた時に getResourceAsStream が null を返すため、その後の read で NPE を吐いてしまう。

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause

java.lang.NullPointerException: null
    at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:878) ~[guava-30.1-jre.jar!/:na]
    at com.google.common.io.ByteStreams.toByteArray(ByteStreams.java:220) ~[guava-30.1-jre.jar!/:na]
    at com.google.common.io.ByteSource.read(ByteSource.java:286) ~[guava-30.1-jre.jar!/:na]
    at com.google.common.io.ByteSource$AsCharSource.read(ByteSource.java:472) ~[guava-30.1-jre.jar!/:na]
    at com.example.demo.DemoController.lambda$index$1(DemoController.java:29) ~[classes!/:na]
    at java.base/java.util.stream.ReduceOps$2ReducingSink.accept(ReduceOps.java:123) ~[na:na]
    at java.base/java.util.stream.ReduceOps$2ReducingSink.combine(ReduceOps.java:135) ~[na:na]
    at java.base/java.util.stream.ReduceOps$2ReducingSink.combine(ReduceOps.java:107) ~[na:na]
    at java.base/java.util.stream.ReduceOps$ReduceTask.onCompletion(ReduceOps.java:959) ~[na:na]
    at java.base/java.util.concurrent.CountedCompleter.tryComplete(CountedCompleter.java:592) ~[na:na]
    at java.base/java.util.stream.AbstractTask.compute(AbstractTask.java:328) ~[na:na]
    at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:746) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594) ~[na:na]
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177) ~[na:na]

回避するためには ClassPathResource を使うと良さそうである。(手元のコードはこれで回避することができた。)

docs.spring.io