引言
在软件设计模式,有个原则叫开闭原则(对拓展开放,对修改关闭),其核心思想是面向抽象编程。
举个例子,我们定义一个车的抽象,然后我实现一个抽象将其变成自行车,在实现这个抽象将其变成摩托车,那么对于车子这个抽象来说就有了两种不用的实现。
一般情况下,我们自己选择调用哪个实现,来使用相关的功能。那么问题来了,如果这两个实现变成两个模块,如果在上层接口调用势必要涉及到具体实现类,这就违反了开闭原则。
Java中提供了SPI机制,就是为了解决这个问题的。
什么是SPI?
SPI是Service Provider Interface的缩写,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
SPI整体机制图如下:
我们可以看出来,SPI最大的特点是由服务调用方制定好接口规范,服务提供方来根据规范进行实现。这一点,和我们日常开发的时候有点点区别。我们日常开发的时候,都是自己定好规范,然后提供给服务调用方。
实战演示
首先创建一个Java工程,文件结构如下:
spi-demo
|–resources
|–META-INF
|–serevices
|–src
|–com.zhu.spitest
在com.zhu.spitest下定义我们要实现的接口Phone,类全限定名是com.zhu.spitest.Phone,代码如下:
package com.zhu.spitest; public interface Phone { String getSystemInfo(); }
在相同路径下写下具体实现类,代码如下:
package com.zhu.spitest; public class Android implements Phone{ @Override public String getSystemInfo() { return "安卓手机"; } } package com.zhu.spitest; public class Iphone implements Phone{ @Override public String getSystemInfo() { return "苹果手机"; } }
在resouces/META-INF/services目录下,新增一个文件,文件名是步骤2中结构的全限定名,即com.zhu.spitest.Phone。
在4中的文件中写入如下内容:
com.zhu.spitest.Iphone com.zhu.spitest.Android
在Phone同级目录下创建一个测试类Main,具体代码如下:
package com.zhu.spitest; import java.util.ServiceLoader; public class Main { public static void main(String[] args) { ServiceLoader<Phone> serviceLoaders = ServiceLoader.load(Phone.class); serviceLoaders.forEach(e->{ String systemInfo = e.getSystemInfo(); System.out.println(systemInfo); }); } } //输出结果 //苹果手机 //安卓手机
实现原理解析
我们可以看到,其实最关键的代码就是ServiceLoader.load(Phone.class)
,我们打开源码发现有个变量:
private static final String PREFIX = "META-INF/services/";
这就是为什么我们之前要在resources目录下创建META-INF/services/文件夹的原因。其实原理也很简单,就是在这个文件夹下,根据文件名和内容去加载指定的类。
有什么用?
解耦的作用之前说了,还有一种作用其实是破环双亲委托模型!
我们看一个常用的类java.sql.Driver,相信大家都很熟悉,这是数据库驱动的顶层接口,它的包处于JAVA_HOME/lib目录下,一般是Bootstrap类加载器来加载。但是,这个东西只是一个接口,没有具体的实现。具体的实现,是有各个数据库厂商来提供的,而三方的包一般都是由app类加载器来加载的,这就是问题所在。
为了绕过这个限制,就有了spi,它会传递进来一个当前线程的类加载器来加载厂商的实现。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}