Java のクラスローダー、親委任、SPI メカニズム

参考:
Java 親委任モデル: なぜ親委任なのか? それを壊す方法は?どこが壊れているのでしょうか?
[Code Pipixia] では、親の委任メカニズム [原理、長所、短所] を検討し、それを打破する方法を説明します。
JDK/Dubbo/Spring の 3 つの SPI メカニズムのうち、どれが優れていますか?


序文

ビジネス開発を行うときにクラス ローダーに触れる機会はほとんどありませんが、Tomcat や Spring などのオープン ソース プロジェクトについて詳しく学びたい場合、または基盤となるアーキテクチャの開発に従事したい場合は、クラス ローダーを理解し、さらには精通していることが不可欠です。クラスローディングの原則。

Javaのクラスローダーとは何ですか? 保護者代表団とは何ですか? なぜ保護者代表団なのか?それを壊す方法は?私はこれらの概念についてはある程度理解していますし、面接用にこれらの知識ポイントを暗記したこともありますが、さらに詳しく説明すると、ほとんど知識がありません。


1. クラスローダー

クラスローダーは、その名前が示すように、Java バイトコードを java.lang.Class インスタンスにロードできるツールです。このプロセスには、バイト配列の読み取り、検証、解析、初期化などが含まれます。さらに、画像ファイルや構成ファイルなどのリソースをロードすることもできます。

クラスローダーの機能:

  • 動的ロードでは、プログラムの実行開始時にロードする必要はありません。代わりに、プログラムの実行中にオンデマンドで動的にロードされます。圧縮された jar、war、ネットワーク、ローカル ファイルなど、バイトコードのソースは多数あります。クラス ローダーの動的ロード機能は、ホット デプロイメントとホット ロードを強力にサポートします。
  • クラスローダーがクラスをロードすると、ロードするプログラムで別のクラスローダーが明示的に指定されていない限り、このクラスが依存する他のすべてのクラスと参照がこのクラスローダーによってロードされます。したがって、親の委任を解除しても、拡張クラス ローダーより上の順序を解除することはできません。

クラスの一意性は、クラスをロードするクラス ローダーとクラス自体 (クラスの完全修飾名 + 一意の識別子としてのクラス ローダーのインスタンス ID) によって決まります。2 つのクラス (クラス オブジェクト 、 、およびキーワードなど​equals()含む) が等しいかどうかの比較は、2 つのクラスが同じクラス ローダーによってロードされた場合にのみ意味があります。そうでない場合は、これら 2 つのクラスが同じクラス ファイルから生成され、次のクラス ファイルによってロードされた場合でも意味があります。同じ仮想マシンをロードするクラス ローダーが異なる限り、2 つのクラスは等価であってはなりません。​​isAssignableFrom()​​isInstance()​​​​​instanceof​​

実装の観点から、クラス ローダーは 2 つのタイプに分類できます: 1 つはC++ 言語で実装され、仮想マシン自体の一部であるスタートアップ クラス ローダー、もう 1 つは拡張クラス ローダーを含むjava.lang.ClassLoader​​からアプリケーション クラス ローダー、およびカスタム クラス ローダー

  • ​​<JAVA_HOME>\libブートストラップ クラスローダー (Bootstrap ClassLoader):ディレクトリ内のパス、またはパラメーターで指定されたパスのロードを担当し​​-Xbootclasspath、仮想マシンによって認識されます ( rt.jar など、一貫性のないクラス ライブラリのファイル名によってのみ認識されます)。 name は lib ディレクトリに配置されてもロードされません) クラス ライブラリは仮想マシンのメモリにロードされます。スタートアップ クラス ローダーは Java プログラムから直接参照できませんが、ユーザーがカスタム クラス ローダーを作成するときに、Bootstrap ClassLoader をその親として設定したい場合は、null を直接設定できます。

  • ​​<JAVA_HOME>\lib\ext​​​拡張クラスローダー:ディレクトリまたはjava.ext.dirsシステム変数で指定されたパスにあるすべてのクラス ライブラリをロードします。このクラスローダーは​​sun.misc.Launcher$ExtClassLoader​​​によって実装されています。拡張クラスローダーは起動クラスローダーによってロードされ、その親クラスローダーは起動クラスローダー、つまりparent=nullです。

  • ​​sun.misc.Launcher$App-ClassLoaderアプリケーション クラスローダー:によって実装される、ユーザー クラス パス (ClassPath) で指定されたクラス ライブラリのロードを担当します。​​java.lang.ClassLoader​​​開発者はの​​getSystemClassLoader()​​メソッドを通じてアプリケーション クラス ローダーを直接取得できるため、システム クラス ローダーと呼ぶこともできます。アプリケーション クラス ローダーも起動クラス ローダーによってロードされますが、その親クラス ローダーは拡張クラス ローダーです。アプリケーションでは、通常、システム クラス ローダーがデフォルトのクラス ローダーです。


