Loading

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文件(没有后缀)

内容如下:
image

image

然后我们再用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 路径下,配置内容如下

image

与 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 属性,还可以默认一个“别名”的实现。

image

image

Spring SPI

Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单

image

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的方式就可以固定获取自定义的扩展
posted @ 2021-07-16 16:09  Xianhao  阅读(113)  评论(0编辑  收藏  举报