从 JDBC Driver 看 Java 的 SPI 机制与应用

背景

JDBC API 是 JDK 标准的一部分,定义了一组 Java 与数据库交互的接口,而数据库种类繁多,具体如何交互的实现由数据库厂商提供。拿 MySQL 为例,在 JDBC API 4.0 版本之后,通常我们只要引入它提供的 driver mysql-connector-java 作为我们的依赖,然后就可以调用 JDBC API 创建连接并执行 SQL 与数据库交互了。那么这个过程中,我们的程序是如何定位到 driver 具体的位置并正确加载的呢?这就要提到 SPI 机制了。

SPI 机制

SPI(Service Provider Interface) 可以说是一种面向扩展的设计模式,应用的核心逻辑通过面向接口的编程思路,并设计可扩展点,在运行时候通过服务发现机制加载扩展点的具体实现,从而达到在不修改核心逻辑的情况下,具体实现可灵活替换的效果。

截屏2022-03-28 上午2.20.05.png

Java 的 SPI 机制

Java 6 提供了 SPI 机制,Java 的 SPI 机制包含了两类角色,首先是服务定义者,涉及到的概念有:

  • Service Provider Interface(SPI) :服务提供者接口,通常是一组接口或者抽象类,统一定义了服务的消费形式。
  • ServiceLoader:服务在运行时的加载机制,根据定义的 SPI 找到具体的实现。

然后是服务实现者,涉及到的概念有:

  • Service Provider:服务的具体实现,是服务提供者对 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-28 上午2.00.56.png

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

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

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

MySQL Driver 如何实现 SPI

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

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

截屏2022-03-27 下午8.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 进行了注册。

那么如果发现了多个 driver 的话,要选择哪一个具体的实现呢?那就是 JDBC URL 了,driver 的实现有一个约定,如果 driver 根据 JDBC URL 判断不是自己可以处理的连接就直接返回空,DriverManager 就是基于这个约定直到找到第一个不返回 null 的连接为止。

总结

本文聊了聊 SPI 的设计思想,分析了 Java SPI 的机制与 ServiceLoader 服务加载的实现原理,并且借 JDBC API 为例看了具体的应用。

可以发现,Java 提供了一种不依赖额外框架的标准的本地服务发现机制,我们可以基于这个机制让我们的应用根据灵活性和可扩展性,在服务端基础库或者是安卓生态下都可以看到它的影子,只不过除了本地的服务发现之外,应该选择哪一个服务实现仍然是在设计扩展功能时需要额外考虑的部分。

总之了解 Java SPI 更重要的是学习 SPI 设计思想的一种实践,实际上你完全可以实现一个自己版本的 ServiceLoader(根据具体情况定义一个自己的服务发现机制,甚至 Jar 包也完全可以是通过远程加载的)来为你的应用提供一个独特的可扩展机制。

猜你喜欢

转载自juejin.im/post/7079989975533486111
今日推荐