开源软件:Dubbo 01 SPI

Dubbo 源码解析系列

Posted by Shoukai Huang on June 1, 2019

Java SPI

相关概念

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

整体机制图如下:

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

使用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略 比较常见的例子:

数据库驱动加载接口实现类的加载

  • JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载
  • SLF4J加载不同提供商的日志实现类
  • Spring:Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo:Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

安全问题

测试Java SPI时,获取 System.getSecurityManager() 返回 null

在程序启动参数上添加 -Djava.security.manager 后,得到如下错误

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.security.AccessControlException: access denied ("java.util.PropertyPermission" "idea.is.junit5" "read")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkPropertyAccess(SecurityManager.java:1294)
	at java.lang.System.getProperty(System.java:717)
	at com.intellij.rt.execution.junit.JUnitStarter.isJUnit5Preferred(JUnitStarter.java:179)
	at com.intellij.rt.execution.junit.JUnitStarter.processParameters(JUnitStarter.java:75)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:59)

处理方案为,编写Java Policy文件,命名:spi.policy,如:

1
2
3
grant {
    permission java.security.AllPermission;
};

JVM 启动参数添加如下内容,程序即可获取 System.getSecurityManager()

1
-Djava.security.policy=/……/spi.policy

Dubbo SPI

概念

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

示例

原理

改进

Dubbo改进了JDK标准的SPI的以下问题:

  • JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
  • 增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。

参考