Java–深入理解SPI机制
总结于《深入理解 Java 中 SPI 机制》、《深入理解SPI机制》
简介
SPI(Service Provider Interface),是 JDK 内置的一种 服务发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL 和PostgreSQL 都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中 SPI 机制主要思想是将装配的控制权移到程序之外(有点 IOC 内味了),在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
SPI与API区别:
- API是调用并用于实现目标的类、接口、方法等的描述;
- SPI是扩展和实现以实现目标的类、接口、方法等的描述;
换句话说,API 为操作提供特定的类、方法,SPI 通过操作来符合特定的类、方法。
例子
首先我们通过一个小例子,能更好的理解 SPI 机制:
1 2 3
| public interface SPIService { void excute(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class SpiImpl1 implements SPIService{ @Override public void excute() { System.out.println("SpiImpl1.excute()"); } }
public class SpiImpl2 implements SPIService{ @Override public void excute() { System.out.println("SpiImpl2.excute()"); } }
|
- 最后要在 classpath 路径下配置添加一个文件,文件名是 SPIService 接口的全限定名,内容是实现了这个接口的类的全限定名,多个实现类之间用回车隔开。
文件里的内容:
测试
然后我们用 SPI 提供的类 ServiceLoader.load
或者Service.providers
方法来拿到他们的实现类的实例。其中,Service.providers
包位于sun.misc.Service
,而ServiceLoader.load
包位于java.util.ServiceLoader
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test {
public static void main(String[] args) { Iterator<SPIService> providers = Service.providers(SPIService.class); ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class); while (providers.hasNext()){ SPIService spi = providers.next(); spi.excute(); } System.out.println("----------------------------------"); Iterator<SPIService> iterator = loader.iterator(); while (iterator.hasNext()){ SPIService spi = iterator.next(); spi.excute(); } } }
|
1 2 3 4 5
| SpiImpl1.excute() SpiImpl2.excute() ---------------------------------- SpiImpl1.excute() SpiImpl2.excute()
|
源码分析
- 我们可以看到,这里规定了默认的配置文件路径
META-INF/services
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public final class ServiceLoader<S> implements Iterable<S> { private static final String PREFIX = "META-INF/services/"; private final Class<S> service; private final ClassLoader loader; private final AccessControlContext acc; private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); private LazyIterator lookupIterator; }
|
- 核心功能的实现即查找实现类和创建类实例的过程都是由
LazyIterator
实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; public boolean hasNext() { 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; } public S next() { if (!hasNext()) { 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, x); } throw new Error(); } }
|
首先,**ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。
其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。
最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
不足
不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
多个并发多线程使用 ServiceLoader 类的实例是不安全的。
以上不足可以参考 dubbo 实现的 SPI 机制,但是我还没学
总结
通过 SPI 服务发现机制,我们就可以实现很多复杂的场景,比如 JDBC 的场景,由 Java 提供数据库连接规定的接口然后由第三方去实现,不同的厂商有不同的实现规范。在 JDBC 的 jar 包中就有 META-IFN\services
下的一个 java.sql.Driver 文件,里面的内容就是 com.mysql.cj.jdbc.Driver ,这个类就是关于数据库连接的具体实现。