blog

私はJDKシリーズを理解する - パート4 - JavaSPIはどのように動作するか?

\n冒頭の言葉\nこの記事では、JavaのSPIベースのデモの実装と、その実装原理、つまりクラスのソースコード解析に焦点を当てます。\n\nSPIの簡単な説明\nSPIの正式名称はService Pr...

Jul 5, 2020 · 11 min. read
シェア

よくわかるJDKシリーズ - 第4回 - JavaSPIはどのように機能するのか?

オープニング単語

本稿では、Java SPIに基づくデモの実装と、その実装原理、すなわちServiceLoaderクラスのソースコード解析に焦点を当てます。

SPI

SPIの正式名称はService Provider Interfaceで、サービス・プロバイダー・インターフェースと訳されます。これはJavaに組み込まれたサービスプロバイダ発見メカニズムで、環境変数にインターフェースの実装を追加するだけで、プログラムが自動的にクラスをロードして使用することができます。

SPIは拡張性に優れており、フレームワークがルールを設定し、特定のベンダーが実装を提供します。実装方式を変更したい場合は、環境変数に別のベンダーの実装を入れればよく、コードを修正する必要はありません。

簡単なSPIベースのデモです。

Java SPIを実装するための手順

インターフェースの定義

まず第一に、"標準 "と呼ばれるインターフェイスを定義する必要があり、ベンダーは、JDBC標準インターフェイスのMySQLの実装とOracleの実装では、例えば、実装するには、この標準インターフェイスに基づいています。

このデモでは、私が定義した標準的なHelloSpiインターフェイスは、sayメソッドの実装を必要とします。

public interface HelloSpi {
 /**
 * spiインターフェイスのメソッド
 */
 void say();
}

インタフェース実装クラスの作成

public class HelloInChinese implements HelloSpi {
 @Override
 public void say() {
 System.out.println("from HelloInChinese: こんにちは");
 }
}
public class HelloInEnglish implements HelloSpi {
 @Override
 public void say() {
 System.out.println("from HelloInEnglish: hello");
 }
}

クラスインスタンスの実装の作成でServiceLoaderでパラメータレスコンストラクタを介して達成されるため、ここで注意があり、実装クラスは、パラメータレスコンストラクタを持っている必要があり、それ以外の場合は、エラーを報告します、具体的なコードは後で分析されます。

インタフェース完全修飾設定メタファイルの作成

次のステップでは、resourcesディレクトリに META-INF/services フォルダを作成し、 HelloSpiインターフェースの完全修飾名を持つファイル(私の場合はorg.walker.planes.spi)を作成します。

ファイルの中身は、先ほど作成した2つの実装クラスの完全修飾名で、ファイルの各行は実装クラスを表しています。

org.walker.planes.spi.HelloInEnglish
org.walker.planes.spi.HelloInChinese

ServiceLoaderを使用した設定ファイル内のクラスのロード

mainメソッドを持つテストクラスを作成し、ServiceLoader#load(Class)メソッドを呼び出して対応するクラスをロードして実行します。

public class SpiMain {
 public static void main(String[] args) {
 // HelloSpiインターフェイス実装クラスをロードする
 ServiceLoader<HelloSpi> shouts = ServiceLoader.load(HelloSpi.class);
 // sayメソッドを実行する
 for (HelloSpi s : shouts) {
 s.say();
 }
 }
}
実行結果は次のとおりである:
from HelloInEnglish: hello
from HelloInChinese:  

この時点で、Java SPIメカニズムに基づく簡単なデモが実装されました。

この時点で、Java SPIメカニズムに基づくシンプルなデモが完成しました。この例では、設定ファイルを追加する以外に、より重要なクラスがServiceLoaderであり、ServiceLoader#load(Class)メソッドを呼び出すことで、アプリケーションがインターフェース実装クラスをロードし、sayメソッドを実行することがわかります。

JavaSPIが動作する確率は、ServiceLoaderクラスに基づいていることがわかります。では、ServiceLoaderクラスがどのようにインターフェイス実装クラスを読み込んでいるのか分析してみましょう。

ServiceLoader ソースコード解析

