SPI机制学习
SPI是什么
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。利用特性,可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
SPI 有什么用?
假设我们现在设计了一套日志框架,可以用XML文件的方式进行配置,也可以用Properties文件的方式进行配置
我们会写出这样的代码
public interface LoggerConfiguration {
void configure(String configFile);
}
public class PropertiesLoggerConfiguration implements LoggerConfiguration {
@Override
public void configure(String configFile) {
System.out.println("PropertiesLoggerConfiguration" + configFile);
}
}
public class XMLLoggerConfiguration implements LoggerConfiguration {
@Override
public void configure(String configFile) {
System.out.println("XMLLoggerConfiguration" + configFile);
}
}
public class LoggerFactory {
public static void main(String[] args) {
String configFile = "";
LoggerConfiguration loggerConfiguration = new XMLLoggerConfiguration();
loggerConfiguration.configure(configFile);
}
}
这样的写法固然是可以达到目的的,不过扩展性不太好,因为如果想定制/扩展/重写解析功能的话,我还得重新定义入口的代码,LoggerFactory 也得重写,不够灵活,侵入性太强了。
比如现在想增加一个 yml 文件配置的方式,作为日志配置文件,那么只需要新建一个YAMLLoggerConfiguration,实现 LoggerConfiguration 就可以。但是……怎么注入呢,怎么让 LoggerFactory中使用新建的这个 那么只需要新建一个YAMLLoggerConfiguration ?难不成连 LoggerFactory 也重写了?
如果借助SPI机制的话,这个事情就很简单了,可以很方便的完成这个入口的扩展功能。
JDK SPI
JDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其作用就是,可以通过类名获取在"META-INF/services/"下的多个配置实现文件
我们在META-INF/services/下创建一个com.panxianhao.SPILearn.LoggerConfiguration文件(没有后缀)
内容如下:
然后我们再用SPI的方式重写LoggerFactory
public class JDKSPILoggerFactory {
public static void main(String[] args) {
String configFile = "-JDK-SPI";
ServiceLoader<LoggerConfiguration> serviceLoader = ServiceLoader.load(LoggerConfiguration.class);
Iterator<LoggerConfiguration> iterator = serviceLoader.iterator();
LoggerConfiguration configuration = null;
while (iterator.hasNext()) {
//加载并初始化实现类
configuration = iterator.next();
}
//对最后一个configuration类调用configure方法
configuration.configure(configFile);
}
}
JDK自带的SPI实现方式,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠ClassPath的顺序是一个非常不严谨的方式。
Dubbo SPI
Dubbo 中实现了一套新的 SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI 注解。
@SPI
public interface Robot {
void sayHello();
}
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 Robot 多个 SPI 实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!
通过 @SPI 注解的 value 属性,还可以默认一个“别名”的实现。
Spring SPI
Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单
Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦
由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个
区别
JDK SPI | DUBBO SPI | Spring SPI | |
---|---|---|---|
文件方式 | 每个扩展点单独一个文件 | 每个扩展点单独一个文件 | 所有的扩展点在一个文件 |
获取某个固定的实现 | 不支持,只能按顺序获取所有实现 | 有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便 | 不支持,只能按顺序获取所有实现。但由于Spring Boot ClassLoader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展 |