JDBCドライバーからJavaのSPIメカニズムとアプリケーションを見る

バックグラウンド

JDBC APIはJDK標準の一部であり、Javaがデータベースと対話するための一連のインターフェースを定義します。データベースには多くのタイプがあり、対話方法の実装はデータベースベンダーによって提供されます。MySQLを例にとると、JDBC APIバージョン4.0以降は、通常、MySQLによって提供されるドライバーをmysql-connector-java依存関係として導入するだけで済み、JDBC APIを呼び出して接続を作成し、SQLを実行してデータベースと対話できます。では、このプロセスでは、プログラムはどのようにしてドライバーの特定の場所を特定し、正しくロードするのでしょうか。これは、SPIメカニズムについて言及することです。

SPIメカニズム

SPI(サービスプロバイダーインターフェイス)は、拡張指向のデザインパターンと言えます。アプリケーションのコアロジックは、インターフェイス指向のプログラミングアイデアによって設計され、拡張可能なポイントが設計され、拡張ポイントの特定の実装が読み込まれます。実行時のサービス検出メカニズムを介して、コアロジックを変更しない場合、柔軟な置換の効果が実現されます。

スクリーンショット2022-03-28AM2.20.05.png

JavaのSPIメカニズム

Java6はSPIメカニズムを提供します。JavaSPIメカニズムには2つの役割があります。1つはサービス定義者です。関連する概念は次のとおりです。

  • サービスプロバイダーインターフェイス(SPI):サービスプロバイダーインターフェイス(通常はインターフェイスのセットまたは抽象クラス)は、サービスの消費形式を一律に定義します。
  • ServiceLoader:実行時のサービスのロードメカニズム。特定の実装は、定義されたSPIに従って検出されます。

次に、サービス実装者がいます。関連する概念は次のとおりです。

  • サービスプロバイダー:サービスの具体的な実装は、サービスプロバイダーによるSPIの実装です。

SPIメカニズムの原理について話す簡単なケース

举一个简单的例子,比如我们的服务需要对消费者提供支付能力,但实际的支付能力可能由支付宝或者微信提供,那么我们作为服务定义者,首先定义服务的标准接口,它本身就是一个普通的 Java Interface,比如我们定义我们的 SPI:

package me.leozdgao.demo.spi;

public interface Payment {
    void pay(Long amount, Long from, Long to);
}
复制代码

这个接口应该被公开出来供服务提供者实现,我们可以把它放到一个独立的包中去发布,比如叫做 my-system-spi,接下来就是服务提供者去实现了,首先引入包含 SPI 定义的包:

<dependency>
    <groupId>me.leozdgao</groupId>
    <artifactId>my-system-spi</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
复制代码

然后来进行对它的实现:

package me.leozdgao.payment;

public class MyPayment implements Payment {

    @Override
    public void pay(Long amount, Long from, Long to) {
        System.out.println("Pay with " + amount + " from " + from + " to " + to);
    }
}
复制代码

同时,服务提供者需要对外告知自己有对某个 SPI 的实现,告知的方式有一个约定,就是在 Jar 包的 META-INF/services 文件夹中,定义一个以 SPI 全限定名为文件名的文件,文件内定义 SPI 实现类的全限定名,比如我们需要创建一个 META-INF/services/me.leozdgao.demo.spi.Payment 文件,文件的内容如下:

me.leozdgao.payment.MyPayment
复制代码

如果一个 Jar 包中有多个实现,则可以都列出来并通过换行符分割即可。

服务提供者完成实现后,发布自己的 Jar 包,那么接下来我们就需要去加载它了,这就涉及到最关键的 ServiceLoader 了,我们先看看如何使用:

package me.leozdgao.demo.service;

import me.leozdgao.easyerp.spi.Payment;

import java.util.Iterator;
import java.util.ServiceLoader;

public class MyService {

    @Override
    public void doPay(Long amount, Long from, Long to) {
        ServiceLoader<Payment> loader = ServiceLoader.load(Payment.class);

        for (Driver driver : loader) {
                // ...
        }
    }
}
复制代码