上記のJava SPI実装のデモで、そのワークフローはもうお分かりだと思いますが、ServiceLoaderクラスがどのようにインターフェイス・クラスに基づいて実装クラスを見つけ、ロードするかを説明します。

ServiceLoader#load(Class)

ServiceLoader#load(Class)メソッドを呼び出して対応するクラスをロードする、上記の例のステップ4を見てみましょう。

// メインメソッドのロードメソッドの例
public static <S> ServiceLoader<S> load(Class<S> service) {
 // スレッドコンテキストクラスローダーを取得する
 ClassLoader cl = Thread.currentThread().getContextClassLoader();
 // clスレッドコンテキストクラスローダーを介してサービスターゲットクラスをロードするためにオーバーロードメソッドを呼び出す
 return ServiceLoader.load(service, cl);
}
// load オーバーロードされたメソッド
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
 return new ServiceLoader<>(service, loader);
}

とはいえ、JVMの2親委譲メカニズムとスレッド・コンテキスト・クラス・ローダーについて触れておくことは重要です。

JVM 親の委任メカニズム

つまり、クラス・ローダーがクラスをロードするリクエストを受け取ると、クラス・ローダー自身はクラスをロードしようとせず、このリクエストを完了するために親ローダーに委譲します。

一般にクラス・ローダーと呼ばれるものには、スターター・クラス・ローダー、拡張クラス・ローダー、アプリケーション・クラス・ローダー、カスタム・クラス・ローダーがあります。

