@DelegateでJavaバージョンの差異が顕在化するケース
JDK 8でビルドしたクラスファイルをJDK 7で実行するとClassNotFoundExceptionが発生する事象でハマったのでメモ。結論からいうと、ソースコードでJava 8に固有のクラスを直接参照していなくても、@Delegate
によって間接的に参照されるケースがあります。
前提
JDK 8でのコンパイル時にsource, targetのバージョンを指定しているものとします。 そうしないと、JDK 7でクラスファイルを読み込んだ時に以下のエラーが発生します。
Exception in thread "main" java.lang.UnsupportedClassVersionError: Main : Unsupported major.minor version 52.0
Gradleでは以下の2行を書くだけでOKです。
sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7
再現方法
例えば、以下のように@Delegate
を使ったクラスがあるとします。
class NamedMap<T> { @Delegate private final Map<String, T> map = [:] }
そして、クラスファイルをJDK 7で読み込むと以下のエラーが発生します。
Exception in thread "main" java.lang.NoClassDefFoundError: java/util/function/BiFunction
原因
先ほどのクラスをJDK 7でコンパイルした場合、Java 7のMap
クラスに対するデリゲートメソッドが生成されます。
一方、JDK 8でコンパイルした場合、Java 8のMap
クラスに対するデリゲートメソッドが生成されます。
Java 8のMap
クラスでは新しいメソッドが追加されているため、Java 7の実行環境では存在しないクラスを参照することになります。
例えば、Java 8で新たに追加されたcomputeIfPresent(K, BiFunction<K, V, V>)
メソッドのデリゲートは以下のようになります。
class NamedMap<T> { @Delegate private final Map<String, T> map = [:] // コンパイル時にデリゲートメソッドが追加される T computeIfPresent(String key, BiFunction<K, V, V> func) { map.computeIfPresent(key, func) } }
このクラスファイルをJDK 7で読み込もうとすると、Java 8に固有のクラスBiFunction
が見つからないため、ClassNotFoundExceptionが発生します。
ソースコードでJava 8に固有のクラスを直接参照していなくても、@Delegate
によって間接的に参照されるケースがあります。
対策
クロスコンパイルを行う場合、古いバージョンのJDKでコンパイルするか、-bootclasspath
を指定するようにしましょう。
また、Travis CIなどを活用して複数バージョンのJDKで自動テストを実行するようにしましょう。
余談
ソースコードを見てもClassNotFoundExceptionの原因がつかめない場合、クラスファイルを逆コンパイルすると解決の糸口が見えることがあります。 以下のようにjavapコマンドを使います。
javap -classpath build/classes/main com.example.Main
特にオプションを付けなければメソッド一覧が表示されます。
Compiled from "Main.groovy" public class com.example.Main { /* 中略 */ public java.lang.Object super$2$computeIfPresent(java.lang.String, java.util.function.BiFunction); }
-c
オプションを付ければメソッドの中身が逆コンパイルされます。
今回はjavapコマンドによる逆コンパイルで原因を見つけました。