可以看到我们通过调用 ServiceLoader.load 方法并传入我们的 SPI 接口来创建了一个 ServiceLoader 实例,由于 ServiceLoader 实现了 Iterator 迭代器接口,通过访问迭代器就可以获取实现了 SPI 的服务。如果有多个 SPI 的实现的话,具体采用哪个就需要自行处理判断了。

这样我们就在运行时顺利完成了服务的加载,未来我们如果要对 SPI 的实现要做替换,也完全不需要修改我们的逻辑代码。

ServiceLoader 的服务加载实现原理

ServiceLoader.load 方法本质就是创建一个 ServiceLoader 实例,而服务加载主要在 ServiceLoader 的 Lazy Iterator 实现中,我们来看看迭代器方法的实现逻辑:

スクリーンショット2022-03-282.00.56am.png

具体源码请参考 JDK 源码:java.util.ServiceLoader

由于是 Lazy Iterator,ServiceLoader 实例并不会一开始就去找到所有的实现,而是在不断的调用迭代器的过程中去懒加载实现类、完成实例化,并将实现类实例化的结果缓存起来。服务的初始化逻辑也反映出了两个约定:

  • 实现类的定位依赖 META-INF/services/* 的声明(前面已经提到)
  • 服务实现类需要提供无参构造函数来进行实例化

MySQL Driver 如何实现 SPI

上面介绍完 SPI 的实现机制后,再来回答开头的问题:我们的程序是如何定位到 driver 具体的位置并正确加载的?这个就是 DriverManager 的实现了。

DriverManager 的静态方法 getDriversgetDriverdriversgetConnection 中都会调用一个 ensureDriversInitialized 方法,这个方法会保证 driver 的初始化并仅执行一次,我们具体看一下它的实现逻辑:

スクリーンショット2022-03-278.51.00pm.png

具体源码请参看 JDK 源码:java.sql.DriverManager

通过两种方式进行 driver 的初始化,一种是通过系统参数 jdbc.drivers 指定,通过反射的方式调用 Class.forName 进行初始化,另一种就是利用 ServiceLoader.load(Driver.class) 找到服务提供方,通过调用迭代器触发服务的初始化。

服务具体的初始化方式利用的是类的静态代码块机制,以 MySQL 的 driver 为例:

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!");
        }
    }
}
复制代码

可以看到 Driver 的实现包含一个静态代码块,在通过反射 Class.forName 或者被 ServiceLoader 迭代器初始化时,都可以触发它的执行,在这里调用了 DriverManager.registerDriver 进行了注册。

では、複数のドライバーが見つかった場合、どの特定の実装を選択するのでしょうか。これがJDBCURLです。ドライバーの実装には規則があります。ドライバーが接続がJDBCURLに従って処理できるものではないと判断した場合、ドライバーは直接nullを返します。DriverManagerこの規則に基づいて、 nullを返さない最初の接続。

要約する

この記事では、SPIの設計アイデアについて説明し、Java SPIのメカニズムとServiceLoaderサービス例として取り上げて特定のアプリケーションを確認します。

Javaは、追加のフレームワークに依存しない標準のローカルサービス検出メカニズムを提供していることがわかります。このメカニズムに基づいて、サーバー側の基本ライブラリまたはAndroidエコシステムの両方で、柔軟性とスケーラビリティに基づいてアプリケーションを作成できます。 。その影になりますが、ローカルサービスの検出に加えて、拡張機能を設計する際には、どのサービス実装を選択するかを検討する必要があります。

つまり、Java SPIを理解することは、SPI設計のアイデアの実践を学ぶためにより重要です。実際、独自のバージョンのServiceLoaderを実装できます(特定の状況に応じて独自のサービス検出メカニズムを定義し、Jarパッケージもリモートでロードできます)。 。)アプリケーションに独自の拡張メカニズムを提供します。

おすすめ

転載: juejin.im/post/7079989975533486111
おすすめ