Dubbo SPI扩展机制详解
Dubbo SPI扩展机制详解定位Dubbo 第 03 篇核心机制篇讲透 Dubbo 的扩展点加载机制——这是理解 Dubbo 一切可插拔能力的钥匙适用版本Dubbo 3.xJDK 8/17目录一、SPI 设计动机二、ExtensionLoader 核心机制三、Adaptive 自适应扩展四、Activate 条件激活五、IOC 与 AOPWrapper六、自定义扩展实战七、总结八、常见高频面试题一、SPI 设计动机1.1 框架为什么必须可插拔回看 02 篇的调用链协议、序列化、集群容错、负载均衡、代理工厂、过滤器——每一环都存在多种合理选择而且不同团队会有自定义诉求。框架不可能写死任何一个因此需要一个统一的面向扩展点编程的机制框架定义扩展点接口如 LoadBalance ↕ SPI 机制按配置/上下文选择实现 业务/第三方提供具体实现如自研一致性哈希SPIService Provider Interface就是这套接口与实现分离、运行时动态装配的机制。1.2 Java 原生 SPI 的不足Java 自带ServiceLoaderMETA-INF/services/ 迭代器但有硬伤问题后果全量加载一次实例化所有实现浪费资源某个实现类加载失败会导致全部失败无法按名获取只能遍历不能给我 random 那个无依赖注入扩展实现之间无法互相依赖无包装能力无法给扩展统一加装饰器对 RPC 框架来说按需、按名、可注入、可包装都是刚需所以 Dubbo 自研了一套增强 SPI。1.3 Dubbo SPI 的增强点Java SPI Dubbo SPI ───────────── ───────────────────────────── 全量加载 → 懒加载 三级缓存 只能遍历 → 按 key 精确获取 无默认值 → SPI(default) 指定默认实现 无动态选择 → Adaptive 按 URL 参数动态选 无条件启用 → Activate 按场景批量激活 无依赖注入 → setter 自动 IOC Wrapper AOP二、ExtensionLoader 核心机制2.1 一个扩展点一个 LoaderExtensionLoader是 SPI 的中枢每个扩展点接口对应一个实例ExtensionLoaderLoadBalanceloaderExtensionLoader.getExtensionLoader(LoadBalance.class);LoadBalancelbloader.getExtension(random);// 按名取单例LoadBalancedftloader.getDefaultExtension();// 取 SPI 默认2.2 扩展文件放在哪按优先级从三个目录扫描都在 classpath 下目录用途META-INF/dubbo/internal/Dubbo 框架内部扩展META-INF/dubbo/用户自定义扩展的标准位置META-INF/services/兼容 Java 原生 SPI 格式文件名 扩展点接口的全限定名内容 key实现类全限定名# META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance randomorg.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobinorg.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance leastactiveorg.apache.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance这个 key 就是配置里写的名字——loadbalance roundrobin就是在这里匹配的。这也解释了配置值从哪来全部来自 SPI 文件的 key。2.3 加载流程与缓存第一次 getExtension(random) ▼ 检查缓存 → 未命中 ▼ 扫描三个目录找到同名文件 ▼ 解析出 key→类名 映射只存 Class不实例化 ▼ 按 key 找到类 → 实例化单例 ▼ IOC 注入 Wrapper 包装见第五节 ▼ 放入实例缓存返回缓存分三层扩展类缓存Class 级、扩展实例缓存已创建的单例、自适应适配器缓存Adaptive 生成物下节讲。加载过程加锁保证并发安全。懒加载是关键文件里写了 20 个实现只会实例化真正用到的那个——这正是对 Java SPI全量加载的修正。2.4 SPI 注解指定默认实现SPI(random)// 默认实现是 key 为 random 的那个publicinterfaceLoadBalance{TInvokerTselect(ListInvokerTinvokers,...);}配置没指定时用默认getDefaultExtension()也返回它。三、Adaptive 自适应扩展3.1 解决什么问题有些扩展点的实现选择依赖运行时参数。例如负载均衡同一个LoadBalance引用不同请求可能要用不同算法——选择依据在 URL 参数loadbalance里。但生成代理/包装集群时URL 还没确定此时无法选定实现。解决思路不提前选定而是生成一个适配器把选择推迟到真正调用时从方法参数里的 URL 现场决定。3.2 两种用法用法一注解在方法上最常用——由 Dubbo 自动生成适配器SPI(random)publicinterfaceLoadBalance{Adaptive// 标注在方法上TInvokerTselect(ListInvokerTinvokers,URLurl,Invocationinv);}生成的适配器逻辑等价于publicInvokerselect(Listinvokers,URLurl,Invocationinv){// ① 从 URL 取参数决定用哪个实现key 默认是接口小写名 loadbalanceStringnameurl.getParameter(loadbalance,random);// ② 按名从 ExtensionLoader 拿真实例LoadBalancerealExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(name);// ③ 委托执行returnreal.select(invokers,url,inv);}约束方法参数里必须能拿到 URL直接参数或可从参数对象getUrl()获取否则无法生成适配器。用法二注解在类上手工适配器——用于逻辑复杂、自动生成覆盖不了的场景Adaptive// 标注在类上这个类本身就是适配器publicclassAdaptiveProtocolimplementsProtocol{// 内部自行实现按 url.getProtocol() 委托到具体 Protocol的逻辑}Protocol、Transporter等核心扩展点用的是这种手工适配器。3.3 自适应扩展的意义没有 Adaptive扩展必须在启动时定死 → 无法按服务/按请求动态切换 有了 Adaptive扩展选择推迟到调用期 → 多服务共享同一框架却各用各的实现这就是一个 JVM 里 A 服务用 random、B 服务用一致性哈希能成立的原因——每次调用现场读各自 URL 的参数。四、Activate 条件激活4.1 场景一次取一组扩展Adaptive是选一个Activate是按条件激活一批——典型场景是 Filter 链提供方要装一批入口过滤器消费方要装另一批还要根据 URL 里有没有某参数决定是否启用。4.2 注解与获取Activate(group{CommonConstants.PROVIDER,CommonConstants.CONSUMER},valuetoken)// 仅当 URL 含 token 参数时激活publicclassTokenFilterimplementsFilter{...}// 框架内部按组取全部激活的 FilterListFilterfiltersloader.getActivateExtension(url,filter,group);参数含义注解参数作用group只在指定侧激活PROVIDER / CONSUMER / 两者valueURL 中存在这些参数名才激活条件开关order链内排序调用方的三个入参url条件来源、keyURL 里额外手工指定的扩展名列表如filter myFilter、group当前是提供方还是消费方。4.3 自定义扩展如何混入内置链自定义 Filter 想插进默认链两种方式类上加Activate(group ...)全场景自动生效不加注解通过配置filter myFilter显式挂载此时按配置中myFilter与-前缀组合决定增删。五、IOC 与 AOPWrapper5.1 SPI 的 IOCsetter 自动注入扩展实例创建后Dubbo 扫描它的 setter 方法按参数类型从其他扩展点的 Loader里找实例注入publicclassMyFilterimplementsFilter{privateProtocolprotocol;// Dubbo 看到 setProtocol(Protocol)自动注入 Protocol 的自适应实例publicvoidsetProtocol(Protocolprotocol){this.protocolprotocol;}}这让扩展实现之间可以互相依赖Filter 依赖 Protocol、Router 依赖 LoadBalance而无需自己 new 或依赖 Spring 容器。5.2 SPI 的 AOPWrapper 包装约定一个类若实现了扩展点接口 X且构造函数接收 X 类型参数类名为XxxWrapper则被识别为包装器。真实实现RandomLoadBalance 包装器 LoadBalanceWrapper(LoadBalance inner) ← 持有原实现前后加逻辑加载时自动套上getExtension(random) → new RandomLoadBalance() → IOC 注入 → 被 LoadBalanceWrapper 包裹若存在可多层嵌套 → 返回最外层效果等同装饰器模式统一加日志/监控/校验不侵入每个实现。框架内部对 Protocol 就有这样的 Wrapper加入配置合并、注册逻辑等。5.3 完整装配顺序类加载 → 实例化 → setter IOC 注入 → Wrapper 层层包装 → 缓存六、自定义扩展实战以自定义负载均衡为例完整四步6.1 实现扩展接口publicclassIpHashLoadBalanceextendsAbstractLoadBalance{OverrideprotectedTInvokerTdoSelect(ListInvokerTinvokers,URLurl,Invocationinvocation){// 按调用方 IP 哈希同一来源固定打到同一台简例StringclientIpRpcContext.getServiceContext().getRemoteHost();intindexMath.abs(clientIp.hashCode())%invokers.size();returninvokers.get(index);}}6.2 写 SPI 配置文件# META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance iphashcom.demo.lb.IpHashLoadBalance文件位置自己工程的src/main/resources/META-INF/dubbo/文件名即扩展点接口全限定名。6.3 配置引用DubboReference(loadbalanceiphash)privateGreetingServicegreetingService;或全局dubbo.consumer.load-balanceiphash。6.4 验证与进阶验证调用时断点打在doSelect确认命中覆盖内置配置文件用相同 key如random...即可替换默认实现——这是不换配置值、只换实现的手段慎用其他高频自定义扩展点Filter横切逻辑、Router灰度路由、Cluster自定义容错、Protocol私有协议接入。七、总结SPI 是 Dubbo 一切可插拔能力的地基协议、序列化、容错、负载均衡、代理、过滤器全部是扩展点记住配置里的值 SPI 文件里的 key。ExtensionLoader一个扩展点一个 Loader从META-INF/dubbo(/internal)与services三目录扫描懒加载 三级缓存按 key 精确取单例SPI指定默认。Adaptive把实现选择推迟到调用期从方法参数的 URL 现场决定实现同一框架、每服务各选各的方法上标注自动生成适配器类上标注则手写适配器。Activate按 group/value 条件批量激活扩展是 Filter 链装配的机制自定义 Filter 可注解自动挂载或配置显式挂载。IOC Wrapper AOP让扩展之间可依赖、可统一增强装配顺序为实例化 → 注入 → 包装。自定义扩展四步实现接口 → 写META-INF/dubbo/配置 → 配置引用 key → 验证同 key 可覆盖内置实现。八、常见高频面试题1. Dubbo SPI 和 Java SPI 有什么区别要点文件位置不同META-INF/dubbo vs META-INF/services且 Dubbo 用 key类名格式支持按名获取加载策略不同——Java SPI 全量实例化Dubbo 懒加载加缓存Dubbo 额外提供 SPI 默认值、Adaptive 动态选择、Activate 条件激活、setter IOC 注入与 Wrapper AOP。本质区别Java SPI 面向发现所有实现Dubbo SPI 面向按需装配一个/一组实现。2. Adaptive 注解的原理是什么为什么方法参数里必须有 URL要点Adaptive 标注方法时Dubbo 用字节码生成自适应适配器类调用时先从参数中的 URL 取出扩展名key 默认是接口小写名再向 ExtensionLoader 按名获取真实例并委托执行。选择推迟到调用期所以同一扩展点可被不同服务按各自 URL 参数动态选用。必须有 URL 是因为实现选择的依据就在 URL 参数里没有 URL 就无从决定用哪个实现。3. ExtensionLoader 是怎么加载扩展的缓存如何组织要点每个扩展点接口对应一个 ExtensionLoader扫描 META-INF/dubbo/internal、META-INF/dubbo、META-INF/services 三目录下以接口全限定名命名的文件解析 key→类名映射懒加载第一次按 key 获取才实例化缓存三层——扩展类缓存、实例缓存单例、自适应适配器缓存加载加锁保证并发安全。4. Activate 是干什么的Filter 链怎么用它装配要点Activate 用于按条件批量激活扩展典型是 Filter。注解参数 group 限定提供方/消费方侧value 表示 URL 需含指定参数才激活order 控制排序。框架通过 getActivateExtension(url, key, group) 收集所有满足条件的 Filter与配置里手工指定的合并成最终链。自定义 Filter 加 Activate 可全场景生效或用配置filter xxx显式挂载。5. Dubbo SPI 如何实现依赖注入IOC要点扩展实例创建后ExtensionLoader 扫描其 setter 方法按参数类型找到对应扩展点的 Loader取其自适应实例注入。这让 Filter 可以注入 Protocol、Router 可以注入其他扩展扩展实现间解耦于 Spring 容器之外。注意它只支持 setter 注入不支持构造器注入。6. 什么是 WrapperDubbo SPI 的 AOP 怎么实现要点实现扩展点接口、构造函数接收同类型参数、命名为 XxxWrapper 的类会被识别为包装器加载真实扩展后自动用 Wrapper 层层包裹装饰器模式调用先经过 Wrapper 再进真实实现。用于统一横切如 Protocol 的 Wrapper 里做配置合并、注册逻辑不侵入各实现。7. 如何写一个自定义的负载均衡策略并让它生效要点四步——① 继承 AbstractLoadBalance 实现 doSelect② 在 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance 文件里写自定义key全限定类名③ 配置引用DubboReference(loadbalance“key”) 或全局配置④ 断点/日志验证命中。若用与内置相同的 key 会覆盖内置实现。8. 为什么 Dubbo 里配置值如 loadbalancerandom能直接对应到实现类要点这些值就是 SPI 配置文件中的 key。ExtensionLoader 解析META-INF/dubbo/下以接口全限定名命名的文件得到 key→实现类映射配置指定 key 后Adaptive 适配器或调用处按 key 向 Loader 获取对应实例。所以新增一个配置可选值等价于注册一个 SPI 扩展。9. SPI 注解的 value 有什么作用要点指定该扩展点的默认实现 key。当配置未显式指定时如没配 loadbalance框架用默认值LoadBalance 默认 “random”getDefaultExtension() 返回该实现。注意 SPI 只定义默认值本身不启用自适应动态选择还需要 Adaptive。10. Dubbo 的扩展机制和 Spring 的 Bean 机制能互相替代吗要点定位不同不能简单替代。Dubbo SPI 服务于框架内部扩展点按 key 装配、支持自适应/条件激活、不依赖 Spring 容器Dubbo 可脱离 Spring 使用Spring 负责业务 Bean 的生命周期与业务层 DI。二者衔接点Dubbo 的 Spring Boot Starter 把 DubboService/DubboReference 桥接到 Spring 容器业务实现类仍是 Spring Bean而其内部调用链上的扩展装配走 SPI。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →