1. spi 是什么
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了开闭原则,Java SPI就是为某个接口寻找服务实现的机制,Java Spi的核心思想就是解耦。
整体机制图如下:
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
总结起来就是:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
2. 应用场景
-
数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
-
日志门面接口实现类加载
SLF4J加载不同提供应商的日志实现类
-
Spring
Servlet容器启动初始化
org.springframework.web.SpringServletContainerInitializer
-
Spring Boot
自动装配过程中,加载META-INF/spring.factories文件,解析properties文件
-
Dubbo
Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来
例如Protocol 协议接口
3. 使用步骤
以支付服务为例:
-
创建一个
PayService
添加一个pay
方法package com.imooc.spi; import java.math.BigDecimal; public interface PayService { void pay(BigDecimal price); }
-
创建
AlipayService
和WechatPayService
,实现PayService
⚠️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.util.ServiceLoader; import java.math.BigDecimal; public class PayTests { public static void main(String[] args) { ServiceLoader
payServices = ServiceLoader.load(PayService.class); for (PayService payService : payServices) { payService.pay(new BigDecimal(1)); } } } -
运行测试类,查看返回结果
4. 原理分析
首先,我们先打开ServiceLoader
这个类
public final class ServiceLoaderimplements Iterable{ // SPI文件路径的前缀 private static final String PREFIX = "META-INF/services/"; // 需要加载的服务的类或接口 private Classservice; // 用于定位、加载和实例化提供程序的类加载器 private ClassLoader loader; // 创建ServiceLoader时获取的访问控制上下文 private final AccessControlContext acc; // 按实例化顺序缓存Provider private LinkedHashMapproviders = new LinkedHashMap(); // 懒加载迭代器 private LazyIterator lookupIterator; ...... }
参考具体ServiceLoader具体源码,代码量不多,实现的流程如下:
-
应用程序调用ServiceLoader.load方法
// 1. 获取ClassLoad public static
ServiceLoaderload(Classservice) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } // 2. 调用构造方法 public staticServiceLoaderload(Classservice, ClassLoader loader){ return new ServiceLoader<>(service, loader); } // 3. 校验参数和ClassLoad private ServiceLoader(Classsvc, 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(); } //4. 清理缓存容器,实例懒加载迭代器 public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } -
我们简单看一下这个懒加载迭代器
// 实现完全懒惰的提供程序查找的私有内部类 private class LazyIterator implements Iterator
{ // 需要加载的服务的类或接口 Classservice; // 用于定位、加载和实例化提供程序的类加载器 ClassLoader loader; // 枚举类型的资源路径 Enumerationconfigs = null; // 迭代器 Iterator pending = null; // 配置文件中下一行className String nextName = null; private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } // 加载配置PREFIX + service.getName()的文件 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; } // 获取下一个Service实现 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 } // for循环遍历时 public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedActionaction = new PrivilegedAction () { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } // 禁止删除 public void remove() { throw new UnsupportedOperationException(); } } -
将给定URL的内容作为提供程序配置文件进行分析。
private Iterator
parse(Class> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; ArrayList names = new ArrayList<>(); try { in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; while ((lc = parseLine(service, u, r, lc, names)) >= 0); } catch (IOException x) { fail(service, "Error reading configuration file", x); } finally { try { if (r != null) r.close(); if (in != null) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file", y); } } return names.iterator(); } -
按行解析配置文件,并保存names列表中
private int parseLine(Class> service, URL u, BufferedReader r, int lc, List
names) throws IOException, ServiceConfigurationError { String ln = r.readLine(); if (ln == null) { return -1; } int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) fail(service, u, lc, "Illegal configuration-file syntax"); int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) fail(service, u, lc, "Illegal provider-class name: " + ln); } // 判断provider容器中是否包含 不包含则讲classname加入 names列表中 if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } return lc + 1; }
5. 总结
优点:使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点:线程不安全,虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。