在这个RPC项目中( https://github.com/brucewayne9064/MyRPCframework )有使用到SPI这种机制。分别是在以下几个地方:

  • SocketRpcClient的ServiceDiscovery接口,用基于zookeeper的ZkServiceDiscoveryImpl实现
  • ServiceRegistry接口有基于zookeeper的ZkServiceRegistryImpl实现
  • ZkServiceDiscoveryImpl的LoadBalance接口,有两种实现,一个是基于一致性哈希的ConsistentHashLoadBalance实现,一个是基于加权随机的RandomLoadBalance实现
  • RpcRequestTransport接口,有两种实现,一种是socket实现,一种是netty实现

spi即为service provider interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。

简单点说SPI就是 JDK 内置的一个服务发现机制,它使得接口和具体实现完全解耦。我们只声明接口,具体的实现类在配置中选择。

具体的就是你定义了一个接口,然后在META-INF/services目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。

这样就通过配置来决定具体用哪个实现!

例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。

下面我们通过一个简单的示例演示下 JDK SPI 的基本使用方式:

首先我们需要创建一个 Log 接口,来模拟日志打印的功能:

1
2
3
public interface Log { 
void log(String info);
}

接下来提供两个实现—— Logback 和 Log4j,分别代表两个不同日志框架的实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class Logback implements Log { 
@Override
public void log(String info) {
System.out.println("Logback:" + info);
}
}
public class Log4j implements Log {
@Override
public void log(String info) {
System.out.println("Log4j:" + info);
}
}

在项目的 resources/META-INF/services 目录下添加一个名为 com.xxx.Log 的文件,这是 JDK SPI 需要读取的配置文件,具体内容如下:

1
2
com.xxx.impl.Log4j 
com.xxx.impl.Logback

最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main { 
public static void main(String[] args) {
ServiceLoader<Log> serviceLoader =
ServiceLoader.load(Log.class);
Iterator<Log> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Log log = iterator.next();
log.log("JDK SPI");
}
     // JDK8 Lamda表达式的forEach遍历写法
// serviceLoader.forEach(log -> log.log("hello"));
}
}
// 输出如下:
// Log4j:JDK SPI
// Logback:JDK SPI