Java SPI を深く理解できない人はいないでしょうか?

1. SPI の概要と例

1.1 SPI の概要

SPI は、サードパーティの実装または拡張インターフェイスをサポートするために Java によって提供されるメカニズムであり、正式名は Service Provider Loader です。従来の API 呼び出し元には、インターフェイスの実装を選択する権利がなく、インターフェイス プロバイダーの実装に従って呼び出しを行うことしかできません。SPI プロバイダーを使用すると、実装を呼び出し元に対して外部化できるため、API のスケーラビリティとプラグイン可能性が大幅に向上します。モジュールです。

1.2 Java SPI 実装要素:

SPI インターフェイス: 標準インターフェイスを定義します。これは Java の能力でもあり、さまざまな標準が定義されています。

SPI 実装: 標準インターフェイスを実装します。メーカーごとに異なる実装が可能です。

SPI 構成: META-INF/services ディレクトリに、インターフェイスの完全修飾クラス名という名前のファイルを作成します。内容は、各実装クラスの完全修飾クラス名です。

SPI ロード: ServiceLoader を使用して SPI 構成サービスをロードします。これは SPI の核心であり、その本質はクラス ロードです。

1.3 SPI インターフェースの定義

まずは独自の SPI を作成しましょう。上記の手順に従い、まず標準インターフェイスを定義します。

package com.star.spi.hello;
public interface IHelloService {
    
    
      String sayHi(String name);
}

1.4 SPI インターフェースの実装

以下では 2 つの実装をシミュレートします。1 つは Java 言語をシミュレートし、もう 1 つは Python をシミュレートします。

package com.star.spi.hello;
public class JavaHelloService implements IHelloService {
    
    
    @Override
    public String sayHi(String name) {
    
    
        return String.format("hello %s from java !", name);
    }
}
package com.star.spi.hello;
public class Py3HelloService implements IHelloService {
    
    
    @Override
    public String sayHi(String name) {
    
    
        return String.format("hello %s from python3 !", name);
    }
}

1.5 SPI 構成

Java SPI を使用する場合は、対応するルールに従う必要があります。プロジェクトに META-INF/services ディレクトリを作成します。ファイル名はインターフェイスの完全修飾クラス名です: com.star.spi.hello.IHelloService。内容は実装クラスの完全修飾クラス名です。

com.star.spi.hello.Py3HelloService
com.star.spi.hello.JavaHelloService

1.6 SPI ロード

使い方は比較的簡単で、ServiceLoader はすべてをサイレントに実装します。


package com.star.spi.hello;

import java.util.ServiceLoader;

public class HelloSpi {
    
    
    public static void main(String[] args) {
    
    
        ServiceLoader<IHelloService> serviceLoader = ServiceLoader.load(IHelloService.class);
        serviceLoader.forEach(item -> System.out.println(item.sayHi("spi")));
    }
}

コンソール出力

hello spi from python3 !
hello spi from java !

2. SPIソースコードの解釈

上記ではすでに SPI を実行していますが、SPI を使用するための鍵となるのは ServiceLoader であることがわかります。ServiceLoader は本質的に、外部構成に従ってターゲット実装クラスをロードするクラス ローダーです。その内部動作メカニズムと、それがどのように解析されロードされるかを見てみましょう。

2.1 ServiceLoaderの概要

package java.util;

public final class ServiceLoader<S>
    implements Iterable<S> 
    
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

2.2 ServiceLoader による SPI のロード

2.2.1 静荷重法

まず、現在のスレッド クラス ローダーを使用して、制限されたインターフェイスまたは抽象クラスをロードするエントリ ロード関数を見てみましょう。

public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
                                           ClassLoader loader){
    
    
    return new ServiceLoader<>(service, loader);
}
2.2.2 施工方法

上記のコードの最後のステップで、新しい ServiceLoader オブジェクトが作成されることがわかります。次に、構築メソッドを入力します。リロード メソッドの重要なステップがわかります。まず、キャッシュがクリアされ、次に遅延読み込みが実行されます。

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    
    
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}
public void reload() {
    
    
   providers.clear();
   lookupIterator = new LazyIterator(service, loader);
}
2.2.3 イテレータ

ServiceLoader自体はIteratorインターフェースを実装しており、iterator関数ではプロバイダ内に既にキャッシュがある場合はオブジェクト(LinkedHashMap)のイテレータを直接返し、そうでない場合は上記で作成した遅延読み込みイテレータを使用して繰り返しを行います。


