プラグ可能なコンポーネントの設計メカニズム—SPI

著者:Jingdong Logistics Kong Xiangdong

1.SPIとは?

SPI の正式名称は Service Provider Interface であり、サービス インターフェイスを提供します; これはサービス検出メカニズムです. SPI の本質は、ファイル内のインターフェイス実装クラスの完全修飾名を構成することであり、サービス ローダーは構成を読み取りますファイルを開き、実装クラスをロードします。このようにして、実装クラスを実行時にインターフェイス用に動的に置き換えることができます。この機能により、SPI メカニズムを介してプログラムに拡張機能を簡単に提供できます。

以下に示すように:

システム設計の各抽象化には、多くの場合、多くの異なる実装スキームがあります. オブジェクト指向設計では、一般に、インターフェイスに基づいてモジュール間でプログラムすることが推奨されます. モジュールの実装はハードコーディングされません. コードに特定の実装クラスが含まれると、プラガブルの引き抜きの原則に違反しています。Java SPIは、特定のインターフェースのサービスの実装を見つけるためのメカニズムを提供します。これは、アセンブリの制御をプログラムの外に移動するIOCの考え方に似ています。これは、モジュール化で特に重要です。SPI は Java が提供するサービス ディスカバリ メカニズムであるというよりは、デカップリングのアイデアであると言った方が適切です。

2. 使用シーンは?

  • データベース ドライバのローディング インターフェイスは、次のようなクラスのローディングを実現します: JDBC は Mysql、Oracle をロードします...
  • ログ ファサード インターフェイスは、次のようなクラス ローディングを実装します。 SLF4J は log4j と logback をサポートします。
  • SPI は Spring で広く使用されており、特に spring-boot での自動構成の実装
  • また、Dubbo は多くの SPI を使用してフレームワークの拡張を実装します. 元の SPI をカプセル化し、ユーザーが Filter インターフェイスを拡張および実装できるようにします.

3. 使い方の紹介

Java SPI を使用するには、次の規則に従う必要があります。

  • サービスプロバイダーがインターフェースの特定の実装を提供する場合、JAR パッケージの META-INF/services ディレクトリに「インターフェース完全修飾名」という名前のファイルを作成する必要があり、その内容は実装の完全修飾名です。クラス;
  • インターフェイス実装クラスが配置されている JAR は、メイン プログラムのクラスパスの下に配置されます。つまり、依存関係が導入されます。
  • メイン プログラムは、java.util.ServiceLoder を介して実装モジュールを動的にロードし、META-INF/services ディレクトリ内のファイルをスキャンして実装クラスの完全修飾名を見つけ、そのクラスを JVM にロードしてインスタンス化します。
  • SPI の実装クラスは、パラメーターのないコンストラクターを運ぶ必要があります。

例:

spi インターフェイス モジュールの定義

定义一组接口:public interface MyDriver 

spi-jd-ドライバー

spi-ali-driver

实现为:public class JdDriver implements MyDriver
  public class AliDriver implements MyDriver 

src/main/resources/ の下に /META-INF/services ディレクトリを作成し、インターフェースにちなんで名付けられたファイル (org.MyDriver ファイル) を追加します。

内容は、com.jd.JdDriver と com.ali.AliDriver のそれぞれに適用される実装クラスです。

spiコア

一般に、これはプラットフォームによって提供されるコア パッケージであり、実装クラスのロードと使用などの戦略が含まれます。ここでは、単純にロジックを実装します: a. 特定の実装が見つからず、例外がスローされる b. 複数の実装が見つかった場合、それらを別々に印刷します

public void invoker(){
    ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
    Iterator<MyDriver> drivers = serviceLoader.iterator();
    boolean isNotFound = true;
    while (drivers.hasNext()){
        isNotFound = false;
        drivers.next().load();
    }
    if(isNotFound){
        throw new RuntimeException("一个驱动实现类都不存在");
    }
}

spi-テスト

public class App 
{
    public static void main( String[] args )
    {
        DriverFactory factory = new DriverFactory();
        factory.invoker();
    }
}

1. spi-core パッケージを導入し、結果を実行する

2. spi-core、spi-jd-driver パッケージの導入

3.引入spi-core,spi-jd-driver,spi-ali-driver

4. 原理分析

たった今、特定の実装クラスを取得した方法がわかりましたか?

わずか 2 行のコード:

ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();

そのため、まず ServiceLoader クラスを見てみましょう。

public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径
 private static final String PREFIX = "META-INF/services/";
    // 代表被加载的类或者接口
    private final Class<S> service;
    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒查找迭代器,真正加载服务的类
    private LazyIterator lookupIterator;
  
 //服务提供者查找的迭代器
    private class LazyIterator
        implements Iterator<S>
    {
 .....
private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
//全限定名:com.xxxx.xxx
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }


        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
//通过反射获取
                c = Class.forName(cn, false, loader);
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            }
        }
........

おおよそのプロセスは次のとおりです。

  • アプリケーションは ServiceLoader.load メソッドを呼び出します

  • アプリケーションがイテレータを介してオブジェクト インスタンスを取得する場合、まずプロバイダ オブジェクトにキャッシュされたサンプル オブジェクトがあるかどうかを判断し、存在する場合はそれを直接返します。

  • 存在しない場合は、クラスの再印刷を実行し、META-INF/services の下の構成ファイルを読み取って、インスタンス化できるすべてのクラスの名前を取得し、構成ファイルを JAR 全体で取得できるようにします。リフレクション メソッド クラスを使用してオブジェクトを読み込みます。 .forName() および use Instance() インスタンス化されたクラス メソッドは、インスタンス化されたクラスを providers オブジェクトにキャッシュし、同期的に返します。

5. まとめ

長所:デカップリング

SPI を使用すると、サードパーティ サービス モジュールのアセンブリ制御ロジックが呼び出し元のビジネス コードから分離され、結合されず、アプリケーションはフレームワーク拡張を有効にし、実際のビジネス状況に応じてフレームワーク コンポーネントを置き換えることができます。

SPI を使用すると、次の方法で実装クラスを取得する必要がなくなります。

  • コードのハードコーディングされたインポート インポート

  • JDBC4.0以前などのリフレクション取得用クラスの完全修飾名を指定Class.forName("com.mysql.jdbc.Driver")

欠点:

ServiceLoader は遅延読み込みと見なすことができますが、トラバーサルを介してのみ取得できます。つまり、インターフェースのすべての実装クラスが一度読み込まれ、インスタンス化されます。一部の実装クラスを使用したくない場合は、それも読み込まれてインスタンス化されるため、無駄が生じます。特定の実装クラスを取得する方法は十分に柔軟ではなく、Iterator の形式でのみ取得でき、特定のパラメーターに従って対応する実装クラスを取得することはできません。

6.コントラスト

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/8564660