1. 概要

Java 9より前の場合、Java Reflection APIには超大国があります。これは、非公開クラスのメンバーに制限なくアクセスできるようにするためのものです。 Java 9以降、モジュラーシステムはReflectionAPIを妥当な範囲に制限したいと考えています。

このチュートリアルでは、モジュールシステムとリフレクションの関係を調べます。

2. モジュラーシステムと反射

リフレクションとモジュールシステムはJavaの歴史の中で異なる時期に登場しますが、信頼できるプラットフォームを構築するために協力する必要があります。

2.1. 基礎となるモデル

Javaモジュールシステムの目標の1つは、強力なカプセル化です。 強力なカプセル化は、主に読みやすさとアクセシビリティで構成されています

  • モジュールの可読性は大まかな概念であり、あるモジュールが別のモジュールに依存しているかどうかに関係します。
  • モジュールのアクセシビリティはより細かい概念であり、あるクラスが別のクラスのフィールドまたはメソッドにアクセスできるかどうかを考慮します。 これは、クラス境界、パッケージ境界、およびモジュール境界によって提供されます。

これら2つのルールの関係は、読みやすさが最優先され、アクセシビリティは読みやすさに基づいて構築されるということです。 たとえば、クラスが public であるがエクスポートされていない場合、読みやすさによりそれ以上の使用が妨げられます。 また、非公開クラスがエクスポートされたパッケージに含まれている場合、読みやすさはその通過を許可しますが、アクセシビリティはそれを拒否します。

読みやすさを向上させるために、モジュール宣言で「 require 」ディレクティブを使用するか、コマンドラインで「 –add-reads 」オプションを指定するか、を呼び出すことができます。 Module.addReadsメソッド。 同様に、境界のカプセル化を解除するには、モジュール宣言で「 opens 」ディレクティブを使用し、コマンドラインで「 –add-opens」オプションを指定します。または、Module.addOpensメソッドを呼び出します。

リフレクションでさえ、読みやすさとアクセシビリティのルールを破ることはできません。 そうしないと、対応するエラーまたは警告が発生します。 注意点の1つ:リフレクションを使用する場合、ランタイムは2つのモジュール間に読みやすさのエッジを自動的に設定します。これは、問題が発生した場合、アクセシビリティが原因であることも意味します。

2.2. さまざまなリフレクションのユースケース

Javaモジュールシステムには、さまざまなモジュールタイプがあります。たとえば、名前付きモジュール、名前なしモジュール、プラットフォーム/システムモジュール、アプリケーションモジュールなどです。

明確にするために、「モジュールシステム」と「システムモジュール」の2つの概念は紛らわしいように聞こえるかもしれません。 それでは、「システムモジュール」の代わりに「プラットフォームモジュール」の概念を使用しましょう。

上記のモジュールタイプを考慮すると、異なるモジュールタイプ間にはかなりの数の組み合わせが存在します。 一般に、名前のないモジュールは、自動モジュールを除いて、名前の付いたモジュールで読み取ることはできません。 3つの典型的なシナリオだけを調べてみましょう。

上の図で、ディープリフレクションとは、Reflection APIを使用して、 setAccessible(flag)メソッドを呼び出してクラスの非公開メンバーにアクセスすることを意味します。 リフレクションを使用して別の名前付きモジュールから名前付きモジュールにアクセスすると、IllegalAccessExceptionまたはInaccessibleObjectExceptionが発生します。 同様に、リフレクションを使用して、名前のないモジュールからモジュールの名前の付いたアプリケーションにアクセスすると、同じエラーが発生します。

ただし、リフレクションを使用して名前のないモジュールからプラットフォームモジュールにアクセスすると、IllegalAccessExceptionまたは警告が表示されます。 また、警告メッセージは、問題が発生している場所を見つけて、さらに対処するのに役立ちます。

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

上記の警告メッセージフォームで、 $ PERPETRATOR は反映されたクラス情報を表し、 $VICTIMは反映されたクラス情報を表します。 そして、このメッセージは緩和された強力なカプセル化に起因します。

2.3. リラックスした強力なカプセル化

Java 9より前は、多くのサードパーティライブラリがリフレクションAPIを利用して魔法の仕事をしています。 ただし、モジュールシステムの強力なカプセル化ルールは、そのコードのほとんど、特にJDK内部APIにアクセスするためにディープリフレクションを使用するコードを無効にします。 それは望ましくありません。 Java8からJava9のモジュラーシステムへのスムーズな移行のために、妥協点があります。リラックスした強力なカプセル化です。

