Java9以上でのGuice使用で警告表示と解決法

2019年6月13日

問題

Java10でしか確認していないのだが、おそらくJava9でも同様だろう。以下のような警告が標準エラーに出る。

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/C:/Users/admin/********/guice-4.2.0.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

あるいは、Guiceの使用でなくとも、Javaシステムのクラス中のprivateフィールドにアクセスしようとして以下のように記述すると、

java.io.ObjectStreamClass.class.getDeclaredField("name").setAccessible(true);

やはり、以下の警告になる。

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by **** (file:/C:/****/) to field java.io.ObjectStreamClass.name
WARNING: Please consider reporting this to the maintainers of ****
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

原因

このようなアクセスをdeep reflectionというのだそうだが、これは将来的に禁止されるのだそうだ。もちろんこれは、「一般的にsetAccessibleが禁止される」のではなく、Java9でモジュール化された(つまりモジュールとして守られた)Javaのシステムライブラリへのアクセスに関して禁止されるものと思われる。

add-opensによる抑制

ウェブ検索してみると、「JVMオプションの–add-opensフラグをつけろ」という書き込みが多数あるのだが、どうやってもうまく行かなかった。それに、このオプションはJava8では当然エラーになってしまう。

つまり、このオプションを使う場合には、あらかじめJavaのバージョンがわかっていなければならない。対象アプリは「Java8以上ならどれでも良い」ということにしたいので、対応するのは面倒だ。

StackOverflowにある解決策

How to hide warning “Illegal reflective access” in java 9 without JVM argument?という書き込みがある。

System.errのリダイレクト

上で回答者が指摘しているように、System.errをリダイレクトしても問題の解決にならない。例えば、以下である。

    PrintStream err = System.err;    
    System.setErr(new PrintStream(new OutputStream() {
      public void write(int b) {
          //DO NOTHING
      }      
    }));  
    // 警告を起こすコード
    System.setErr(err);

なぜなら、JVMのブート時に既にSystem.errに格納されているオブジェクトが警告システムに取得されてしまっているからだそうだ。だから、main()以降で上記のコードを実行しても効果がない。

System.errのクローズ

上記でサンプルとして上げられているのは、System.errをクローズしてしまい。その後に、標準出力にリダイレクトするということ。

public static void disableWarning() {
    System.err.close();
    System.setErr(System.out);
}

もちろんこれでは、他の警告を見逃してしまう。

sun.misc.Unsafeの使用

実際に行ってみてうまくいったものは以下だ。先の書き込みのコードそのものなのだが、ともあれ、以下をmain()直後に呼び出せば避けられるようだ。原理はわからないのだが。

  @SuppressWarnings("restriction")
  public static void ignoreJava9Warning() {
    try {
      Field theUnsafe = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
      theUnsafe.setAccessible(true);
      sun.misc.Unsafe u = (sun.misc.Unsafe) theUnsafe.get(null);
      Class<?> cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
      Field logger = cls.getDeclaredField("logger");
      u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
    } catch (Exception e) {
      // Java9以前では例外
    }
  }

コメントに書いたように、Java8では例外が発生するので無視する。

また、将来のJavaでこの方法が使えなくなった場合にもやはり例外が発生するはずなので、このコードを残しておいても安全と思われる。

Eclipseでのsun.misc.Unsafeのアクセス

上のコードをEclipse上で記述しようとすると、エラーになってしまう。プロジェクトでも設定できるのだが、今回はWindow>Preferenceの方で設定してしまおう。

以下でaccessを入力すれば、目的の場所にすぐたどりつける。これをErrorからWarningに変更しておく。

コンパイラの警告

さらに、これを普通にjavacにかけると警告が発生するのだが、今のところこれを出さないようにする方法はわからない。