一、概述
SPI(Service Provider Interface),是Java内置的一种服务提供发现机制,可以用来提高框架的扩展性,主要用于框架的开发中,比如Dubbo,不同框架中实现略有差异,但核心机制相同,而Java的SPI机制可以为接口寻找服务实现。SPI机制将服务的具体实现转移到了程序外,为框架的扩展和解耦提供了极大的便利。
得益于SPI优秀的能力,为模块功能的动态扩展提供了很好的支撑。
本文会先简单介绍Java内置的SPI和Dubbo中的SPI应用,重点介绍分析Spring中的SPI机制,对比Spring SPI和Java内置的SPI以及与 Dubbo SPI的异同。
二、Java SPI
Java内置的SPI通过java.util.ServiceLoader类解析classPath和jar包的META-INF/services/目录 下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。
2.1 Java SPI
先通过代码来了解下Java SPI的实现
① 创建服务提供接口
package jdk.spi; // 接口 public interface DataBaseSPI { public void dataBaseOperation(); }
② 创建服务提供接口的实现类
- MysqlDataBaseSPIImpl
实现类1
package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI { @Override public void dataBaseOperation() { System.out.println("Operate Mysql database!!!"); } }
- OracleDataBaseSPIImpl
实现类2
package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI { @Override public void dataBaseOperation() { System.out.println("Operate Oracle database!!!"); } }
③ 在项目META-INF/services/目录下创建jdk.spi.DataBaseSPI文件
jdk.spi.DataBaseSPI
jdk.spi.impl.MysqlDataBaseSPIImpl jdk.spi.impl.OracleDataBaseSPIImpl
④ 运行代码:
JdkSpiTest#main()
package jdk.spi; import java.util.ServiceLoader; public class JdkSpiTest { public static void main(String args[]){ // 加载jdk.spi.DataBaseSPI文件中DataBaseSPI的实现类(懒加载) ServiceLoader<DataBaseSPI> dataBaseSpis = ServiceLoader.load(DataBaseSPI.class); // ServiceLoader实现了Iterable,故此处可以使用for循环遍历加载到的实现类 for(DataBaseSPI spi : dataBaseSpis){ spi.dataBaseOperation(); } } }
⑤ 运行结果:
Operate Mysql database!!! Operate Oracle database!!!
2.2 源码分析
上述实现即为使用Java内置SPI实现的简单示例,ServiceLoader是Java内置的用于查找服务提供接口的工具类,通过调用load()方法实现对服务提供接口的查找(严格意义上此步并未真正的开始查找,只做初始化),最后遍历来逐个访问服务提供接口的实现类。
上述访问服务实现类的方式很不方便,如:无法直接使用某个服务,需要通过遍历来访问服务提供接口的各个实现,到此很多同学会有疑问:
- Java内置的访问方式只能通过遍历实现吗?
- 服务提供接口必须放到META-INF/services/目录下?是否可以放到其他目录下?
在分析源码之前先给出答案:两个都是的;Java内置的SPI机制只能通过遍历的方式访问服务提供接口的实现类,而且服务提供接口的配置文件也只能放在META-INF/services/目录下。
ServiceLoader部分源码
public final class ServiceLoader<S> implements Iterable<S>{ // 服务提供接口对应文件放置目录 private static final String PREFIX = "META-INF/services/"; // The class or interface representing the service being loaded private final Class<S> service; // 类加载器 private final ClassLoader loader; // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; // 按照初始化顺序缓存服务提供接口实例 private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 内部类,实现了Iterator接口 private LazyIterator lookupIterator; }
从源码中可以发现:
- ServiceLoader类本身实现了Iterable接口并实现了其中的iterator方法,iterator方法的实现中调用了LazyIterator这个内部类中的方法,解析完服务提供接口文件后最终结果放在了Iterator中返回,并不支持服务提供接口实现类的直接访问。
- 所有服务提供接口的对应文件都是放置在META-INF/services/目录下,final类型决定了PREFIX目录不可变更。
所以Java内置的SPI机制思想是非常好的,但其内置实现上的不足也很明显。
三、Dubbo SPI
Dubbo SPI沿用了Java SPI的设计思想,但在实现上有了很大的改进,不仅可以直接访问扩展类,而且在访问的灵活性和扩展的便捷性都做了很大的提升。
3.1 基本概念
① 扩展点
一个Java接口,等同于服务提供接口,需用@SPI注解修饰。
② 扩展
扩展点的实现类。
③ 扩展类加载器:ExtensionLoader
类似于Java SPI的ServiceLoader,主要用来加载并实例化扩展类。一个扩展点对应一个扩展加载器。
④ Dubbo扩展文件加载路径
Dubbo框架支持从以下三个路径来加载扩展类:
- META-INF/dubbo/internal
- META-INF/dubbo
- META-INF/services
Dubbo框架针对三个不同路径下的扩展配置文件对应三个策略类:
- DubboInternalLoadingStrategy
- DubboLoadingStrategy
- ServicesLoadingStrategy
三个路径下的扩展配置文件并没有特殊之处,一般情况下:
- META-INF/dubbo对开发者开放
- META-INF/dubbo/internal 用来加载Dubbo内部的扩展点
- META-INF/services 兼容Java SPI
⑤ 扩展配置文件
和Java SPI不同,Dubbo的扩展配置文件中扩展类都有一个名称,便于在应用中引用它们。
如:Dubbo SPI扩展配置文件
#扩展实例名称=扩展点实现类 adaptive=org.apache.dubbo.common.compiler.support.AdaptiveCompiler jdk=org.apache.dubbo.common.compiler.support.JdkCompiler javassist=org.apache.dubbo.common.compiler.support.JavassistCompiler
3.2 Dubbo SPI
先通过代码来演示下 Dubbo SPI 的实现。
① 创建扩展点(即服务提供接口)
扩展点
package dubbo.spi; import org.apache.dubbo.common.extension.SPI; @SPI // 注解标记当前接口为扩展点 public interface DataBaseSPI { public void dataBaseOperation(); }
② 创建扩展点实现类
- MysqlDataBaseSPIImpl
扩展类1
package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI { @Override public void dataBaseOperation() { System.out.println("Dubbo SPI Operate Mysql database!!!"); } }
- OracleDataBaseSPIImpl
扩展类2
package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI { @Override public void dataBaseOperation() { System.out.println("Dubbo SPI Operate Oracle database!!!"); } }
③在项目META-INF/dubbo/目录下创建dubbo.spi.DataBaseSPI文件:
dubbo.spi.DataBaseSPI
#扩展实例名称=扩展点实现类 mysql = dubbo.spi.impl.MysqlDataBaseSPIImpl oracle = dubbo.spi.impl.OracleDataBaseSPIImpl
PS:文件内容中,等号左边为该扩展类对应的扩展实例名称,右边为扩展类(内容格式为一行一个扩展类,多个扩展类分为多行)
④ 运行代码:
DubboSpiTest#main()
package dubbo.spi; import org.apache.dubbo.common.extension.ExtensionLoader; public class DubboSpiTest { public static void main(String args[]){ // 使用扩展类加载器加载指定扩展的实现 ExtensionLoader<DataBaseSPI> dataBaseSpis = ExtensionLoader.getExtensionLoader(DataBaseSPI.class); // 根据指定的名称加载扩展实例(与dubbo.spi.DataBaseSPI中一致) DataBaseSPI spi = dataBaseSpis.getExtension("mysql"); spi.dataBaseOperation(); DataBaseSPI spi2 = dataBaseSpis.getExtension("oracle"); spi2.dataBaseOperation(); } }
⑤ 运行结果:
Dubbo SPI Operate Mysql database!!! Dubbo SPI Operate Oracle database!!!
从上面的代码实现直观来看,Dubbo SPI在使用上和Java SPI比较类似,但也有差异。
相同:
- 扩展点即服务提供接口、扩展即服务提供接口实现类、扩展配置文件即services目录下的配置文件 三者相同。
- 都是先创建加载器然后访问具体的服务实现类,包括深层次的在初始化加载器时都未实时解析扩展配置文件来获取扩展点实现,而是在使用时才正式解析并获取扩展点实现(即懒加载)。
不同:
- 扩展点必须使用@SPI注解修饰(源码中解析会对此做校验)。
- Dubbo中扩展配置文件每个扩展(服务提供接口实现类)都指定了一个名称。
- Dubbo SPI在获取扩展类实例时直接通过扩展配置文件中指定的名称获取,而非Java SPI的循环遍历,在使用上更灵活。
3.3 源码分析
以上述的代码实现作为源码分析入口,了解下Dubbo SPI是如何实现的。
ExtensionLoader
① 通过ExtensionLoader.getExtensionLoader(Classtype)创建对应扩展类型的扩展加载器。
ExtensionLoader#getExtensionLoader()
public static <T> Extension