初探Java的SPI机制

最近听到了一个神奇的词“SPI”,本着听不懂就多查查的道理去查了一波,发现网上搜不到这个词,只能搜到个“串行外设接口”的释义。
在请教了许多dalao之后,我终于对Java中的SPI机制有所了解了。

SPI机制其实应用很广泛

其实SPI机制并不是一个离我们很遥远的高级特性,应用很广泛,比如以MySQL连接件举个例子。

我们经常编写一个Java连接MySQL数据库程序的时候,会用到mysql-connector-java.jar文件,我把它叫做MySQL连接件,它其实内含了MySQL的驱动,这才能使得我们能够连接数据库。

用压缩软件打开这个Jar文件,就能看到META-INF里面居然有个services目录。这个目录里有个java.sql.Driver文件,里面存储的内容是com.mysql.cj.jdbc.Driver,如下图。

这是啥意思呢?
首先,如果大致了解过JDBC后就能明白,任何数据库驱动都需要实现java.sql.Driver接口,MySQL的连接件中对Driver接口的实现类正是com.mysql.cj.jdbc.Driver类。它这个文件名字恰好是java.sql.Driver,内容恰好是它的实现类com.mysql.cj.jdbc.Driver这使我很难不能联想到它在描述一个接口与实现类之间的对应关系

好,已经足够了,现在我们暂时到这里停止,在继续解释之前,不妨做一个这样的小实验。
我们把mysql-connector-java.jar作为依赖引入工程以后,我们用它建立数据库连接,这个代码该咋写?
来看下面这个代码。

String jdbcUrl = "JDBC URL"; //JDBC URL
String jdbcUsername = "root"; //用户名
String jdbcPasswd = "1234556"; //密码

Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUsername, jdbcPasswd); //这就是建立的连接了

这代码显然没毛病,但是我有个问题,Class.forName是干啥用的嘞?我把它去了行不行?
也许你会说,这代码是老师教的,课本写的,书上讲的,网上抄的,大家都这么整,我手贱去了它干啥。无妨,我就是想去掉试试看嘛。
结果发现......
诶?一点事都没有!该咋样就咋样,程序正常运行了!

现在我们遇到了灵异现象,灵异的表现在于它证明了老师教的代码居然没有用,书上讲的居然是废话,网上抄的居然是垃圾代码,大家都在写没用的东西。
针对这个灵异现象,我想从两方面来展开讲述这个问题。

  1. Class.forName是干啥用的,我为啥没事干要写这个东西?
  2. 明白了Class.forName是干啥用的了以后,为啥我去掉这个玩意儿却没事呢?

第一件事,Class.forName是干什么用的

在Java中,万物皆对象的概念能更进一步去理解。每一个被加载的类,都有一个对应的类对象,这个对象的类型为Class。例如String这个类的对象可以用String.class获取到。
还有种获取方法是Class.forName(类的全名)。例如String类的全名是java.lang.String,所以获取String这个类的对象的话,可以用Class.forName("java.lang.String")获取。

为了给类的动态加载提供便利,如果在使用Class.forName时,它在已经加载的类里并没有找到对应的类的话,会尝试去加载这个类

我们在导入了连接件的jar包之后,这个jar包其实只是徒有虚名挂在那里了而已,里面的类并没有被加载,可以理解为运行环境已经知道有这个jar包存在了,但是这个jar包里的东西根本就没人去用,没人用的话它就不会被“激活”(这个说法不太准确)。
所以我们在做数据库连接操作前,先将com.mysql.cj.jdbc.Driver通过Class.forName获得一下类对象,其实目的是确保MySQL驱动已经加载了

第二件事,为啥我们刚才去掉这玩意儿没事呢

我们现在并不知道问题的答案是什么,但是我们推理一下就能知道个大概了。
很明显,按理说我们导入了这个包的依赖以后,它里面的类是不会被主动加载的,只有有人主动调用了才会被加载。我们没Class.forName,下面的代码用的全部都是java.sql包里的东西,而不是连接件jar包里的东西,这充分意味着这并不是我们自己主动去调用加载的。

