jdk原生spi通过tccl一次性粗粒度破坏双亲委派以加载应用类,dubbo spi则按扩展点细粒度隔离类加载器,解决类冲突与多版本共存问题。

JDK 原生 SPI 和 Dubbo SPI 都需要突破双亲委派模型,但它们的“类加载破坏”目的、方式与粒度完全不同——不是谁更“彻底”,而是各自为解决不同问题而设计的适配策略。
Java SPI 的类加载破坏:只为加载第三方实现
Java SPI 的核心限制在于:启动类加载器(Bootstrap ClassLoader)无法加载应用路径下的 jar 包(如 mysql-connector-java.jar),而 JDBC Driver 接口又定义在 rt.jar 中。若严格遵循双亲委派,就无法动态加载用户提供的实现类。
解决方案是引入线程上下文类加载器(TCCL):
- ServiceLoader 在初始化时,主动获取
Thread.currentThread().getContextClassLoader() - 后续所有实现类的加载(
Class.forName(..., true, classLoader))均使用该 TCCL,而非调用方的类加载器 - 这相当于“临时委托给子类加载器”,绕过双亲委派链条中上层无法访问下层资源的问题
- 破坏是一次性的、全局的、粗粒度的:整个 ServiceLoader 实例及其迭代出的所有实现类,共用同一个 TCCL
Dubbo SPI 的类加载破坏:按扩展点精细隔离
Dubbo 并不直接依赖 TCCL 做统一加载,而是将类加载控制权下沉到每个 ExtensionLoader 实例内部,并结合当前扩展点的运行上下文动态决策:
- 每个 ExtensionLoader 在首次创建时,会快照式捕获当前线程的 TCCL,作为该扩展点的默认加载器
- 但真正加载某个扩展类(如
RandomLoadBalance)时,Dubbo 会优先尝试用该扩展类所在 jar 的类加载器(即其 defineClass 的 loader)来加载它自身及依赖类 - 若失败(如跨模块依赖),再 fallback 到 ExtensionLoader 记录的 loader 或当前 TCCL
- Wrapper 类、依赖注入对象(如其他 SPI 扩展或 Spring Bean)的加载,也按各自所属模块的类加载器策略处理,彼此隔离
- 破坏是按需的、分层的、细粒度的:不同扩展点、甚至同一扩展点的不同 Wrapper,可使用不同类加载器
关键差异对比:不只是“谁用了 TCCL”
二者本质区别不在是否“破坏”,而在破坏的意图和可控性:
- Java SPI 破坏的是加载“位置”:确保能从应用 classpath 加载实现,解决“找不到类”的问题
- Dubbo SPI 破坏的是加载“归属”:确保模块间类可见性隔离、避免 ClassCastException、支持热替换与多版本共存,解决“找错类”和“类冲突”的问题
- Dubbo 的 ExtensionLoader 内部封装了完整的类加载委派逻辑,包括对 OSGi、Spring Boot DevTools、自定义 ClassLoader 的兼容处理,远超 TCCL 的简单委托
- 当 Dubbo 运行在复杂容器(如 Tomcat、Arthas、模块化 JDK)中时,其类加载策略能主动识别环境并切换策略;Java SPI 则完全依赖用户手动设置 TCCL,极易出错
一个典型场景说明差异
假设项目同时引入了两个版本的序列化组件:fastjson-1.2.76.jar 和 fastjson-2.0.42.jar,且都提供了 Serialization SPI 实现:
- Java SPI 会把两个实现全加载进同一个 ServiceLoader,最终可能因类加载器冲突导致
NoClassDefFoundError或静态块重复执行 - Dubbo SPI 通过 ExtensionLoader 分别管理两个实现,每个 loader 绑定对应 jar 的类加载器,在实例化时自然隔离类型空间,还能通过
@Activate或 URL 参数精准路由到指定版本










