Java 与 Dubbo 的 SPI 机制介绍

Java SPI 机制

概念介绍

Java 原生支持 SPI 机制,具体介绍如下:

  • 核心概念
    • SPI(Service Provider Interface)是一种服务发现机制。
    • SPI 的核心思想是定义一个接口,由多个实现类提供不同的实现方式,在系统运行时根据配置或者默认策略,动态加载并使用具体的实现类。
    • SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样就可以在运行时,动态为接口替换实现类。
  • 工作原理

    • (1) 接口与实现类
      • 假设有一个接口 A,它有多个实现类:A -> A1、A2、A3
    • (2) 配置实现类
      • 可以在配置文件中指定接口 A 对应使用哪个实现类。
    • (3) 运行时加载
      • 程序启动时,会读取配置文件,根据配置信息找到对应的实现类,实例化并使用该对象。
    • (4) Java 原生 SPI 机制
      • Java 原生 SPI 机制的使用要求:
        • resources/META-INF/services/ 目录下,创建一个与接口全限定名相同的文件,例如:resources/META-INF/services/com.example.service.A
        • 文件内容的格式是一行一个实现类的全限定名(可以有多行,即支持多个不同的实现类),例如:com.example.service.impl.A1
        • 运行时通过 ServiceLoader 等工具扫描依赖的 Jar 包,在其中查找该文件,并加载指定的实现类,比如:ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
  • 应用场景

    • SPI 机制常用于插件式扩展。
    • 比如:如果你在开发一个框架,可以通过 SPI 让外部开发者编写插件,扩展框架的功能,而不必修改框架的源码。
  • 典型案例

    • JDBC
      • Java 标准库只定义了一套 JDBC 接口,并没有真正的实现。
      • 数据库厂商(如 MySQL、Oracle)会提供自己的实现,并通过 SPI 机制声明在 resources/META-INF/services/ 目录中。
      • 运行时,Java 会根据项目引入的数据库驱动 Jar 包,自动找到对应的 JDBC 实现类。

Dubbo SPI 机制

概念介绍

