Java8 では動くけど Java11 では死ぬコードがあって、調査をしたら、原因がこれだったが、再現に手間取ったので結果をメモっておく。
発生条件は以下のとおりである。
- fat jar を
java -jar
で起動すると発生する。- gradle からの
bootRun
やtest
では発生しない。(fat jar から起動するようなテストを書かない限り)
- gradle からの
ForkJoinPool.commonPool()
で提供されるスレッドでThread.currentThread().getContextClassLoader()
) からリソースをロードする。- すると
src/main/resources
配下のリソースが参照できなくて null になる。
- すると
実際に死んだコードはストリームの parallel()
) 内でリソースを参照していた。
再現するコード (かなり雑) は以下のとおりである。これを Spring Initializr で生成したプロジェクトにぶっこんで bootJar
をつくる。
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 を使うと良さそうである。(手元のコードはこれで回避することができた。)