public Iterator<S> iterator() {
    
    
    return new Iterator<S>() {
    
    
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        public boolean hasNext() {
    
    
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }
        public S next() {
    
    
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
        public void remove() {
    
    
            throw new UnsupportedOperationException();
        }
    };
}
2.2.4 LazyIterator

LazyIterator自体もイテレータであり、実際に実装クラスをロードするために使用され、クラスローダでディレクトリを指定して完全修飾クラス名をロードし、リフレクションによってインスタンスを作成し、作成後にキャッシュに入れます。

private boolean hasNextService() {
    
    
    if (nextName != null) {
    
    
        return true;
    }
    if (configs == null) {
    
    
        try {
    
    
            //  目标实现类
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
    
    
            fail(service, "Error locating configuration files", x);
        }
    }
    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);
    } catch (ClassNotFoundException x) {
    
    
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
    
    
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
    
    
        // 实例化实现类
        S p = service.cast(c.newInstance());
        // 放入缓存
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
    
    
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

2.3 クラスローダーの説明

注意深い人なら、上記のクラス ロード プロセスでは、現在のスレッドのクラス ローダーが使用されることに気づいたはずです。ここで、親委任メカニズムにより、SPI はコア ライブラリに配置され、Bootstrap クラス ローダーによってロードされることに注意してください。Bootstrap は SPI 実装クラスをロードできず、実装クラスは App クラス ローダーによってのみロードできます。

3. SPI の一般的な用途

3.1 SPI データベースドライバー

Java は標準のデータ駆動型インターフェイスのみを提供しており、実装はさまざまなデータベース ベンダーによって行われます。標準のドライバー インターフェイスは java.sql.Driver です。mysql ドライバー パッケージを確認してください。

ここに画像の説明を挿入

標準の Java SPI が使用されていることがわかります。そのため、データベース接続を取得するには簡単なコード行だけが必要です。

DriverManager.getConnection("", "", "");
可以看到DriverManager一上来就执行了静态代码块,进行驱动初始化

static {
    
    
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

次に、SPI テクノロジを使用して Driver.class がロードされ、Mysql がスキャンされます。

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
所以在getConnection方法时,已经拿到了已注册的驱动

for(DriverInfo aDriver : registeredDrivers) {
    
    ...}

registeredDrivers は、Mysql ドライバーが SPI によってロードされたときに自身を登録し、メーカーの実装クラスを Java 標準に登録するために完全に連携しました。

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    
    
    public Driver() throws SQLException {
    
    
    }
    static {
    
    
        try {
    
    
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
    
    
            throw new RuntimeException("Can't register driver!");
        }
    }
}

上記は、SPI を使用してデータベース ドライバーをロードするプロセスであり、簡潔、明確、そしてきちんとしています。

3.2 SPIのSpringBoot

SpringBoot での SPI のアプリケーションを見ていきましょう。SpringBoot がシンプルである理由は、多くの自動アセンブリを実行するのに役立つからです。スタートアップクラスから始めましょう

@SpringBootApplication
public class App {
    
    ...}
一个注解就能搞定一切?这只是表面,继续进入该注解看下

@SpringBootConfiguration
@EnableAutoConfiguration
EnableAutoConfiguration中又包含了新的注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    
    }

このうち、@AutoConfigurationPackage は、変更するクラスのパッケージをルート パスとして使用するため、通常、スタートアップ クラスはルート パスに配置されます。

次に、「AutoConfigurationImportSelector」と入力します。このクラスはここで直接インポートされるためです。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    
    
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
        getBeanClassLoader());
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
        + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    
    
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
    
    
        return result;
    } else {
    
    
        HashMap result = new HashMap();

        try {
    
    
            Enumeration urls = classLoader.getResources("META-INF/spring.factories");
            ...
        }
      

キーの読み込みメソッド、読み込みパス META-INF/spring.factories を確認し、キーと値の形式でファイルに保存し、完全修飾クラス名をファイルの下に読み込むことでアセンブリを自動化できます。具体的には、次の方法です。それらはシリーズで動作しますか?

SpringBoot の依存スターターはすべて spring-boot-starter パッケージに依存し、さらに spring-boot-starter パッケージは spring-boot-autoconfigure パッケージに依存します。
ここに画像の説明を挿入

ロードおよびインスタンス化プロセス中に、自動アセンブリ タスクはさまざまな構成条件 @ConditionalXXX に従ってインテリジェントに完了できます。

知らず知らずのうちに書きすぎてしまいました、次の記事からDubboのSPI入門を始めましょう Java SPIの拡張とも言えますが、SPIの考え方は変わっていません。

おすすめ

転載: blog.csdn.net/weixin_43275277/article/details/127860132