Просмотр механизма и приложения SPI Java из драйвера JDBC

задний план

API JDBC является частью стандарта JDK и определяет набор интерфейсов Java для взаимодействия с базами данных.Существует много типов баз данных, и реализация того, как взаимодействовать, предоставляется поставщиками баз данных. Возьмите MySQL в качестве примера, после JDBC API версии 4.0 нам обычно нужно только ввести драйвер, предоставленный им, mysql-connector-javaв качестве нашей зависимости, а затем мы можем вызвать JDBC API для создания соединения и выполнить SQL для взаимодействия с базой данных. Итак, как в этом процессе наша программа находит конкретное местоположение драйвера и загружает его правильно? Это не говоря уже о механизме SPI.

SPI-механизм

Можно сказать, что SPI (интерфейс поставщика услуг) является шаблоном проектирования, ориентированным на расширение.Основная логика приложения разработана с помощью идеи программирования, ориентированной на интерфейс, и разработаны расширяемые точки, и загружена конкретная реализация точек расширения. через механизм обнаружения службы во время выполнения, чтобы достичь В случае отсутствия изменения основной логики реализуется эффект гибкой замены.

Скриншот 28.03.2022 2.20.05.png

Механизм SPI в Java

Java 6 предоставляет механизм SPI. Механизм Java SPI включает в себя две роли. Первая — это определитель службы. Используемые концепции:

  • Интерфейс поставщика услуг (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 实现中,我们来看看迭代器方法的实现逻辑:

Скриншот 28.03.2022 2.00.56 am.png

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

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

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

MySQL Driver 如何实现 SPI

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

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

Скриншот 27.03.2022 20.51.00.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 进行了注册。

Итак, если обнаружено несколько драйверов, какую конкретную реализацию выбрать? Это URL-адрес JDBC. Реализация драйвера имеет соглашение. Если драйвер считает, что соединение не является чем-то, что он может обработать в соответствии с URL-адресом JDBC, он напрямую вернет значение null. DriverManagerНа основе этого соглашения он найдет первое соединение, которое не возвращает null.

Суммировать

В этой статье рассказывается об идее дизайна SPI, анализируется механизм Java SPI и принцип реализации загрузки ServiceLoaderсервисов , а также на примере JDBC API рассматривается конкретное приложение.

Можно обнаружить, что Java предоставляет стандартный механизм обнаружения локальных сервисов, который не зависит от дополнительных фреймворков.На основе этого механизма мы можем делать наши приложения, основанные на гибкости и масштабируемости, как в базовой библиотеке на стороне сервера, так и в экосистеме Android. В свою очередь, в дополнение к обнаружению локальных служб выбор реализации службы по-прежнему является дополнительным фактором при разработке расширенной функциональности.

Короче говоря, понимание Java SPI более важно, чтобы изучить на практике идеи проектирования SPI, Фактически, вы можете реализовать свою собственную версию ServiceLoader (определить свой собственный механизм обнаружения служб в соответствии с конкретной ситуацией, и даже пакеты Jar могут быть загружены удаленно . ), чтобы обеспечить уникальный механизм расширения для вашего приложения.

рекомендация

отjuejin.im/post/7079989975533486111
рекомендация