この記事で理解する必要があることの1つは、ブートストラップ・クラス・ローダーがJavaのコア・クラスをロードする責任があるということです:

  • Javaに位置_HOME\lib
  • Xbootclasspath パラメーターで指定されたパスにあります。
  • VMが認識するクラス・ライブラリ(rt.jar、tools.jarなど

アプリケーション・クラス・ローダーもあり、これはユーザー・クラス・パスのクラスパスで指定されたクラス・ライブラリをロードする役割を果たします。

前のセクションで述べたように、SPI は JDK によって提供される標準インタフェースで、環境変数にその実装が存在するとき、ベンダの実装を使用するようにクラスを自動的にロードできるようにベンダによって実装されます。

JDKは、一般的にコアコードベースと一緒に配置されている標準的なSPIインタフェースを提供することは注目に値する、例えば、JDBCドライバDriver.classインタフェースは、rt.jarパッケージ内にあります。つまり、SPIインターフェイスは、ブートクラスのクラスローダによってロードされ、伝統的な2つの親の委任メカニズムに基づいている場合、実際には、ブートクラスのクラスローダを介してクラスのベンダの実装をロードするには、この時間は、クラスのベンダの実装がクラスパスにあることがわかります、それはアプリケーションのクラスローダによってロードされる必要があり、ブートクラスのクラスローダによってロードすることはできません。

このようなジレンマに基づき、スレッドコンテキストクラスのローディングが生まれました。

スレッドコンテキストクラスローダー

スレッド・コンテキスト・クラス・ローダーは Thread クラスのプロパティで、現在のスレッドのクラス・ローダーをキャッシュするために使用されます。

ServiceLoader#load(Class)メソッドでは、まず現在のスレッドのスレッドコンテキストクラスローダーを取得します。サンプルコードでは、このメソッドを実行しているスレッドがメインスレッドで、メインスレッドをロードするクラスローダーがアプリケーションクラスローダーです。

アプリケーション・クラス・ローダーは、クラスパス上で指定されたクラス・ライブラリをロードする役割を担っており、現在のプロジェクトは確かにクラスパス・パスに属しています。したがって、アプリケーション・クラス・ローダーを使ってSPIインターフェースの実装クラスをロードすれば、正常にロードできます。

本題に戻り、スレッドコンテキストクラスのロードに基づいて、ServiceLoaderクラスがどのようにSPIインターフェイス実装クラスをロードするかを分析し続けましょう。

ソースコードの追跡

load のオーバーロードされたメソッドは、実際には ServiceLoader クラスのインスタンスを作成し、ターゲット インタフェース HelloSpi.class とクラス ローダ AppClassLoader を渡します。

そして、いくつかの操作は、このServiceLoaderクラスのコンストラクタ・メソッドで実行されます。

private ServiceLoader(Class<S> svc, ClassLoader cl) {
 // ターゲットインターフェイスクラスが空であるかどうかをチェックし、それが空の場合、NullPointerExceptionを投げる
 service = Objects.requireNonNull(svc, "Service interface cannot be null");
 
 // clが空の場合、デフォルトでシステムクラスローダーが使用される。
 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
 
 // Java セキュリティ管理に関連し、この記事では詳しく説明していない
 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
 
 // キャッシュプロバイダプロバイダを空にし、すべてのSPIインタフェース実装クラスをリロードする
 reload();
}

ServiceLoader#reload()メソッドは、実際にはServiceLoaderクラスのプライベートなメンバ変数プロバイダに対してクリア処理を行い、遅延ロードされたイテレータLazyIteratorオブジェクトを作成し、引数としてターゲットインタフェースクラスとクラスローダを渡します。

// Cached providers, in instantiation order
// キャッシュプロバイダは、実際には、SPIインターフェイスの実装クラスのオブジェクトを格納することです、実装クラス名のキー、SPIインターフェイスの実装クラスのインスタンスの値
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 現在のレイジーローディングイテレータは、反復の開始時にオブジェクトを作成し、プロバイダにそれらを置く。
private LazyIterator lookupIterator;
 
public void reload() {
 // 空のプロバイダ
 providers.clear();
 // 遅延ロードイテレータを作成する
 lookupIterator = new LazyIterator(service, loader);
}

ここまでで、ServiceLoaderクラスはSPIインターフェイスの実装クラスをロードする準備ができました。プログラムがforループでServiceLoaderオブジェクトを走査するとき、実際にはIteratorインターフェイスのhasNextメソッドを呼び出し、最後に前のステップで述べたLazyIterator#hasNext()メソッドを呼び出します。メソッドを呼び出し、それがtrueを返したら、次のメソッドを呼び出して反復処理を開始します。

public boolean hasNext() {
 // Java アクセス制御コンテキストがNULLの場合、hasNextServiceメソッドが呼び出される。
 if (acc == null) {
 return hasNextService();
 } else {
 PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
 public Boolean run() { return hasNextService(); }
 };
 return AccessController.doPrivileged(action, acc);
 }
}
// Githubhttps://.com/Planeswalker23
private boolean hasNextService() {
 // このメソッドを入力する最初の時間 nextName== null
 if (nextName != null) {
 return true;
 }
 // configs この属性は、URL型のEnumerationオブジェクトを表し、このメソッドを最初に入力したときはNULLである。
 if (configs == null) {
 try {
 // PREFIX = "META-INF/services/",これはまた、我々はMETA-INF/services/ディレクトリにインターフェイスのパス名ファイルを作成する必要がある理由を説明する。
 // service 属性は、LazyIteratorオブジェクトを作成するときに渡されたサービスオブジェクトであり、SPIインターフェースクラスHelloSpiである。.class
 // だからfullName変数は、そのディレクトリに作成されたファイルの名前を表すMETA-INF/services/HelloSpiである。
 String fullName = PREFIX + service.getName();
 // クラスローダーローダーがNULLの場合、設定ファイルはシステムのクラスローダーによってロードされる。
 // nullでない場合は、クラスローダを介して設定ファイルをロードする
 if (loader == null)
 configs = ClassLoader.getSystemResources(fullName);
 else
 configs = loader.getResources(fullName);
 } catch (IOException x) {
 fail(service, "Error locating configuration files", x);
 }
 }
 // このメソッドを最初に入力したとき、保留中のイテレータはnullである
 while ((pending == null) || !pending.hasNext()) {
 // 設定ファイルにデータがない場合は、falseを返す
 if (!configs.hasMoreElements()) {
 return false;
 }
 // 設定ファイル内のフルパスのクラス名は、イテレータに変換され、各行が保留プロパティの要素を表す保留プロパティに格納される。
 pending = parse(service, configs.nextElement());
 }
 // 保留中のイテレータの次の要素にnextNameプロパティを設定する
 nextName = pending.next();
 return true;
}
// Githubhttps://.com/Planeswalker23
private S nextService() {
 // それはhasNextServiceメソッドを呼び出すのは初めてではない、保留中の役割.next() nextNameに割り当てる
 if (!hasNextService())
 throw new NoSuchElementException();
 // ロードされ、インスタンス化される現在のクラス名をマークする。
 String = nextName;
 // 次に、nextNameプロパティをnullに設定する。
 nextName = null;
 Class<?> c = null;
 try {
 // クラス名、false属性、クラスローダーローダーによってクラスを呼び出す。#forName クラスのロードのためのメソッド
 c = Class.forName, false, loader);
 } catch (ClassNotFoundException x) {
 fail(service, "Provider " + + " not found");
 }
 if (!service.isAssignableFrom(c)) {
 fail(service, "Provider " + + " not a subtype");
 }
 try {
 // 正常にロードされたクラスをインスタンス化し、それをHelloSpi型に強制する。
 S p = service.cast(c.newInstance());
 // インスタンス化されたオブジェクトをプロバイダのプロパティに入れる
 providers.put, p);
 // インスタンス化されたオブジェクトを返し、sayメソッドを呼び出してforループで実装されたクラスのロジックを実行する。
 return p;
 } catch (Throwable x) {
 fail(service, "Provider " + + " could not be instantiated", x);
 }
 throw new Error(); // This cannot happen
}