緩和された強力なカプセル化により、実行時の動作を制御するためのランチャーオプション –illegal-accessが提供されます。 –illegal-access オプションは、リフレクションを使用して名前のないモジュールからプラットフォームモジュールにアクセスする場合にのみ機能することに注意してください。 それ以外の場合、このオプションは効果がありません。

–illegal-access オプションには、次の4つの具体的な値があります。

  • permit :プラットフォームモジュールの各パッケージを名前のないモジュールに対して開き、警告メッセージを1回だけ表示します
  • warn :「 permit 」と同じですが、不正なリフレクティブアクセス操作ごとに警告メッセージが表示されます
  • debug :「 warn 」と同じで、対応するスタックトレースも出力します
  • deny :すべての不正なリフレクティブアクセス操作を無効にします

Java 9以降、 –illegal-access =permitがデフォルトのモードです。 他のモードを使用するには、コマンドラインで次のオプションを指定できます。

java --illegal-access=deny com.baeldung.module.unnamed.Main

Java 16では、 –illegal-access =denyがデフォルトのモードになります。 Java 17以降、–illegal-accessオプションは完全に削除されました

3. リフレクションの不正アクセスを修正する方法

Javaモジュールシステムでは、深い反射を可能にするためにパッケージを開く必要があります。

3.1. モジュール宣言で

コードの作成者であれば、module-info。javaでパッケージを開くことができます。

module baeldung.reflected {
    opens com.baeldung.reflected.opened;
}

さらに注意するために、修飾されたopensを使用できます。

module baeldung.reflected {
    opens com.baeldung.reflected.internal to baeldung.intermedium;
}

既存のコードをモジュラーシステムに移行する場合、便宜上、モジュール全体を開くことができます。

open module baeldung.reflected {
    // don't use opens directive
}

オープンモジュールは内部オープンディレクティブを許可しないことに注意してください。

3.2. コマンドラインで

コードの作成者でない場合は、コマンドラインで –add-opensオプションを使用できます。

--add-opens java.base/java.lang=baeldung.reflecting.named

また、名前のないすべてのモジュールにオープンを追加するには、ALL-UNNAMEDを使用できます。

java --add-opens java.base/java.lang=ALL-UNNAMED

3.3. 実行時

実行時にオープンを追加するには、Module.addOpensメソッドを使用できます。

srcModule.addOpens("com.baeldung.reflected.internal", targetModule);

上記のコードスニペットでは、srcModuleは「com.baeldung.reflected.internal」パッケージをtargetModuleに開きます。

注意すべき点の1つ:Module.addOpensメソッドは呼び出し元に依存します。 このメソッドは、変更されるモジュールから、オープンアクセスを許可したモジュールから、または名前のないモジュールから呼び出す場合にのみ成功します。 そうしないと、IllegalCallerExceptionが発生します。

ターゲットモジュールにオープンを追加する別の方法は、Javaエージェントを使用することです。 java.instrument モジュールでは、InstrumentationクラスがJava9以降の新しいredefineModuleメソッドを追加しました。 このメソッドを使用して、読み取り、エクスポート、オープン、使用、および提供を追加できます。

void redefineModule(Instrumentation inst, Module src, Module target) {
    // prepare extra reads
    Set<Module> extraReads = Collections.singleton(target);

    // prepare extra exports
    Set<String> packages = src.getPackages();
    Map<String, Set<Module>> extraExports = new HashMap<>();
    for (String pkg : packages) {
        extraExports.put(pkg, extraReads);
    }

    // prepare extra opens
    Map<String, Set<Module>> extraOpens = new HashMap<>();
    for (String pkg : packages) {
        extraOpens.put(pkg, extraReads);
    }

    // prepare extra uses
    Set<Class<?>> extraUses = Collections.emptySet();

    // prepare extra provides
    Map<Class<?>, List<Class<?>>> extraProvides = Collections.emptyMap();

    // redefine module
    inst.redefineModule(src, extraReads, extraExports, extraOpens, extraUses, extraProvides);
}

上記のコードでは、最初に target モジュールを使用して、 extraReads extraExports 、およびextraOpens変数を作成します。 次に、Instrumentation.redefineModuleメソッドを呼び出します。 その結果、srcモジュールはtargetモジュールにアクセスできるようになります。

4. 結論

このチュートリアルでは、最初にモジュールシステムの読みやすさとアクセシビリティを紹介しました。 次に、さまざまな違法なリフレクティブアクセスのユースケースと、緩和された強力なカプセル化がJava8からJava9モジュールシステムへの移行にどのように役立つかを調べました。 最後に、違法なリフレクティブアクセスを解決するためのさまざまなアプローチを提供しました。

いつものように、このチュートリアルのソースコードはGitHubにあります。