SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了开闭原则,Java SPI就是为某个接口寻找服务实现的机制,Java Spi的核心思想就是 解耦 。
整体机制图如下:
Java SPI 实际上是“ 基于接口的编程+策略模式+配置文件 ”组合实现的动态加载机制。
总结起来就是: 调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
日志门面接口实现类加载
SLF4J加载不同提供应商的日志实现类
Spring Spring Boot
自动装配过程中,加载META-INF/spring.factories文件,解析properties文件
Dubbo
Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来
例如Protocol 协议接口
以支付服务为例:
创建一个 PayService
添加一个 pay
方法
package com.imooc.spi; import java.math.BigDecimal; public interface PayService { void pay(BigDecimal price); } 复制代码
创建 AlipayService
和 WechatPayService
,实现 PayService
:warning:SPI的实现类必须携带一个不带参数的构造方法;
package com.imooc.spi; import java.math.BigDecimal; public class AlipayService implements PayService{ public void pay(BigDecimal price) { System.out.println("使用支付宝支付"); } } 复制代码
package com.imooc.spi; import java.math.BigDecimal; public class WechatPayService implements PayService{ public void pay(BigDecimal price) { System.out.println("使用微信支付"); } } 复制代码
resources目录下创建目录META-INF/services
在META-INF/services创建com.imooc.spi.PayService文件
先以AlipayService为例:在com.imooc.spi.PayService添加com.imooc.spi.AlipayService的文件内容
创建测试类
package com.imooc.spi; import com.sun.tools.javac.util.ServiceLoader; import java.math.BigDecimal; public class PayTests { public static void main(String[] args) { ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class); for (PayService payService : payServices) { payService.pay(new BigDecimal(1)); } } } 复制代码
运行测试类,查看返回结果
使用支付宝支付 复制代码
首先,我们先打开 ServiceLoader<S>
这个类
public final class ServiceLoader<S> implements Iterable<S> { // SPI文件路径的前缀 private static final String PREFIX = "META-INF/services/"; // 需要加载类的接口 private Class<S> service; // 类加载器 private ClassLoader loader; // 缓存providers,保存着service实现 private LinkedHashMap<String, S> providers = new LinkedHashMap(); // 懒加载的查找迭代器 private ServiceLoader<S>.LazyIterator lookupIterator; ...... } 复制代码
参考具体ServiceLoader具体源码,代码量不多,实现的流程如下:
应用程序调用ServiceLoader.load方法
// 1. 获取ClassLoad public static <S> ServiceLoader<S> load(Class<S> var0) { ClassLoader var1 = Thread.currentThread().getContextClassLoader(); return load(var0, var1); } // 2. 调用构造方法 public static <S> ServiceLoader<S> load(Class<S> var0, ClassLoader var1) { return new ServiceLoader(var0, var1); } // 3. 校验参数和ClassLoad private ServiceLoader(Class<S> var1, ClassLoader var2) { this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null"); this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2; this.reload(); } //4. 清理缓存容器,实例懒加载迭代器 public void reload() { this.providers.clear(); this.lookupIterator = new ServiceLoader.LazyIterator(this.service, this.loader, null); } 复制代码
我们简单看一下这个懒加载迭代器
private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs; Iterator<String> pending; String nextName; private LazyIterator(Class<S> var1, ClassLoader var2) { this.configs = null; this.pending = null; this.nextName = null; this.service = var2; this.loader = var3; } // 迭代执行并获取解析出来的com.imooc.spi.AlipayService public boolean hasNext() { if (this.nextName != null) { return true; } else { if (this.configs == null) { try { String var1 = "META-INF/services/" + this.service.getName(); if (this.loader == null) { this.configs = ClassLoader.getSystemResources(var1); } else { this.configs = this.loader.getResources(var1); } } catch (IOException var2) { ServiceLoader.fail(this.service, "Error locating configuration files", var2); } } while(this.pending == null || !this.pending.hasNext()) { if (!this.configs.hasMoreElements()) { return false; } this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement()); } this.nextName = (String)this.pending.next(); return true; } } public S next() { if (!this.hasNext()) { throw new NoSuchElementException(); } else { String var1 = this.nextName; this.nextName = null; Class var2 = null; try { // 通过反射方法Class.forName()加载类对象 var2 = Class.forName(var1, false, this.loader); } catch (ClassNotFoundException var5) { ServiceLoader.fail(this.service, "Provider " + var1 + " not found"); } if (!this.service.isAssignableFrom(var2)) { ServiceLoader.fail(this.service, "Provider " + var1 + " not a subtype"); } try { // 调用instance()方法将类实例化 Object var3 = this.service.cast(var2.newInstance()); // 保存容器 ServiceLoader.this.providers.put(var1, var3); // 并返回实例 return var3; } catch (Throwable var4) { ServiceLoader.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4); throw new Error(); } } } // 禁止删除 public void remove() { throw new UnsupportedOperationException(); } } 复制代码
优点:使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点:线程不安全,虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。