2. 保護者の委任メカニズム

1. 親委任メカニズムの概要

JVM は、起動時にすべての .class ファイルをロードするのではなく、実行中にプログラムがクラスを使用するときにのみクラスをロードします起動クラス ローダーを除き、他のすべてのクラス ローダーは抽象クラス ClassLoader を継承する必要があります。この抽象クラスは 3 つの主要なメソッドを定義します。それらの機能と関係を理解することが非常に重要です。

public abstract class ClassLoader {
    
    
    //每个类加载器都有个父加载器
    private final ClassLoader parent;

    public Class<?> loadClass(String name) {
    
    
        //查找一下这个类是不是已经加载过了
        Class<?> c = findLoadedClass(name);

        //如果没有加载过
        if (c == null) {
    
    
          //先委派给父加载器去加载,注意这是个递归调用
          if (parent != null) {
    
    
              c = parent.loadClass(name);
          } else {
    
    
              // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加载器没加载成功,调用自己的findClass去加载
        if (c == null) {
    
    
            c = findClass(name);
        }

        return c;
    }

    protected Class<?> findClass(String name){
    
    
        // 1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
        // ...

        // 2. 调用defineClass将字节数组转成Class对象
        return defineClass(buf, off, len)}

    // 将字节码数组解析成一个Class对象,用native方法实现
    protected final Class<?> defineClass(byte[] b, int off, int len){
    
    
        // ...
    }
}

上記のコードからいくつかの重要な情報を取得できます。

  1. JVM クラス ローダーは階層構造になっており、親子関係があります。この関係は継承や保守ではなく、組み合わせです。各クラス ローダーは、親ローダーを指す親フィールドを保持します。
  2. defineClass​このメソッドの役割は、ネイティブ メソッドを呼び出して、Java クラスのバイトコードを解析して Class オブジェクトにすることです。
  3. findClass​​​このメソッドの主な役割は、.class ファイルを検索し、.class ファイルをメモリに読み込んでバイトコード配列を取得し、defineClassメソッドを。サブクラスは を実装する必要がありますfindClass
  4. loadClass​​このメソッドの主な役割は、親委任メカニズムを実装することです。最初にこのクラスがロードされているかどうかを確認し、ロードされている場合は直接戻ります。ロードされていない場合は、ロードのために親ローダーに委任されます。これは再帰呼び出しであり、委任されています。最上位のクラスローダー (スタートアップクラスローダー) がクラスをロードできない場合、クラスはサブクラスローダーにレイヤーごとに委譲されます

ここに画像の説明を挿入します

2. 親の委任メカニズムの役割

親委任により、クラス ローダー、ボトムアップ委任、およびトップダウン ローディングにより、各クラスが各クラス ローダー内で同じクラスになることが保証されます。

非常に明白な目的:​​java​​​公式クラス ライブラリ​​<JAVA_HOME>\lib​​​と拡張クラス ライブラリの読み込みセキュリティを確保し​​<JAVA_HOME>\lib\ext​​、開発者によって上書きされないようにすることです。たとえば​​java.lang.Object​​​、クラスがそこに格納されます​​rt.jar​​。どのクラス ローダーがこのクラスをロードしようとしても、最終的にはロードのために起動クラス ローダーに委任されます。したがって、オブジェクト クラスは、さまざまなクラス ローダー環境で同じクラスになります。プログラム。

開発者が独自のオープンソース フレームワークを開発する場合、クラス ローダーをカスタマイズし、親委任モデルを使用して、独自のフレームワークによってロードされる必要があるクラスがアプリケーションによって上書きされないように保護することもできます。

その利点は次のように要約されます。

  1. クラスの繰り返しロードを避ける
  2. プログラムのセキュリティを保護し、コア API が自由に改ざんされるのを防ぎます。

短所: シナリオによっては、親の委任システムが制限的すぎるため、目標を達成するために親の委任メカニズムを破る必要がある場合があります。例: SPI メカニズム

3. 親の委任とスレッド コンテキスト クラス ローダーの制限

親委任メカニズムの制限を紹介するために、例としてデータベース接続を作成する JDBC を取り上げます。

次のコードを実行して接続を作成する場合、DriverManagerクラスを初期化する必要があります

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "root");

クラスを初期化するときDriverManager、次のコード行が実行され、インターフェイスを実装するclasspath次のすべての実装クラスがロードされます。Driver

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

ここで問題が発生します。以下に
あるクラスはスタートアップ クラス ローダーによってロードされるため、ロード中に上記のコードが検出されると、すべてのドライバー実装クラス (SPI) をロードしようとしますが、これらの実装クラスは基本的にサードパーティです。ただし、サードパーティのクラスはスタートアップ クラス ローダーによってロードできません。rt.jarjava.sql.DriverManager

この問題を解決するにはどうすればよいでしょうか? JDBC は、アプリケーション クラス ローダー (スレッド コンテキスト ローダー、デフォルトでは AppClassLoader) を導入することで、
の委任の原則を破ります。ThreadContextClassLoaderAppClassLoader

ServiceLoader.load()成し遂げる:

public static <S> ServiceLoader<S> load(Class<S> service) {
    
    
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

スレッド コンテキスト クラス ローダーは、実際にはクラス ローダー配信メカニズムです。java.lang.Thread#setContextClassLoader メソッドを使用してスレッドのコンテキスト クラス ローダーを設定できます。このクラス ローダーは、その後のスレッドの実行中に取得して使用できます (java.lang.Thread#getContextClassLoader))。

スレッドの作成時にコンテキスト クラス ローダーが設定されていない場合は、親スレッドから取得されます (parent = currentThread())。アプリケーションのグローバル スコープに設定されていない場合、アプリケーション クラス ローダーは次のようにロードされます。デフォルトのデバイス。

スレッド コンテキスト クラス ローダーは、親の委任の破棄を容易にしているようです。

典型的な例は JNDI サービスです。JNDI は現在 Java の標準サービスです。そのコードはスタートアップ クラス ローダー (JDK 1.3 に組み込まれている rt.jar) によってロードされますが、JNDI の目的はリソースを一元化することです。管理と検索のためにを実行するには、独立ベンダーによって実装され、アプリケーションの ClassPath の下にデプロイされた JNDI インターフェイス プロバイダー (SPI、Service Provider Interface) のコードを呼び出す必要がありますが、起動クラス ローダーが ClassPath の下にクラスをロードすることは不可能です。

ただし、スレッド コンテキスト クラス ローダーを使用すると、処理が簡単になります。JNDI サービスは、スレッド コンテキスト クラス ローダーを使用して、必要な SPI コードをロードします。つまり、親クラス ローダーは、子クラス ローダーにクラス ロード アクションを完了するよう要求します。これは、実際、これは親委任モデルの階層構造を開き、クラス ローダーを逆に使用するため、親委任モデルの一般原則に違反しますが、これについては何もできません。

Java の SPI に関連するすべてのロード アクション (JNDI、JDBC、JCE、JAXB、JBI など) は基本的にこのメソッドを使用します。

周志明著「Java 仮想マシンの徹底理解」より抜粋

Tomcat が親の委任メカニズムを破壊する例:

Tomcat は Web コンテナであるため、Web コンテナには複数のアプリケーションをデプロイする必要がある場合があります。異なるアプリケーションは、同じサードパーティ クラス ライブラリの異なるバージョンに依存する場合がありますが、クラス ライブラリの異なるバージョンのクラスの完全パス名は同じである場合があります。

デフォルトの親委任クラスロードメカニズムが使用されている場合、複数の同一クラスをロードすることはできません。したがって、Tomcat は、親の委任の原則を破棄し、分離メカニズムを提供し、Web コンテナごとに個別の WebAppClassLoader ローダーを提供します。

Tomcat のクラス ロード メカニズム: 分離を実現するために、Web アプリケーションによって定義されたクラスが最初にロードされます。したがって、親委任契約は守られません。各アプリケーションには独自のクラス ローダー - WebAppClassLoader があり、クラス ファイルをロードします。独自のディレクトリがあり、時間が来るとロードのために CommonClassLoader に渡されますが、これは親の委任とはまったく逆です。

4. 親の委任メカニズムがどのように壊れているか

親の委任メカニズムが壊れる方法は 2 つあります。

  1. スレッドコンテキストクラスローダー。ThreadContextClassLoader を使用して、上位クラス ローダーでロードできないクラスをロードします (上記のデータベース接続の JDBC 作成で説明した)。
  2. カスタムクラスローダー。親の委任メカニズムを破壊するこの方法を紹介しましょう。

クラスローダをカスタマイズしたい場合はClassLoaderを継承して書き換える必要があり​​findClass​​​、親から委任されたクラスロード順序を破棄したい場合はクラスローダを書き換える必要があります​​loadClass​​​以下は、loadClass をオーバーライドして親の委任を解除するカスタム クラス ローダーです。

package co.dreampointer.test.classloader;

import java.io.*;

public class MyClassLoader extends ClassLoader {
    
    
    public MyClassLoader(ClassLoader parent) {
    
    
        super(parent);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    
    
        // 1.找到ExtClassLoader,并首先委派给它加载
        // 为什么?
        // 双亲委派的破坏只能发生在"AppClassLoader"及其以下的加载委派顺序,"ExtClassLoader"上面的双亲委派是不能破坏的!
        // 因为任何类都是继承自超类"java.lang.Object",而加载一个类时,也会加载继承的类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。
        ClassLoader classLoader = getSystemClassLoader();
        while (classLoader.getParent() != null) {
    
    
            classLoader = classLoader.getParent();
        }
        Class<?> clazz = null;
        try {
    
    
            clazz = classLoader.loadClass(name);
        } catch (ClassNotFoundException ignored) {
    
    
        }
        if (clazz != null) {
    
    
            return clazz;
        }

        // 2.自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
    
    
            return clazz;
        }

        // 3.自己加载不了,再调用父类loadClass,保持双亲委派模式
        return super.loadClass(name);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        // 1.获取class文件二进制字节数组
        byte[] data;
        try {
    
    
            ByteArrayOutputStream baOS = new ByteArrayOutputStream();
            // 加载class文件内容到字节数组
            FileInputStream fIS = new FileInputStream("test/target/classes/co/dreampointer/test/classloader/MyTarget.class");
            byte[] bytes = new byte[1024];

            int len;
            while ((len = fIS.read(bytes)) != -1) {
    
    
                baOS.write(bytes, 0, len);
            }
            data = baOS.toByteArray();
        } catch (IOException e) {
    
    
            e.printStackTrace();
            return null;
        }

        // 2.字节码数组加载到 JVM 的方法区,
        // 并在 JVM 的堆区建立一个java.lang.Class对象的实例
        // 用来封装 Java 类相关的数据和方法
        return this.defineClass(name, data, 0, data.length);
    }
}

ここに画像の説明を挿入します

テストプログラム:

package co.dreampointer.test.classloader;

public class Main {
    
    
    public static void main(String[] args) throws ClassNotFoundException {
    
    
        // 初始化MyClassLoader
        // 将加载MyClassLoader类的类加载器设置为MyClassLoader的parent
        MyClassLoader myClassLoader = new MyClassLoader(MyClassLoader.class.getClassLoader());

        System.out.println("MyClassLoader的父类加载器:" + myClassLoader.getParent());
        // 加载 MyTarget
        Class<MyTarget> clazz = (Class<MyTarget>) myClassLoader.loadClass("co.dreampointer.test.classloader.MyTarget");
        System.out.println("MyTarget的类加载器:" + clazz.getClassLoader());
    }
}

//控制台打印
MyClassLoader的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
MyTarget的类加载器:co.dreampointer.test.classloader.MyClassLoader@5b1d2887

親委任を破棄する位置に注意してください: カスタム クラス ロード メカニズムは最初にロードのために ExtClassLoader に委任され、次に ExtClassLoader は BootstrapClassLoader に委任されます。どちらもロードできない場合は、MyClassLoader がロードするクラス ローダをカスタマイズします。MyClassLoader がロードできない場合は、それは AppClassLoader に渡されます。カスタム クラス ローダーに直接ロードさせることができないのはなぜでしょうか?

親委任の破棄は AppClassLoader 以下の読み込み委任シーケンスでのみ発生するため、ExtClassLoader より上の親委任は破棄できません。

根本原因: すべてのクラスは、スーパー クラス java.lang.Object から継承します。クラスをロードすると、継承されたクラスもロードされます。クラス内で他のクラスも参照されている場合、それらはオンデマンドでロードされ、クラス ローダーは現在のクラスをロードするすべてのクラスローダー。

たとえば、MyTarget クラスは Object を暗黙的に継承するだけであり、カスタム クラス ローダー MyClassLoader が MyTarget をロードすると、Object もロードされます。loadClass が MyClassLoader の findClass を直接呼び出すと、エラー java.lang.SecurityException: Prohibited package name: java.lang が報告されます。

セキュリティ上の理由から、Java では、BootStrapClassLOader 以外のクラス ローダーが公式の Java. ディレクトリにクラス ライブラリをロードすることを許可していません。defineClass ソース コードでは、最終的にネイティブ メソッドdefineClass1 が呼び出されて Class オブジェクトが取得されますが、その前に、クラスの完全修飾名が java. で始まるかどうかがチェックされます。(Java のクラスローディングを完全にバイパスしたい場合は、defineClass を自分で実装する必要があります。ただし、個人的な能力が限られているため、defineClass の書き換えについて詳しく調べたことがありません。通常、ExtClassLoader は破棄されません。親の委任 (Java が使用されなくなった場合を除く)

defineClass のソースコードは次のとおりです。

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    
    
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}
private ProtectionDomain preDefineClass(String name,
                                        ProtectionDomain pd)
{
    
    
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
    // relies on the fact that spoofing is impossible if a class has a name
    // of the form "java.*"
    if ((name != null) && name.startsWith("java.")) {
    
    
        throw new SecurityException
            ("Prohibited package name: " +
             name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
    
    
        pd = defaultDomain;
    }

    if (name != null) checkCerts(name, pd.getCodeSource());

    return pd;
}

カスタム クラス ローダーを介して親の委任を破棄するケースは、日常の開発で非常に一般的です。たとえば、TomcatWeb アプリケーション間の読み込み分離を実現するために、クラス ローダーがカスタマイズされます。各コンテキストは Web アプリケーションを表し、webappClassLoader を持ちます。別の例としては、ホット デプロイメントとホット ロードの実装にはカスタム クラス ローダーが必要です。破棄する場所はAppClassLoaderをスキップすることです。

5. まとめ

  1. Javaにおけるクラスロードは、.classファイルのバイナリバイトコード配列を取得してJVMのメソッド領域にロードし、JVM内にヒープ領域を作成してJavaクラスに関連するデータとメソッドをカプセル化します。 . java.lang.Class オブジェクト インスタンス。

  2. Java にはデフォルトで 3 つのクラス ローダー、起動クラス ローダー (BootstrapClassLoader)、拡張クラス ローダー (ExtClassLoader)、およびアプリケーション クラス ローダー (システム クラス ローダーとも呼ばれます) (AppClassLoader) があります。クラスローダ間には親子関係があり、この関係は継承関係ではなく組み合わせ関係となります。parent=null の場合、その親は起動クラス ローダーです。起動クラスローダは、Java プログラムから直接参照することはできません。

  3. 親の委任は、クラス ローダー間の階層関係です。クラスをロードするプロセスは、再帰呼び出しプロセスです。まず、親クラス ローダーは、最上位の起動クラス ローダーに到達するまで、階層ごとに委任されてロードされます。起動クラス ローダーロード時に、ロードのためにサブクラスローダーにレイヤーごとに委任されます。

  4. 親委任の目的は主に、公式 Java クラス ライブラリ <JAVA_HOME>\lib および拡張クラス ライブラリ <JAVA_HOME>\lib\ext の読み込みセキュリティを確保することです。開発者によってオーバーライドされます。

  5. 親の委任を破棄するには 2 つの方法があります: 1 つ目はカスタム クラス ローダーで、findClass とloadClass をオーバーライドする必要があります。2 つ目は、スレッド コンテキスト クラス ローダーの推移性を使用して、親クラス ローダーがサブクラスのロード アクションを呼び出すようにする方法です。ローダ。


3. SPIの仕組み

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

1.JDK SPI

JDK で提供される SPI 関数のコア クラスは java.util.ServiceLoader です。「META-INF/services/」配下に複数の設定実装ファイルをクラス名で取得する機能です。

例:javax.servlet.ServletContainerInitializerファイルの内容は、ファイル名で表されるインターフェイスの実装クラスの完全修飾クラス名です。org.springframework.web.SpringServletContainerInitializer
ここに画像の説明を挿入します

ロード順序 (クラスパス) はユーザーによって指定されるため、最初にロードするか最後にロードするかに関係なく、ユーザー定義の構成をロードできない可能性があります。

したがって、これは JDK SPI メカニズムの欠点でもあり、どの実装がロードされているかを確認することができず、指定された実装をロードすることも不可能であり、ClassPath の順序のみに依存するのは非常に不正確な方法です。

2. 春のSPI

Spring の SPI 設定ファイルは固定ファイルでありMETA-INF/spring.factories、機能的には JDK に似ています。各インターフェイスには複数の拡張機能を実装でき、使い方は非常に簡単です。

// すべてのファクトリ ファイルに設定されている LoggingSystemFactory を取得します

List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

以下はSpringBootのspring.factoriesの設定です。

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

Spring SPI では、すべての設定が固定ファイルに入れられるため、多数のファイルを設定する手間が省けます。複数のインターフェースの拡張構成については、1 つのファイルを使用する方が良いか、それとも個別のファイルを使用する方が良いかは意見の問題です (個人的にはすっきりとした Spring が好きです)。

SpringのSPIはspring-framework(core)に属しますが、現在は主にSpringBootで使用されています。前の 2 つの SPI メカニズムと同様に、Spring は ClassPath 内の複数の spring.factories ファイルの存在もサポートしており、ロード時に、これらの spring.factories ファイルはクラスパスの順序で順次ロードされ、ArrayList に追加されます。エイリアスがないので重複削除の概念がなく、あるだけ追加していきます。

ただし、Spring の SPI は主に Spring Boot で使用されるため、Spring Boot の ClassLoader は依存関係パッケージ内のファイルではなく、プロジェクト内のファイルのロードを優先します。したがって、プロジェクトに spring.factories ファイルを定義すると、プロジェクト内のファイルが最初に読み込まれ、取得された Factories の中で、プロジェクト内の spring.factories に設定された実装クラスも最初にランク付けされます。

インターフェイスを拡張したい場合は、META-INF/spring.factoriesプロジェクト (SpringBoot) に新しいファイルを作成し、必要な構成を追加するだけです。

たとえば、新しい LoggingSystemFactory 実装を追加するだけの場合は、完全にコピーして変更するのではなく、新しい META-INF/spring.factories ファイルを作成するだけで済みます。

org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory

3. ダボSPI

Dubbo は、SPI メカニズムを通じてすべてのコンポーネントをロードします。ただし、Dubbo は Java のネイティブ SPI メカニズムを使用しませんが、ニーズに合わせて拡張します。Dubbo では、SPI は非常に重要なモジュールです。SPIをベースにDubboを簡単に拡張できます。Dubbo のソースコードを学びたい場合は、SPI の仕組みを理解する必要があります。次に、Java SPI と Dubbo SPI の使い方を理解してから、Dubbo SPI のソース コードを分析してみましょう。
新しい SPI メカニズムが Dubbo に実装されており、より強力でより複雑です。関連するロジックは ExtensionLoader クラスにカプセル化されており、ExtensionLoader を通じて指定された実装クラスをロードできます。Dubbo SPI に必要な設定ファイルは META-INF/dubbo パスに配置する必要があり、設定内容は以下の通りです (以下のデモは dubbo 公式ドキュメントより引用)。

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

Java SPI 実装クラス設定とは異なり、Dubbo SPI はキーと値のペアを通じて設定されるため、指定された実装クラスをオンデマンドでロードできます。また、@SPI アノテーションを使用する場合は、インターフェイス上で @SPI アノテーションをマークする必要があります。Dubbo SPI の使用法を示してみましょう。

@SPI
public interface Robot {
    
    
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    
    @Override
    public void sayHello() {
    
    
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {
    
    
    @Override
    public void sayHello() {
    
    
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class DubboSPITest {
    
    
    @Test
    public void sayHello() throws Exception {
    
    
        ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

Dubbo SPI と JDK SPI の最大の違いは、「エイリアス」をサポートしていることです。拡張ポイントのエイリアスを通じて固定拡張ポイントを取得できます。上記の例のように、Robot の複数の SPI 実装のうち、エイリアス「optimusPrime」の実装を取得できるほか、エイリアス「bumblebee」の実装も取得できるので、この機能はとても便利です!

@SPI アノテーションの value 属性を使用して、デフォルトで「エイリアス」実装を使用することもできます。たとえば、Dubbo では、デフォルトは Dubbo プライベート プロトコルです: dubbo プロトコル - dubbo://
**
Dubbo のプロトコルのインターフェイスを見てみましょう:

@SPI("dubbo")
public interface Protocol {
    
    
    // ...
}

プロトコル インターフェイスでは、@SPI アノテーションが追加され、そのアノテーションの値は dubbo です。SPI 経由で実装を取得すると、プロトコル SPI 構成内のエイリアス dubbo を持つ実装が取得されます。 rpc.Protocol ファイルは次のとおりです。

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

その後、getDefaultExtension を通じて @SPI アノテーションの値に対応する拡張機能の実装を取得するだけで済みます。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol

Adaptive メカニズムもあります。これは非常に柔軟ですが、その使用法はあまり「エレガント」ではないため、ここでは紹介しません。

Dubbo の SPI には「ロード優先順位」もあります。組み込み (内部) が最初にロードされ、次に外部 (外部) が優先順位に従ってロードされます。重複が見つかった場合、それらはスキップされ、ロードされません。ロードされています。

したがって、クラスパスのロード順序に依存して組み込み拡張機能を上書きする場合、これも不合理なアプローチであり、理由は上記と同じであり、ロード順序が厳密ではありません。

4. 比較

3 つの SPI メカニズムを比較すると、JDK の組み込みメカニズムは最も弱いですが、JDK に組み込まれているため、依然として特定のアプリケーション シナリオが存在します。結局のところ、追加の依存関係は必要ありません。Dubbo は最も豊富な機能を備えていますが、そのメカニズムはは少し複雑で、Dubbo でのみ使用でき、完全に独立したモジュールとみなすことはできませんが、Spring の機能は JDK とほぼ同じです。最大の違いは、すべての拡張ポイントが spring.factories で記述されていることです。これも改善であり、IDEA は構文プロンプトを完全にサポートしています。

JDK SPI ダボSPI 春のSPI
ファイルモード 拡張ポイントごとに個別のファイル 拡張ポイントごとに個別のファイル すべての拡張ポイントを 1 つのファイルに
修正された実装を取得する サポートされていません。すべての実装を順番に取得することしかできません 「エイリアス」という概念があり、名前を通じて拡張ポイントの固定実装を取得でき、Dubbo SPI アノテーションと連携すると非常に便利です。 サポートされていません。すべての実装は順番にのみ取得できます。ただし、Spring Boot ClassLoader はユーザー コード内のファイルのロードを優先するため、ユーザー定義の spring.factoires ファイルが最初になることを保証でき、最初のファクトリを取得することでカスタマイズされた拡張機能を固定的に取得できます。
他の なし Dubbo 内での依存関係の注入をサポートし、ディレクトリを通じて Dubbo の組み込み SPI と外部 SPI を区別し、内部 SPI を最初にロードして内部 SPI が最高の優先順位を持つようにします。 なし

おすすめ

転載: blog.csdn.net/qq_45867699/article/details/132100088