JDBCからの親の委任モデルとSPIの表示

JDBCからの親の委任モデルとSPIの表示

(I.概要

Java自体には一連のリソース管理サービスJNDI(Javaネーミングおよびディレクトリインターフェース、Javaネーミングおよびディレクトリインターフェース)があり、rt.jarに配置され、スタートアップクラスローダーによってロードされます。JDBCもそれに含まれています。 JDBCを例に取り、JDBCが親の委任モデルを破る理由を確認してください。

JDBCを使用する場合、データベースベンダーから提供されたデータベース接続ドライバーjarパッケージを自分でダウンロードする必要があることは誰もが知っています。このjarパッケージは、実際にはドライバーインターフェースの実装クラスです。以下はドライバーインターフェースです。

public interface Driver {
    
    
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

JDKは標準化されたインターフェースしか提供できませんが、対応する実装を提供できません。これには、実装するさまざまなデータベースベンダーが必要です。さまざまなデータベースベンダーがインターフェイス指向のプログラミングを通じてこのドライバーインターフェイスを実装しているため、さまざまな種類のデータベースのさまざまな実装がシールドされ、さまざまなデータベースがデータ操作とクエリに同じAPIを使用できます。さらに、Driverの管理を担当するクラスDriverManagerがあります。ほとんどの人はそれに慣れているはずですが、その背後にある原理を理解していない可能性があります。まず、削除されたソースコードを見てみましょう。

public class DriverManager {
    
    
    // 这里用来保存所有Driver的具体实现
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
    
    
        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {
    
    

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
    
    
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
    
    
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);
    }
}

データベースドライバーを使用する前に、DriverManagerのregisterDriver()に登録する必要があり、それを通常どおり使用できることがわかります。

(2)親の委任モデルを破棄しないでください

以前の習慣によると、Class.forName()メソッドを使用してドライバーをロードし、次のようにDriverManagerを介して接続を取得します。

// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.连接到数据库
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

明らかに、Class.forName()メソッドは、ドライバークラスの読み込みをトリガーします。ドライバークラス内にドライバーがどのように登録されているかを確認する例として、MySQLドライバークラスを見てみましょう。

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!");
        }
    }
}

Class.forName()によってドライバークラスが読み込まれると、クラスの静的コードブロックが実行され、MySQLによって実装されたDriverオブジェクトがインスタンス化されてDriverManagerに登録されます。DriverManagerを介して接続を取得するときは、現在のすべてのDriver実装をトラバースし、1つを選択して接続を確立するだけです。

(3)親委譲モデルの破壊(SPIメカニズム)

この読み込みモードについて説明する前に、まずSPIを理解する必要があります。

SPIの完全な名前はサービスプロバイダーインターフェイスであり、主にベンダー定義のコンポーネントまたはプラグインで使用されます。詳細は、java.util.ServiceLoaderのドキュメントで説明されています。Java SPIメカニズムの考え方の概要:システムの抽象モジュールには、多くの場合、ログモジュール、xml解析モジュール、jdbcモジュールなど、さまざまな実装スキームがあります。

オブジェクト指向の設計では、一般にモジュールをインターフェイスプログラミングに基づいて作成することをお勧めします。実装クラスはモジュール間でハードコーディングされません。コードに特定の実装クラスが含まれると、プラグイン可能性の原則に違反します。実装を置き換える必要がある場合は、コードを変更する必要があります。モジュールのアセンブル時にプログラムで動的に指定できないことを認識するために、サービス検出メカニズムが必要です。Java SPIは、そのようなメカニズムを提供します。インターフェースのサービス実装を見つけます。これは、アセンブリの制御をプログラムの外に移動するというIOCのアイデアに多少似ていますが、このメカニズムはモジュール設計で特に重要です。

Java SPIの具体的な合意は次のとおりです。サービスプロバイダーがサービスインターフェースの実装を提供する場合、サービスインターフェースにちなんで名付けられたファイルが同時にjarパッケージのMETA-INF / services /ディレクトリに作成され、このファイルがこれを実現します。サービスインターフェースの具象実装クラス。外部プログラムがこのモジュールをアセンブルすると、jarパッケージMETA-INF / services /の構成ファイルを介して特定の実装クラス名を見つけ、インスタンス化をロードしてモジュールインジェクションを完了することができます。このような規則に基づいて、サービスインターフェイスの実装クラスをコード内で定式化する必要なく、適切に見つけることができます。

JDBC4.0以降、SPIを使用してこのドライバーを登録することをサポートし始めました。具体的な方法は、MySQLドライバーjarパッケージのMETA-INF / services / java.sql.Driverファイルで現在使用されているドライバー実装クラスを指定することです。これを使用すると、直接接続を取得して、上記のClass.forName()の登録プロセスを保存できます。

 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

