一、背景:微服务时代的扩展性挑战

在微服务架构中,系统需要频繁得进行功能扩展和模块解藕。假设你正在开发一个电商平台,需要支持多种支付方式(如支付宝、微信、银联),而这些支付方式的实现可能由不同的团队维护。**如何设计一个灵活的系统,使得新支付方式的接入无需修改核心代码?**这正是 SPI(Service Provider Interface) 机制的核心价值所在。

然而,当尝试使用 Java 原生的 SPI、SPring 的扩展机制 或 Dubbo 的分布式服务发现时,你可能会发现三者在实现方式、性能和适用场景上存在巨大差异。本文将深入解析这三种 SPI 机制的原理与区别,帮助你在世纪项目中做出更合理的技术选型。


二、SPI 介绍

2.1 什么是 SPI?

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

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

2.2 三种 SPI 的定位

框架 SPI类型 核心用途 扩展方式
Java 原生SPI JDK标准服务发现 META-INF/services/
Spring 扩展点机制 应用上下文扩展(如监听器) spring.factories
Dubbo 分布式SPI 服务治理与协议扩展 META-INF/services/META-INF/dubbo/internal/ META-INF/dubbo/META-INF/dubbo/external/

三、Java SPI 机制–ServiceLoader

3.1 使用约定

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

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

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

3.1 来个 demo

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

package com.demo.java;

public interface LoadBalance {
}
package com.demo.java.impl;

import com.demo.java.LoadBalance;

public class RandomLoadBalance implements LoadBalance {
}

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

测试类:

import java.util.Iterator;
import java.util.ServiceLoader;

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);
        }
    }
}

测试结果:

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

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

3.3 实现原理

如下是 Service Loader 中的一段核心代码:

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

然后通过 ClassLoader 获取到资源,其实就是接口的全限定名赌赢的资源,然后交给 parse() 方法解析资源:

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

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

所以其实不难发现 ServiceLoader 实现原理比较简单,总结起来就是:

