SPI 全称为 Service Provider Interface,是一种服务发现机制。
SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
1 Java SPI 示例本节通过一个示例演示 Java SPI 的使用方法。首先,我们定义一个接口,名称为 Robot。
public interface Robot { void sayHello();}接下来定义两个实现类,分别为 OptimusPrime 和 Bumblebee。
public OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } }public Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } }接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:
org.apache.spi.OptimusPrimeorg.apache.spi.Bumblebee做好所需的准备工作,接下来编写代码进行测试。
public JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class); System.out.println("Java SPI"); // 1. forEach 模式 serviceLoader.forEach(Robot::sayHello); // 2. 迭代器模式 Iterator<Robot> iterator = serviceLoader.iterator(); while (iterator.hasNext()) { Robot robot = iterator.next(); //System.out.println(robot); //robot.sayHello(); } }}最后来看一下测试结果,如下 :
2 经典 Java SPI 应用 : JDBC DriverManager在JDBC4.0 之前,我们开发有连接数据库的时候,通常先加载数据库相关的驱动,然后再进行获取连接等的操作。
// STEP 1: Register JDBC driverClass.forName("com.mysql.jdbc.Driver");// STEP 2: Open a connectionString url = "jdbc:xxxx://xxxx:xxxx/xxxx";Connection conn = DriverManager.getConnection(url,username,password);JDBC4.0之后使用了 Java 的 SPI 扩展机制,不再需要用 Class.forName("com.mysql.jdbc.Driver") 来加载驱动,直接就可以获取 JDBC 连接。
接下来,我们来看看应用如何加载 MySQL JDBC 8.0.22 驱动:
首先 DriverManager类是驱动管理器,也是驱动加载的入口。
/** * Load the initial JDBC drivers by checking the System property * jdbc.properties and then use the {@code ServiceLoader} mechanism */static { loadInitialDrivers(); println("JDBC DriverManager initialized");}在 Java 中,static 块用于静态初始化,它在类被加载到 Java 虚拟机中时执行。
静态块会加载实例化驱动,接下来我们看看loadInitialDrivers 方法。
加载驱动代码包含四个步骤:
系统变量中获取有关驱动的定义。使用 SPI 来获取驱动的实现类(字符串的形式)。遍历使用 SPI 获取到的具体实现,实例化各个实现类。根据第一步获取到的驱动列表来实例化具体实现类。我们重点关注 SPI 的用法,首先看第二步,使用 SPI 来获取驱动的实现类 , 对应的代码是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);这里没有去 META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。
接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:
Iterator<Driver> driversIterator = loadedDrivers.iterator();//遍历所有的驱动实现while(driversIterator.hasNext()) { driversIterator.next();}在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索path 下以及 jar 包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。
然后是调用driversIterator.next()方法,此时就会根据驱动名字具体实例化各个实现类了,现在驱动就被找到并实例化了。
3 Java SPI 机制源码解析我们根据第一节 JDK SPI 示例,学习 ServiceLoader 类的实现。
进入 ServiceLoader 类的load方法:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl);}public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) { return new ServiceLoader<>(service, loader);}上面的代码,load 方法会通过传递的服务类型和类加载器classLoader 创建一个 ServiceLoader 对象。
private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload();}// 缓存已经被实例化的服务提供者,按照实例化的顺序存储private LinkedHashMap<String,S> providers = new LinkedHashMap<>();public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader);}私有构造器会创建懒迭代器 LazyIterator 对象 ,所谓懒迭代器,就是对象初始化时,仅仅是初始化,只有在真正调用迭代方法时,才执行加载逻辑。
示例代码中创建完 serviceLoader 之后,接着调用iterator()方法:
Iterator<Robot> iterator = serviceLoader.iterator();// 迭代方法实现public Iterator<S> iterator() { return new Iterator<S>() { Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } };}迭代方法的实现本质是调用懒迭代器 lookupIterator 的 hasNext() 和 next() 方法。
1、hasNext() 方法
public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); }}public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); }}懒迭代器的hasNextService方法首先会通过加载器通过文件全名获取配置对象 Enumeration<URL> configs ,然后调用解析parse方法解析classpath下的META-INF/services/目录里以服务接口命名的文件。
private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true;}当 hasNextService 方法返回 true , 我们可以调用迭代器的 next 方法 ,本质是调用懒加载器 lookupIterator 的 next() 方法:
2、next() 方法
Robot robot = iterator.next();// 调用懒加载器 lookupIterator 的 `next()` 方法private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }通过反射方法 Class.forName() 加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型,然后返回实例对象。
4 Java SPI 机制的缺陷通过上面的解析,可以发现,我们使用 JDK SPI 机制的缺陷 :
不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。多个并发多线程使用 ServiceLoader 类的实例是不安全的。5 Spring SPI 机制Spring SPI 沿用了 Java SPI 的设计思想,Spring 采用的是 spring.factories 方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,提供 Spring 框架的扩展性。
1、创建 MyTestService 接口
public interface MyTestService { void printMylife();}2、创建 MyTestService 接口实现类
WorkTestService :public WorkTestService implements MyTestService { public WorkTestService(){ System.out.println("WorkTestService"); } public void printMylife() { System.out.println("我的工作"); }}FamilyTestService :public FamilyTestService implements MyTestService { public FamilyTestService(){ System.out.println("FamilyTestService"); } public void printMylife() { System.out.println("我的家庭"); }}3、在资源文件目录,创建一个固定的文件 META-INF/spring.factories。
#key是接口的全限定名,value是接口的实现类com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService4、运行代码
// 调用 SpringFactoriesLoader.loadFactories 方法加载 MyTestService 接口所有实现类的实例List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories( MyTestService.class, Thread.currentThread().getContextClassLoader());for (MyTestService testService : myTestServices) { testService.printMylife();}运行结果:
FamilyTestServiceWorkTestService我的家庭我的工作Spring SPI 机制非常类似 ,但还是有一些差异:
Java SPI 是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在 services 目录下。Spring SPI 是一个 spring.factories 配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅 spring.factories 一个配置文件。和 Java SPI 一样,Spring SPI 也无法获取某个固定的实现,只能按顺序获取所有实现。
6 Dubbo SPI 机制基于 Java SPI 的缺陷无法支持按需加载接口实现类,Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。
Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下:
optimusPrime = org.apache.spi.OptimusPrimebumblebee = org.apache.spi.Bumblebee与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。
另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
下面来演示 Dubbo SPI 的用法:
public DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); }}测试结果如下 :
另外,Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性 。
Dubbo SPI :
https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/source/dubbo-spi/
JDK/Dubbo/Spring 三种 SPI 机制,谁更好 ?
https://segmentfault.com/a/1190000039812642