Dubbo 借鉴了 SPI 思想,但没有直接使用 Java 原生的 SPI 机制,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader 类可以加载指定的实现类。

  • 工作原理

    • (1) 接口声明
      • 在 Dubbo 中,如果某个接口需要支持 SPI 扩展,就会加上 @SPI 注解,比如 Protocol 接口:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        // 接口定义
        @SPI("dubbo")
        public interface Protocol {

        int getDefaultPort();

        @Adaptive
        <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

        @Adaptive
        <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

        void destroy();
        }

        // 加载实现类
        Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
      • @SPI("dubbo") 表示默认实现是 dubbo
      • @Adaptive 表示该方法会生成代理逻辑,运行时根据参数动态选择实现类。
    • (2) 实现类配置
      • Dubbo 在自己 Jar 包中的 resources/META-INF/dubbo/internal/ 路径下提供了一个配置文件,文件名是接口的全限定名,比如:
        1
        resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol
      • 配置文件的内容是 key=实现类的全限定名,key 对应 @SPI 注解中的扩展名称,比如:
        1
        2
        3
        dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
        http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
        hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
    • (3) 默认实现加载
      • 如果用户没有配置扩展,Dubbo 会根据 @SPI("dubbo") 的默认值 dubbo,从配置文件中加载对应的实现类 DubboProtocol
      • 这也是 Dubbo 默认使用 Dubbo 协议作为 RPC 通信协议的原因。
    • (4) 动态切换实现类
      • Protocol 接口中,有两个方法加了 @Adaptive 注解。
      • Dubbo 会在运行时生成代理类,在代理方法内部根据传入的 URL 参数的 protocol 值决定使用哪个实现类。
      • 如果 URL 参数中没指定协议,就用默认的 dubbo;如果指定了其他值(如 http),则加载对应的实现类。
  • 实现特点

    • 微内核 + 可插拔:保留一个接口和多个实现,运行时可替换。
    • 组件化:如 Protocol 负责 RPC 调用,可以替换为自定义的 RPC 组件。
    • 动态扩展:可通过 URL 参数或配置文件,在运行时动态切换实现类。
    • 增强的 SPI:相比 Java 原生的 SPI 机制,Dubbo 的实现支持:
      • 支持指定默认的实现类(@SPI 注解的默认值)
      • 运行时动态选择实现类(@Adaptive
      • 接口定义(扩展点)自动生成代理类
  • 使用总结

    • Dubbo SPI 本质上是一个运行时可扩展、可替换的组件机制。
    • Dubbo 大量核心组件(如 ProtocolClusterRegistry 等)都是用这种 SPI 机制实现扩展的。
    • 通过 @SPI 注解 + 配置文件来确定默认的实现类,通过 @Adaptive 注解 + URL 参数来实现动态切换实现类。
    • Dubbo SPI 的扩展文件路径(点击查看源码定义
      • 与 Java SPI 的不一样,Dubbo SPI 提供给开发者使用的扩展文件是 resources/META-INF/dubbo/接口全限定名,文件内容是 key=实现类的全限定名
      • Dubbo 自己内部使用的扩展文件是 resources/META-INF/dubbo/internal/接口全限定名,文件内容是 key=实现类的全限定名

加载流程

Dubbo SPI 机制加载扩展的核心步骤:

  • (1) 读取并解析配置文件
  • (2) 缓存所有扩展实现类
  • (3) 基于用户执行的扩展名,实例化对应的扩展实现类
  • (4) 执行扩展实例属性的 IOC 注入(基于 Setter 注入),以及实例化扩展的包装类,实现 AOP 特性

Dubbo SPI 机制加载扩展的整个流程:

核心注解

Dubbo SPI 机制有两个核心注解,分别是 @Adaptive@Activate

  • @Adaptive 注解(自适应)
    • 主要作用
      • 表示该扩展类或接口方法需要自适应扩展,Dubbo 会在运行时根据 URL 或其他条件动态选择具体扩展实现。
    • 常见使用方式
      • @Adaptive 标注在接口方法上(最常见)
        • 表示由 Dubbo 自动生成 Xxx$Adaptive 代理类
        • 方法内部会根据 URL 参数在运行时选择具体扩展实现
        • 使用例子:
          1
          2
          3
          4
          5
          6
          public interface Protocol {

          @Adaptive({"protocol", "defaultProtocol"})
          Exporter export(Invoker invoker);

          }
          • 当上面调用 export() 方法时,Dubbo 会根据 URL 的 protocoldefaultProtocol 参数决定使用哪个协议实现
      • @Adaptive 标注在扩展类上(较少使用)
        • 表示该扩展类是一个手工编写的自适应扩展类
        • Dubbo 将直接使用这个扩展类,而不会再自动生成代理类
        • 一般用于非常复杂的场景,比如需要手写逻辑替代 Dubbo 自动生成代理类
        • 目前 Dubbo 中仅有两个扩展类标注了 @Adaptive,分别是 AdaptiveCompilerAdaptiveExtensionFactory,表示这些扩展类的加载逻辑由人工编码(静态编码)完成
        • 使用例子:
          1
          2
          3
          4
          5
          6
          7
          8
          9
          @Adaptive
          public class CustomAdaptiveCompiler implements Compiler {

          @Override
          public Class<?> compile(String code, ClassLoader loader) {
          // 自定义代码编译逻辑
          }

          }
    • 使用注意事项
      • 接口上不能标注 @Adaptive
      • 接口方法上可以标注 @Adaptive,表示使用 Dubbo 自动生成的代理类,适用大多数场景
      • 扩展类上可以标注 @Adaptive,表示使用手写的自适应逻辑,适用特殊复杂场景

  • @Activate 注解(自动激活)
    • 主要作用
      • 表示当扩展类被自动加载时,满足 groupvalueorder 等条件,该扩展会自动加入扩展链,无需手动在配置中指定扩展名。
    • 常见使用方式
      • @Activate 标注在扩展类上(最常见)
        • 扩展类在满足 Activate 条件时自动激活
        • 一般用于 Filter、Router、ExporterListener、Registry 等扩展点
        • 使用例子:
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          @Activate(group = {"provider"}, order = 10)
          public class MyProviderFilter implements Filter {

          @Override
          public Result invoke(Invoker<?> invoker, Invocation invocation) {
          // 自定义过滤逻辑
          return invoker.invoke(invocation);
          }

          }
      • @Activate 标注在接口方法上(较少使用)
        • 表示该接口方法返回的扩展类实例在调用时可自动激活
        • 可结合 URL 参数或 groupvalueorder 条件进行动态控制
        • 使用例子:
          1
          2
          3
          4
          5
          6
          public interface SomeFactory {

          @Activate(value = {"feature"}, group = "consumer")
          Extension create();

          }
    • 典型使用 @Activate 自动激活的扩展点(最常见场景)
      • Filter 自动生效(过滤器)
      • Router 自动生效(路由)
      • ExporterListener 自动生效(监听服务暴露)
      • Registry(如注册中心相关监听、通知)自动生效
    • 可使用 @Activate 激活,但通常是通过 URL 指定或者明确选择的的扩展点
      • Cluster(有些装饰逻辑可能自动封装,但多数是由 Dubbo 主流程显式选择)
      • Protocol(支持 @Activate,但较少用于自动激活,一般是 URL 或显式选择)
      • ProxyFactory(可自动激活,但通常通过指定实现选择)
    • 使用注意事项
      • 接口上不能标注 @Activate

总结

  • @Adaptive(自适应)的作用:让扩展在运行时根据 URL 或条件动态选择具体扩展实现;标注在方法上,则表示由 Dubbo 自动生成代理类;标注在类上,则表示使用手写的自适应逻辑。
  • @Activate(自动激活)的作用:让扩展类在满足 Activate 条件时自动激活加入调用链;可以省略,但省略后不会自动激活,必须通过扩展名主动加载,例如: extensionLoader.getExtension("customizedProtocol");

参考资料