ServiceLoaderは、含まれるモジュールがmodule-info.javaの記述子でprovides ... withディレクティブを宣言していない場合、プロバイダーを見つけることに失敗します。Java Platform Module System (JPMS)はデフォルトで強いカプセル化を強制し、ServiceLoader(java.baseに存在)がエクスポートまたはオープンされていないパッケージのクラスにアクセスするのを防ぎます。providesディレクティブは、指定されたプロバイダークラスをインスタンス化するための特権的なリフレクティブアクセスをServiceLoaderに与える契約的宣言として機能し、パッケージをすべてのモジュールにエクスポートすることなく、通常のパッケージアクセス制約を回避します。
コンテキスト: レガシーエンタープライズCRMシステムがJava 8からJava 17に移行されていました。目標は、モノリシックアーキテクチャを異なるドメインにモジュール化することでした:crm-core、crm-email、およびcrm-api。crm-emailモジュールには、crm-apiで定義されたNotificationServiceインターフェースの実装が含まれていました。
移行後、アプリケーションはブートストラップ中にServiceConfigurationErrorをスローしました。これは、EmailNotificationServiceクラスが公開され、JARファイルがモジュールパスに存在しているにもかかわらず発生しました。スタックトレースは、サービスタイプのプロバイダーが見つからないことを示しており、そのため通知サブシステムの初期化に失敗しました。
問題: 開発チームは、実装クラスの公開可視性が十分であると仮定していました。これは、公開クラスがグローバルに可視であったクラスパス時代の仮定を反映していました。しかし、JPMSは、他のモジュールのエクスポートされていないパッケージ内のクラスに対するServiceLoaderのアクセスを防ぎます。crm-emailモジュールは、com.crm.email.internalパッケージをエクスポートしませんでした。重要なことに、module-info.javaにはprovides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationServiceの宣言が欠けていました。したがって、ServiceLoaderはプロバイダーを見つけたりインスタンス化したりできず、モジュールシステムは実装をカプセル化された内部の詳細として扱いました。
検討された解決策:
パッケージをエクスポートする: crm-emailモジュール記述子にexports com.crm.email.internal;を追加。これは、内部実装の詳細を他のすべてのモジュールに公開することになるため、拒否されました。カプセル化に違反し、モジュールシステムが防ごうとした密な結合を生じました。
リフレクション用にパッケージをオープンする: opens com.crm.email.internal;または特にopens com.crm.email.internal to java.base;を使用。これはリフレクティブアクセスを許可しますが、過度に許可的で意味的に不正確であると見なされました。それは、一般的にパッケージが深いリフレクションの対象であることを示しますが、制御されたメカニズムを通じてサービスを提供することには具体的に示していません。
provides ... withディレクティブを使用する: module-info.javaに宣言provides com.crm.api.NotificationService with com.crm.email.internal.EmailNotificationService;を追加。これは、標準的なJPMSの解決策です。サービスの関係を明示的に宣言し、ServiceLoaderにクラスをインスタンス化するために必要なアクセス権を与え、厳密なカプセル化を維持します。
選択された解決策: チームは3番目のオプションを選択しました。このアプローチは、実装コード自体に変更を必要としませんでした。パッケージの内部可視性を保ち、モジュールメタデータにサービス依存関係を明示的にしました。
結果: アプリケーションは、実行時にEmailNotificationServiceを正常にロードしました。モジュール境界は維持され、他のモジュールが内部実装クラスを直接インスタンス化したり依存したりすることを防ぎました。ServiceLoaderは、契約に基づいてサービスを正しく発見し提供することができました。
なぜServiceLoaderはプロバイダークラスが公開のゼロ引数コンストラクターを持つことを要求するのか、またこの制約が破られた場合にどのような特定の例外が現れるのか?
ServiceLoaderは、リフレクションを使用してプロバイダークラスをインスタンス化します。これはClass.getConstructor().newInstance()を使用します。これには公開の引数なしコンストラクターが厳密に要求されます。このコンストラクターが欠如している場合、または公開でない場合、ServiceLoaderはServiceConfigurationErrorをスローします。このエラーは通常、イテレータートラバース中にNoSuchMethodExceptionまたはIllegalAccessExceptionの周りでラップされます。候補者は、他のコンストラクターが定義されている場合、このコンストラクターを明示的に提供する必要があることを見落とすことがよくあります。また、インスタンス化はリクエストが呼ばれるときに遅延して行われ、初期のServiceLoader.load()コールの時点では行われないことに気付いていないことがよくあります。
サービスインターフェースが名前付きモジュール内で定義されているときに、ServiceLoaderメカニズムは無名モジュールにあるプロバイダークラスをどのように扱いますか?
サービスインターフェースが名前付きモジュールに存在しますが実装が無名モジュール(クラスパス)にある場合、ServiceLoaderは依然としてプロバイダーを見つけることができます。無名モジュールはすべての名前付きモジュールを暗黙的に読み取り、すべての名前付きモジュールは暗黙的に無名モジュールを読み取ります。ただし、プロバイダークラスは依然として公開されており、公開の引数なしコンストラクターを持っている必要があります。強いカプセル化がこのシナリオを完全に防ぐという一般的な誤解があります。実際には、無名モジュールは互換性レイヤーとして機能します。無名モジュールのプロバイダーは、無名モジュールを明示的に読み取らない名前付きモジュールのコードからはアクセスできません。これにより、候補者がしばしば考慮しない方向性のアクセス制約が生じます。
ServiceLoader.loadInstalled()メソッドとServiceLoader.load()メソッドを、クラスローダーの委譲とプロバイダーの可視性の観点で区別するのは何ですか?
ServiceLoader.loadInstalled()は、プロバイダーを検索するためにシステムクラスローダー(または最新のJVMバージョンのプラットフォームクラスローダー)を使用します。これは、発見をインストールされた拡張ディレクトリまたはプラットフォームモジュールに制限します。アプリケーションモジュールパスやクラスパスのプロバイダーを明示的に無視します。一方、ServiceLoader.load()は通常、スレッドコンテキストクラスローダーまたは指定されたクラスローダーを使用します。これにより、アプリケーションレベルのプロバイダーを発見できるようになります。候補者はしばしばこれらのメソッドを混同し、アプリケーションプロバイダーが見つからない場合に静かな失敗を引き起こします。これは、loadInstalled()が正しく使用され、標準のロードメソッドのように振る舞うことを期待されるからです。