歴史。 Java 9以前は、リフレクションはsetAccessible(true)を介してアクセス修飾子を恣意的に回避でき、カプセル化を任意に破壊することができました。**Javaプラットフォームモジュールシステム(JPMS)**の導入により、モジュールは内部パッケージへの深いリフレクティブアクセスを明示的に許可する必要がある強いカプセル化がデフォルトで確立されました。
問題。 あるモジュール内のコードが、他のモジュールのパッケージにある非公開フィールドにアクセスしようとすると、JVMは厳格なアクセシビリティチェックを行います。この検証により、ターゲットパッケージが呼び出しモジュールに対して明示的にオープンされていることが保証されます。この許可がない場合、JVMはInaccessibleObjectException(またはレガシーなリフレクションの場合はIllegalAccessException)をスローします。これは、SecurityManagerがインストールされているかどうかや、フィールドがVarHandleを介してアクセスされるかどうかに関係ありません。
解決策。 モジュールはmodule-info.java内でopens package.name [to specific.module];を宣言するか、アプリケーションを--add-opens source.module/package.name=target.moduleフラグで起動する必要があります。このディレクティブは、モジュールの内部アクセシビリティグラフを動的に変更し、ターゲットモジュールをそのパッケージのプライベートメンバーに対して深いリフレクションを行う権限を持つモジュールのセットに追加します。
// モジュール: app.core (module-info.java) module app.core { // パッケージ com.app.internal はオープンされていない exports com.app.api; } // モジュール: framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // --add-opensなしでInaccessibleObjectExceptionをスローします VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
ある開発チームは、モノリシックなSpringベースのアプリケーションをJavaモジュールシステムに移行し、コードベースをコアビジネスロジックモジュール(app.core)と別の依存性注入フレームワークモジュール(framework.inject)に分割しました。デプロイ後すぐに、アプリケーションは、フレームワークがapp.coreの内部com.app.internalパッケージ内にあるプライベートフィールドに設定値を注入しようとしたときにInaccessibleObjectExceptionでクラッシュしました。
三つの潜在的なアーキテクチャ的解決策が評価されました。最初のアプローチは、すべての注入可能なクラスをapp.core内の公開パッケージに移動することを含んでいました。これは即時のアクセス違反を解決することにはなりますが、内部の実装詳細をすべての他のモジュールにさらすこととなり、基礎的なカプセル化の原則に根本的に違反し、メンテナンスの負担を増加させ、将来のセキュリティ監査のための攻撃面を広げてしまいます。二つ目の解決策は、--add-exports JVM引数を使用してフレームワークモジュールに内部パッケージを公開することを提案しました。しかし、--add-exportsは公開型にコンパイル時および実行時の可視性を付与する一方で、プライベートメンバーに対する深いリフレクションを明示的に許可しないため、プライベート状態の変更を必要とするSpringのフィールド注入メカニズムには不十分です。三つ目のオプションは、ターゲットコマンドライン引数--add-opens app.core/com.app.internal=framework.injectを使用しました。このアプローチは、他のすべてのモジュールに対する厳格なソースレベルのカプセル化を維持しつつ、必要な権限をフレームワークに対してのみ明示的に付与し、特定の内部パッケージで深いリフレクションを行うことを可能にしました。
チームは最終的に三つ目のオプションを選択し、必要な--add-opensディレクティブをデプロイメントスクリプトとDocker構成に文書化しました。この解決策は、開発中にモジュールシステムの整合性を維持しながら、フレームワークが正しく機能することを可能にし、明示的に制御されたアクセス境界で成功裏な移行を実現しました。
setAccessible(true)は、異なるモジュールからアクセスされた場合、公開されたパッケージ内のプライベートフィールドに対してなぜ失敗するのか(SecurityManagerが存在しないにもかかわらず)?
候補者は、パッケージのエクスポートをオープンさと混同することがよくあります。exportsディレクティブは、標準的なコンパイルおよび呼び出しのために公開された型とメンバーをアクセス可能にしますが、Java言語のアクセスチェックを抑制するために必要なReflectPermissionを付与することはありません。JPMSの強いカプセル化は、SecurityManagerとは独立して動作し、JVMのアクセス制御メカニズムによって直接強制されます。非公開メンバーに対してsetAccessible(true)を有効にするには、パッケージを明示的にopenとして宣言する必要があるか、モジュール全体をopen moduleとして宣言する必要があります。
MethodHandles.Lookupのキャプチャメカニズムは、モジュール間のアクセシビリティにどのように影響を与え、MethodHandles.lookup().in(targetClass)を呼び出すことがルックアップの能力を低下させる可能性があるのはなぜですか?
Lookupオブジェクトは、その作成者のモジュールおよびパッケージコンテキストのアクセス権限をカプセル化します。Lookup.in(targetClass)が呼び出されると、JVMはルックアップの権限をターゲットクラスのモジュールに基づいて再評価します。ターゲットクラスが、ルックアップのモジュールに対してオープンされていない別のモジュールにある場合、ルックアップは「格下げ」され、PUBLICモードに移行し、PRIVATEおよびMODULEのアクセス権限が剥奪されます。他のモジュールでの完全なアクセス権を維持するためには、ターゲットモジュールがルックアップのモジュールに対してパッケージを明示的にオープンする必要があるか、または、コードがprivateLookupInを利用し、ターゲットクラスが同じモジュール内にあるか、モジュールグラフを介してアクセス可能である必要があります。
--add-exportsと--add-opensの根本的な違いは何であり、なぜ前者は依存性注入中にIllegalAccessExceptionを引き起こすのか(コンパイルが成功しても)?
--add-exportsフラグは、モジュールのエクスポートリストにパッケージを追加し、ターゲットモジュールがコンパイル時および実行時に公開型にアクセスできるようにします。しかし、このディレクティブは、深いリフレクションを制御する「オープン」セットを変更するものではありません。Java言語仕様は、読みやすさ(エクスポート)を反射可能性(オープン)から厳密に分離しています。依存性注入フレームワークは、リフレクションまたはVarHandleを介してプライベートフィールドを操作するために後者を必要とします。したがって、--add-exportsはコンパイラを満たし、メソッド呼び出しを行うことを許可しますが、プライベート状態を変更しようとする実行時の試みは今なお失敗します。--add-opensのみが、深いリフレクションにアクセス可能なパッケージのセットにパッケージを追加し、フレームワークがプライベートフィールドの値を変更することを許可します。