SPIローディングメソッドを使用するプロセスを見てみましょう。

  1. META-INF / services / java.sql.Driverファイルから「com.mysql.jdbc.Driver」という名前の特定の実装クラスを取得します。
  2. このクラスをロードします。ここでは、ロードにclass.forName( "com.mysql.jdbc.Driver")も使用されます。

この読み込み方法は、手動読み込みから自動読み込みに変更されただけで、以前の方法と何ら変わらないように見えますが、この読み込みには多くの問題が発生します。クラス読み込み規則に従って、Class.forName()はこのメソッドの呼び出し元のクラスローダーで読み込まれます前述のように、呼び出し元のDriverManagerはrt.jarにあり、そのClassLoaderはBootstrapClassLoader、およびcom.mysql.jdbcです。ドライバーは<JAVA_HOME> / libの下にないため、BootstrapClassLoaderはこのクラスをロードできません。これは親委任モデルの制限であり、親ローダーは子クラスローダーのパスにあるクラスをロードできません。したがって、この場合、親の委任モデルの制限を破る必要があります。

では、この問題を解決するにはどうすればよいですか?現在の状況によれば、このドライバーはAppClassLoaderによってのみロードできるため、BootstrapClassLoaderにAppClassLoaderを取得するメソッドがある場合、それを介してロードできます。これは、いわゆるスレッドコンテキストローダー(ContextClassLoader)です。ContextClassLoaderは、Thread.setContextClassLoader()メソッドを使用して設定できます。特別な設定がない場合は、親クラスから継承されます。通常、デフォルトではAppClassLoaderが使用されます。明らかに、ContextClassLoaderは、親クラスローダーが子クラスローダーを呼び出すことによってクラスをロードできるようにします。これにより、親委譲モデルの原則が破られます。

まず、SPIサービス読み込みメカニズムを使用してドライバー登録の原則を分析できます。焦点はDriverManager.getConnection()にあります。クラスの静的メソッドを呼び出すと、クラスが初期化されることがわかっています。その静的コードブロックの実行は、クラスを初期化するプロセスの重要な部分です。DriverManagerの静的コードブロックは次のとおりです。

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

loadInitialDrivers()メソッドは静的コードブロックで呼び出されます。このメソッドのソースコードは次のとおりです。

private static void loadInitialDrivers() {
    
    
    //省略代码
    //这里就是查找各个数据库厂商在自己的jar包中通过spi注册的驱动
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try{
    
    
         while(driversIterator.hasNext()) {
    
    
         	driversIterator.next();
         }
    } catch(Throwable t) {
    
    
    	// Do nothing
    }
    //省略代码
}

ソースコードの観点から見ると、SPIの読み込みプロセスは前述のプロセスとまったく同じであることがわかります。SPIの読み込みはServiceLoader.load(Driver.class)メソッドにあることがわかりました。具体的な実装を見てみましょう。

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);
}

コードのコアはContextClassLoaderを取得し、ServiceLoaderを構築することです。ContextClassLoaderはデフォルトでAppClassLoaderの参照を格納します。これは実行時にスレッドに配置されるため、現在のプログラムの場所(BootstrapClassLoaderやExtClassLoaderなど)に関係なく、いつでも必要なときにThread.currentThread()を使用できます。 getContextClassLoader()は、必要な操作を完了するためにアプリケーションクラスローダーを取り出します。

DriverManagerのloadInitialDrivers()メソッドには、driversIterator.next()という文があり、その具体的な実装は次のとおりです。

private S nextService() {
    
    
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    
    
        //此处的cn就是产商在META-INF/services/java.sql.Driver文件中注册的Driver具体实现类的名称
       //此处的loader就是之前构造ServiceLoader时传进去的ContextClassLoader
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
    
    
        fail(service, "Provider " + cn + " not found");
    }
 	//省略部分代码
}

ContextClassLoaderを介してAppClassLoaderを正常に取得しました。同時に、データベースメーカーが子jarパッケージに登録したドライバの特定の実装クラス名も検出したため、rt.jarパッケージのDriverManagerで正常に成功しました。サードパーティアプリケーションパッケージに配置されたクラスがロードされます。

SPIメカニズムはほとんどここで説明されています。端的に言えば、JDKは、サードパーティの実装者が規則(/ METAにクラス名を書き込む)に従っている限り、サードパーティの実装者がサービス(データベースドライバ、ログライブラリなど)をロードするのに役立つ便利な方法を提供します。 -INF)。次に、起動時に、合意されたクラス名を満たすjarパッケージ内のすべてのクラスをスキャンし、forNameを呼び出してロードします。ただし、呼び出し元のClassLoaderはロードできないため、現在実行中のスレッドのスレッドコンテキストクラスローダーにロードしてからロードしてください。

2020年9月16日

おすすめ

転載: blog.csdn.net/weixin_43907422/article/details/108623940