この時点で、ServiceLoaderクラスは、スレッドコンテキストクラスローダーに基づいてJava SPIメカニズムを実装するプロセス全体のソースコード解析を完了しました。

SPIインターフェース実装クラスにパラメータレス・コンストラクタが必要な理由

2.2インタフェースの実装クラスを作るで、実装クラスはパラメータレスのコンストラクタを持たなければならないという記述について触れましたが、その理由を分析してみます。

LazyIterator#nextService()メソッドでロードされたクラスをインスタンス化する際、Class#newInstance()メソッドでインスタンス化を行います。

@CallerSensitive
public T newInstance() {
 // コードのほとんどを省略する...
 
 Class<?>[] empty = {};
 // 参照なしでコンストラクタを取得するためにgetConstructor0を呼び出す、あなたがそれを取得しない場合、それはNoSuchMethodExceptionを報告する。
 final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
 
 // コードのほとんどを省略する...
}
// 参照なしでコンストラクタを取得する クラス#getConstructor0
private Constructor<T> getConstructor0(Class<?>[] parameterTypes, int which) throws NoSuchMethodException {
 // クラスのすべてのコンストラクタを取得し、トラバーサル
 Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
 for (Constructor<T> constructor : constructors) {
 // パラメータなしでコンストラクタを返す
 if (arrayContentsEq(parameterTypes, constructor.getParameterTypes())) {
 return getReflectionFactory().copyConstructor(constructor);
 }
 }
 throw new NoSuchMethodException(getName() + ".<init>" + argumentTypesToString(parameterTypes));
}

上のコードにあるように、クラスをインスタンス化する際、Class#getConstructor0 メソッドを呼び出してコンストラクタを取得しますが、このメソッドは非インストルメント型コンストラクタを取得します。

要約

この記事では、Java SPIメカニズムに基づいた簡単なデモを実装し、スレッドコンテキストクラスローダーに基づいてJava SPIを実装するServiceLoaderクラスのソースコードを分析します。

ここでのまとめのJava SPIの部分については、他の追加があれば教えていただけると幸いです。

お役に立てれば幸いです。

最後になりましたが、この記事は私の個人的な知識ベース「 」に含まれていますので、ご自由にご覧ください。

Read next

実用的なビット操作

different-or-property特性\ndifferent-or特性:ある数nが与えられたとき、n^n=0, n^0=n.となり、different-orは交換と結合の法則を満たします。\n使用法のシナリオ\nアイデア: まず、配列を 1 ビット展開し、同時にそのビットを 0 に代入します。次に、すべての配列の添え字と要素に対して different or を実行し、以下の結果を得ます。

Jul 4, 2020 · 3 min read