阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)

史前动物
发布于 2023-9-18 14:16
浏览
0收藏

大家好,我是三友~~

今天来跟大家聊一聊Java、Spring、Dubbo三者SPI机制的原理和区别。

其实我之前写过一篇类似的文章,但是这篇文章主要是剖析dubbo的SPI机制的源码,中间只是简单地介绍了一下Java、Spring的SPI机制,并没有进行深入,所以本篇就来深入聊一聊这三者的原理和区别。

什么是SPI

SPI全称为Service Provider Interface,是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。

简单来说SPI是一种非常优秀的设计思想,它的核心就是解耦、方便扩展。

Java SPI机制--ServiceLoader

ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:

  • 文件必须放在​​META-INF/services/​​目录底下
  • 文件名必须为接口的全限定名,内容为接口实现的全限定名

这样就能够通过ServiceLoader加载到文件中接口的实现。

来个demo

第一步,需要一个接口以及他的实现类

public interface LoadBalance {
}

public class RandomLoadBalance implements LoadBalance{
}

第二步,在​​META-INF/services/​​目录创建一个文件名LoadBalance全限定名的文件,文件内容为RandomLoadBalance的全限定名

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

测试类:

public class ServiceLoaderDemo {

    public static void main(String[] args){
        ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
        Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
        while (iterator.hasNext()) {
            LoadBalance loadBalance = iterator.next();
            System.out.println("获取到负载均衡策略:" + loadBalance);
        }
    }

}

测试结果:

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

此时就成功获取到了实现。

在实际的框架设计中,上面这段测试代码其实是框架作者写到框架内部的,而对于框架的使用者来说,要想自定义LoadBalance实现,嵌入到框架,仅仅只需要写接口的实现和spi文件即可。

实现原理

如下是ServiceLoader中一段核心代码

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

首先获取一个fullName,其实就是​​META-INF/services/接口的全限定名​

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给​​parse​​方法解析资源

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

​parse​​方法其实就是通过IO流读取文件的内容,这样就可以获取到接口的实现的全限定名

再后面其实就是通过反射实例化对象,这里就不展示了。

所以其实不难发现ServiceLoader实现原理比较简单,总结起来就是通过IO流读取​​META-INF/services/接口的全限定名​​文件的内容,然后反射实例化对象。

优缺点

由于Java的SPI机制实现的比较简单,所以他也有一些缺点。

第一点就是浪费资源,虽然例子中只有一个实现类,但是实际情况下可能会有很多实现类,而Java的SPI会一股脑全进行实例化,但是这些实现了不一定都用得着,所以就会白白浪费资源。

第二点就是无法对区分具体的实现,也就是这么多实现类,到底该用哪个实现呢?如果要判断具体使用哪个,只能依靠接口本身的设计,比如接口可以设计为一个策略接口,又或者接口可以设计带有优先级的,但是不论怎样设计,框架作者都得写代码进行判断。

所以总得来说就是ServiceLoader无法做到按需加载或者按需获取某个具体的实现。

使用场景

虽然说ServiceLoader可能有些缺点,但是还是有使用场景的,比如说:

  • 不需要选择具体的实现,每个被加载的实现都需要被用到
  • 虽然需要选择具体的实现,但是可以通过对接口的设计来解决

Spring SPI机制--SpringFactoriesLoader

Spring我们都不陌生,他也提供了一种SPI的实现SpringFactoriesLoader。

Spring的SPI机制的约定如下:

  • 配置文件必须在​​META-INF/​​目录下,文件名必须为spring.factories
  • 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。

所以也可以看出,Spring的SPI机制跟Java的不论是文件名还是内容约定都不一样。

来个demo

在​​META-INF/​​目录下创建spring.factories文件,LoadBalance为键,RandomLoadBalance为值

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

测试:

public class SpringFactoriesLoaderDemo {

    public static void main(String[] args){
        List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, MyEnableAutoConfiguration.class.getClassLoader());
        for (LoadBalance loadBalance : loadBalances) {
            System.out.println("获取到LoadBalance对象:" + loadBalance);
        }
    }

}

运行结果:

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

成功获取到了实现对象。

核心原理

如下是SpringFactoriesLoader中一段核心代码

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

其实从这可以看出,跟Java实现的差不多,只不过读的是​​META-INF/​​目录下spring.factories文件内容,然后解析出来键值对。

使用场景

Spring的SPI机制在内部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot启动过程中很多扩展点都是通过SPI机制来实现的,这里我举两个例子

1、自动装配

在SpringBoot3.0之前的版本,自动装配是通过SpringFactoriesLoader来加载的。

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

但是SpringBoot3.0之后不再使用SpringFactoriesLoader,而是Spring重新从​​META-INF/spring/​​​目录下的​​org.springframework.boot.autoconfigure.AutoConfiguration.imports​​文件中读取了。

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

至于如何读取的,其实猜也能猜到就跟上面SPI机制读取的方式大概差不多,就是文件路径和名称不一样。

2、PropertySourceLoader的加载

PropertySourceLoader是用来解析application配置文件的,它是一个接口

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

SpringBoot默认提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader两个实现,就是对应properties和yaml文件格式的解析。

SpringBoot在加载PropertySourceLoader时就用了SPI机制

阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别(上篇)-鸿蒙开发者社区

与Java SPI机制对比

首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。

其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。

第三点就是Spring的SPI机制提供了获取类限定名的方法​​loadFactoryNames​​,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。

但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。

所以不知道你有没有发现,PropertySourceLoader它其实就是一个策略接口,注释也有说,所以当你的配置文件是properties格式的时候,他可以找到解析properties格式的PropertiesPropertySourceLoader对象来解析配置文件。


文章转载自公众号:三友的java日记

标签
已于2023-9-18 14:16:40修改
收藏
回复
举报
回复
    相关推荐