好,现在我们已经整明白了,这个Driver实现类咱们没主动去调用,而它却被加载了,并且我们知道它必须得有人主动调用才能会被加载。
让你说说看,这个Driver类是咋被调用的?魔法驱动是嘛?
答案已经很显然了,这必然是由于Java的某个特殊的自带机制,这个机制主动加载了这个驱动类。

其实在Java 6的版本更新中,Java引入了全新的JDBC4.0 API,这套API由于此时恰好Java 6引入了SPI机制而受到了改进,JDBC4.0已经不用我们主动调用刚才的那种Class.forName语句来加载驱动了。

看来SPI机制解决的问题确实具有鲜明的应用场景,并且应用方式我们也能猜出来了,是通过jar包里的META-INF下的services目录里的文件描述的映射关系来完成的配置。那SPI机制具体是怎么回事呢?

下面我们来对SPI机制进行一波初步的探索。

啥是SPI机制

SPI(Service Provider Interface)中文叫做“服务提供接口”,是从JavaSE 6开始就在JDK中开始提供的一种机制,它能够在扩展性插件中起到灵活替换组件与灵活加载插件的功能作用。
还是以上面的数据库连接件为例,JDBC定义了一个接口是java.sql.Driver,这个接口显然希望各个数据库驱动都能对其做实现的,比如MySQL和Oracle数据库的数据库连接件应该对此接口有不同的实现类。
JDBC的目的就是提供一个较为统一化的数据库调用方式,那理论上我现在项目用的是Oracle的数据库连接件,我把Oracle连接件的依赖去了,换成MySQL的连接件做依赖,假设我不考虑SQL语句的细微差异的话,我这个程序应当能顺利运行才对,因为我用的是JDBC的API,这俩数据库驱动都对JDBC做了适配支持,我调用的JDBC API没变,代码不需要大改就能直接跑起来了。

SPI机制对于JDBC而言,它能够为JDBC提供寻找对应服务的能力。这里的服务正是对应的实现类的意思,JDBC的Driver类对于SPI机制而言属于一个标准服务接口。
对于连接件,它对SPI而言属于标准服务接口的提供者。连接件只需要在META-INF里的services目录下编写一个这样的文件,文件名为要实现的接口的全名,内容为实现类的全名,那么SPI机制就能够为标准服务接口匹配上这个提供者实现类了。

SPI机制正是一个这样的机制,使得标准服务接口能够轻松地匹配到可用的提供者。

SPI机制的简单使用

让我们写一个简单的例子来试试看SPI机制怎么用。为此,我们需要准备好两样东西,分别是标准服务接口和对应的提供者。

假设我们现在创建了一个Maven项目,先定义一下标准服务接口,假设是下面这个接口,它能够提供一种输出信息的能力。

package net.tdiant.test;

public interface Output {
    void push(String msg);
}

假如我们想做一个面向标准输出流的输出实现的话,也许我们会做出这样的实现类。

package net.tdiant.test.impl;

public class StandardOutput implements Output {

    public static final OutputStream out = System.out;

    public void push(String msg) {
        out.println(msg);
    }
}

现在我们有了标准服务接口了,并且也有了提供者了,那我们第一步应该做的应该是去META-INFservices目录下创建对应的文件来描述对应关系才对。
在Maven项目的resources文件夹下的META-INF目录里的services目录里,创建net.tdiant.test.Output文件,内容为:

net.tdiant.test.impl.StandardOutput

好了,一切OK,现在让我们试试SPI机制能不能帮我们建立好映射关系了。
编写如下测试类。

package net.tdiant.test;

public class Test {
    public static void main(String[] fuck) {
        ServiceLoader<Output> ls = ServiceLoader.load(Output.class); //这就是SPI机制为我们找到的实现类了
        for(Output output : ls) {
            output.push("Hello world!");
        }
    }
}

不出意外的话,运行以后将会输出

Hello world!

这个Hello world!可以说是意义非凡了。

上一篇
下一篇