通过 IO流读取 \`META-INF/services/接口的全限定名\` 文件的内容
反射实例化对象

3.4 优缺点

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

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

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

3.5 使用场景

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

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

四、Spring SPI 机制–SpringFactoriesLoader

4.1 使用约定

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

Spring 的 SPI 的约定如下:

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

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

4.2 来个 demo

META-INF/ 目录下创建 spring.factories 文件,LoadBalance 为键,RandomLoadBalance 为值,如下所示:

测试:

import com.demo.java.LoadBalance;
import org.springframework.core.io.support.SpringFactoriesLoader;

import java.util.List;

public class SpringFactoriesLoaderDemo {

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

Spring 依赖如下:

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>5.3.6</version>
</dependency>

运行结果:

成功获取到了实现对象。

4.3 核心原理

如下是 SpringFactoriesLoader 中的一段核心代码:

在这里插入图片描述

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

实现原理图如下:

graph LR
A[SpringFactoriesLoader.loadFactories] --> B[查找所有 Jar 包的<br>META-INF/spring.factories]
B --> C[根据传入的接口类型<br>查找对应的 Key]
C --> D[获取所有实现类的全限定名列表]
D --> E[反射实例化并排序<br>(支持 @Order 等注解)]
E --> F[返回指定的实现类列表]

4.4 使用场景

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

1)自动装配

在 SpringBoot 3.0 之前的版本,自动装配是通过 SpringFactoriesLoader 来加载的。

从 SpringBoot 启动到自动装配的调用链如下:

第1层:com.example.DemoApplication.main():10
第2层:org.springframework.boot.SpringApplication.run():1234
第3层:org.springframework.boot.SpringApplication.run():1256
第4层:org.springframework.boot.SpringApplication.run():347
第5层:org.springframework.boot.SpringApplication.refreshContext():412
第6层:org.springframework.context.support.AbstractApplicationContext.refresh():589
第7层:org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors():767
第8层:org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors():138
第9层:org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors():105
第10层:org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry():332
第11层:org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions():396
第12层:org.springframework.context.annotation.ConfigurationClassParser.parse():142
第13层:org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass():321
第14层:org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass():295
第15层:org.springframework.context.annotation.ConfigurationClassParser.processImports():646
第16层:org.springframework.context.annotation.ConfigurationClassParser.processImports():698
第17层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.process():52
第18层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getAutoConfigurationEntry():102
第19层:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getCandidateConfigurations():156
第20层:org.springframework.core.io.support.SpringFactoriesLoader.loadFactoryNames():229

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

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

2)PropertySourceLoader 的加载

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

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

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

4.5 与 Java SPI 机制对比

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

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

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

4.6 不足

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

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


五、Dubbo SPI 机制–ExtensionLoader

5.1 使用约定

ExtensionLoader 是 dubbo 的 SPI 机制的实现类。每一个接口都会有一个自己的 ExtensionLoader 实例对象,这点跟 Java 的 SPI 机制是一样的。

同样地,Dubbo 的 SPI 机制也做了以下几点约定:

  1. 接口必须要加 @SPI 注解;
  2. 配置文件可以放在 META-INF/sourvices/META-INF/dubbo/internal/META-INF/dubbo/META-INF/dubbo/external/ 这四个目录底下,文件名也是接口全限定名。
  3. 内容为键值对,键为短名称(可以理解为 Spring 中 Bean 的名称),值为实现类的全限定名。

5.2 来个 demo

首先在 LoadBalance 接口上添加 @SPI 注解:

package com.demo.dubbo;

import com.alibaba.dubbo.common.extension.SPI;

@SPI
public interface LoadBalance {
}
package com.demo.dubbo.impl;

import com.demo.dubbo.LoadBalance;

public class RandomLoadBalance implements LoadBalance {
}

Dubbo 依赖如下:

<!-- dubbo依赖 -->
<dependency>
    <groupId>com.alibaba.spring.boot</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

然后,修改一下 Java 的 SPI 机制测试时配置文件的内容,改为键值对。因为 Dubbo 的 SPI 机制也可以从 META-INF/services/ 目录下读取文件,而且配置文件也是根据接口的全限定名进行命名,所以配置文件如下:

random=com.demo.dubbo.impl.RandomLoadBalance

测试类:

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
        LoadBalance loadBalance = extensionLoader.getExtension("random");
        System.out.println("获取到 random 键对应的实现类对象:" + loadBalance);
    }
}

通过 ExtensionLoadergetExtension() 方法,传入短名称,这样就可以精确地找到短名称对应的实现类。

所以,从这里可以看出,Dubbo 的 SPI 机制解决了前面提到的无法获取指定实现类的问题

测试结果:

Dubbo 的 SPI 机制除了解决了无法获取指定实现类的问题,还提供了很多额外的功能,这些功能在 Dubbo 内部用的非常多,接下来就详细讲讲。

5.3 dubbo 核心机制

1)自适应机制

自适应自适应扩展类 的含义是说,基于参数,在运行时动态选择到具体的目标类,然后执行。

每个接口有且只有一个自适应类,通过 ExtensionLoadergetAdaptiveExtension() 方法就可以获取到这个类的对象,这个对象可以根据运行时具体的参数找到目标实现类对象,然后调用目标对象的方法。

举个例子,假设上面的 LoadBalance 有个自适应对象,那么获取到这个自适应对象之后,如果在运行期间传入了 random 这个 key,那么这个自适应对象就会找到 random 这个 key 对应的实现类,调用那个实现类的方法,如果动态传入了其它的 key,就路由到其它的实现类。

自适应类有两种方式产生,第一种就是自己指定,在接口的实现类上加 @Adaptive 注解,那么这个实现类就是自适应实现类。

@Adaptive
public class RandomLoadBalance implements LoadBalance {
}

除了自己代码指定,还有一种就是 dubbo 会根据一些条件帮你动态生成一个自适应类,生成过程比较复杂,这里就不展开了。

自适应机制在 dubbo 中用的非常多,而且很多都是自动生成的,如果你不知道 dubbo 的自适应机制,你在阅读源码的时候可能都不知道为什么代码可以走到那里。

2)IOC 和 AOP

一提到 IOCAOP,立马想到的都是 Spring,但是 IOC 和 AOP 并不是 Spring 特有的概念,Dubbo 也实现 IOC 和 AOP 的功能,但是是一个轻量级的。

2.1)依赖注入

Dubbo 依赖注入是通过 setter 注入的方式,注入的对象默认就是上面提到的自适应对象,在 Spring 环境下可以注入 Spring Bean。

public class RoundRobinLoadBalance implements LoadBalance {

  private LoadBalance loadBalance;
  
  public void setLoadBalance(LoadBalance loadBalance) {
    this.loadBalance = loadBalance;
  }
}

如上代码,RoundeRobinLoadBalance 中有一个 setLoadBalance 方法,参数 LoadBalace,在创建 RoundRobinLoadBalance 的时候,在非 Spring 环境底下,Dubbo 就会找到 LoadBalance 自适应对象然后通过反射注入。

这种方式在 Dubbo 中也很常见,比如下面的一个场景:

RegistryProtocol 中会注入一个 Protocol,其实这个注入的 Protocol 就是一个自适应对心啊哥。

2.2)接口回调

Dubbo 也提供了一些类似于 Spring 的一些接口的回调功能,比如说,如果你的类实现了 Filter 接口,那么过滤的时候就会调用以下的方法:

在 dubbo 3.x 的某个版本之后,dubbo 提供了更多接口的回调,比如说 ExtensionPostProcessor、ExtensionAccessorAware,命名跟 Spring 的非常相似,作用也差不多。

2.3)自动包装

自动包装 其实就是 AOP 的功能实现,对目标对象进行代理,并且这个 AOP 功能在默认情况下就是开启的。

在 Dubbo 的 SPI 接口的实现中,有一种特殊的类,被称为 Wrapper 类,这个类的作用就是来实现 AOP 的。

判断 Wrapper 类的唯一标准就是这个类中必须要有这么一个构造参数,这个构造方法的参数只有一个,并且参数类型就是接口的类型,如下代码:

package com.demo.dubbo.impl;

import com.demo.dubbo.LoadBalance;

public class RoundRobinLoadBalance implements LoadBalance {

    private final LoadBalance loadBalance;

    public RoundRobinLoadBalance(LoadBalance loadBalance) {
        this.loadBalance = loadBalance;
    }
}

此时 RoundRobinLoadBalance 就是一个 Wrapper 类。

当通过 random 获取 RandomLoadBalance 目标对象时,那么默认情况下就对 RandomLoadBalance 进行包装,真正获取到的其实是 RoundRobinLoadBalance 对象,RoundRobinLoadBalance 内部引用的对象是 RandomLoadBalance。

在配置文件中加入:

roundrobin=com.demo.dubbo.impl.RoundRobinLoadBalance

测试一下:

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class ExtensionLoaderDemo {

    public static void main(String[] args) {
        ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
        LoadBalance loadBalance = extensionLoader.getExtension("random");
        System.out.println("获取到 random 键对应的实现类对象:" + loadBalance);
    }
}

测试结果:

从结果可以看出,虽然指定了 random,但是实际获取到的是 RoundRobinLoadBalance,而 RoundRobinLoadBalance 内部引用了 RandomLoadBalance,如下所示:

如果有很多的包装类,那么就会形成一个责任链条,一个套一个。

所以 Dubbo 的 AOP 跟 Spring 的 AOP 实现是不一样的,Spring 的 AOP 底层是基于动态代理来的,而 Dubbo 的 AOP 其实算是静态代理,Dubbo 会帮你自动组装这个代理,形成一条责任链。

到这其实我们已经知道,Dubbo 的 SPI 接口的实现类已经有两种类型了:

  1. 自适应类;
  2. Wrapper类。

除了这两种类型,其实还有一种,叫做默认类,就是 @SPI 注解的值对应的实现类,,比如:

@SPI("random")
public interface LoadBalance {
}

此时 random 这个 key 对应的实现类就是默认实现,通过 getDefaultExtension() 这个方法就可以获取到默认实现对象。

3)自动激活

所谓的 自动激活,就是根据你的入参,动态地选择一批实现类返回给你。

自动激活的实现类上需要加上 Activate 注解,这里就又学习了一种实现类的分类。

@Activate
public interface RandomLoadBalance {
}

此时 RandomLoadBalance 就属于可以被自动激活的类。

获取自动激活类的方法是 getActivateExtension(),所以根据这个方法的入参,可以动态选择一批实现类。

自动激活这个机制在 Dubbo 一个核心的使用场景就是 Filter 过滤器链中。

Filter 是 dubbo 中的一个扩展点,可以在请求发起前或者是相应获取之后就进行拦截,作用有点像 Spring MVC 中的 HandlerInterceptor。

Filter 的一些实现类如下:

如上 Filter 有很多实现,所以为了能够区分 Filter 的实现是作用于 provider 端还是 consumer 端,所以就可以用自动激活的机制来根据入参动态选择一批 Filter 实现。

比如说 ConsumerContextFilter 这个 Filter 就作用于 Consumer 端。

实现原理图:

flowchart TD
    A["ExtensionLoader.getExtension('dubbo')"] --> B[扫描所有JAR包的<br>META-INF/dubbo/ 目录]
    B --> C{找到接口文件<br>org.apache.dubbo.rpc.Protocol?}
    C -- 是 --> D[读取文件内容, 解析Key-Value对]
    D --> E[根据Key 'dubbo'<br>定位实现类全限定名]
    E --> F[反射创建<br>DubboProtocol实例]
    F --> G[遍历setter方法, 进行依赖注入<br>(查找并注入其他扩展点)]
    G --> H[遍历所有Wrapper类<br>(AOP增强, 形成责任链)]
    H --> I[返回包装后的、<br>完整的DubboProtocol实例]
    C -- 否 --> J[抛出异常: Extension not found]

最后,这里并没有对dubbo的SPI机制进行源码分析,感兴趣的同学可以看一下**面试常问的dubbo的spi机制到底是什么?(上)**和 **面试常问的dubbo的spi机制到底是什么?(下)**两篇文章。


六、总结

通过以上分析可以看出,实现SPI机制的核心原理就是通过IO流读取指定文件的内容,然后解析,最后加入一些自己的特性。

为了更清晰地展示三者的区别,我们通过一个表格进行总结:

特性维度 Java SPI (ServiceLoader) Spring SPI (SpringFactoriesLoader) Dubbo SPI (ExtensionLoader)
配置文件路径 META-INF/services/ META-INF/spring.factories META-INF/services/META-INF/dubbo/internal/ META-INF/dubbo/META-INF/dubbo/external/
配置文件格式 全类名(每行一个) Key=Value1,Value2… (Properties格式) Name=Class (Properties格式)
加载方式 ServiceLoader.load() + 迭代器 SpringFactoriesLoader.loadFactories() ExtensionLoader.getExtension(name)
获取实现 加载所有实现 按接口类型加载所有实现 按名称获取指定实现
IoC/AOP支持 不支持 与Spring容器集成,支持IoC 支持IoC注入和Wrapper(AOP)
其他特性 支持排序(@Order) 自适应(@Adaptive)、自动激活(@Activate)
优点 JDK标准,简单 与Spring生态无缝集成,功能较Java SPI强 功能最强大,是高性能扩展框架的基石
缺点 笨重,无法按需加载,功能单一 依赖Spring容器 概念复杂,与Dubbo框架强绑定
典型应用场景 JDBC驱动加载等基础扩展 Spring Boot自动配置 Dubbo的所有组件扩展(协议、集群、负载均衡等)

结论

三者的SPI机制是一脉相承又不断进化的关系。

  • Java SPI 是基础,实现的比较简单,并没有什么其它功能。
  • Spring SPI 是在企业应用层面的一次优秀实践,得益于自身的ioc和aop的功能,但是也没有实现太复杂的SPI机制,仅仅是对Java做了一点简化和优化
  • Dubbo SPI 则是面向高性能RPC框架的终极进化形态。为了满足自身框架的使用要求,实现的功能就比较多,不仅将ioc和aop的功能集成到SPI机制中,还提供注入自适应和自动激活等功能。

整理完毕,完结撒花~🌻





参考地址:

1.阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别,https://zhuanlan.zhihu.com/p/614159801

2.Dubbo-SPI机制深度解析,https://juejin.cn/post/7485584242039242802

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