深入理解Java中的spi机制 SPI
全名为Service Provider Interface
是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
JAVA SPI
= 基于接口的编程+策略模式+配置文件 的动态加载机制
Java SPI的具体约定如下:
当服务的提供者,提供了服务接口的一种实现之后,在jar
包的META-INF/services/
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。
而当外部程序装配这个模块的时候,就能通过该jar
包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
根据SPI的规范我们的服务实现类必须有一个无参构造方法
。
为什么一定要在classes
中的META-INF/services
下呢?
JDK提供服务实现查找的一个工具类:java.util.ServiceLoader
在这个类里面已经写死
1 2 private static final String PREFIX = "META-INF/services/" ;
常见的使用场景:
JDBC
加载不同类型的数据库驱动
日志门面接口实现类加载,SLF4J
加载不同提供商的日志实现类
Spring
中大量使用了SPI
,
对servlet3.0
规范
对ServletContainerInitializer
的实现
自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)
等
Dubbo
里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
简单的spi实例 整体包结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 └─main ├─java │ └─com │ └─xinchen │ └─spi │ └─App.java │ └─IService.java │ └─ServiceImplA.java │ └─ServiceImplB.java └─resources └─META-INF └─services └─com.xinchen.spi.IService
SPI接口
1 2 3 public interface IService { void say (String word) ; }
具体实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ServiceImplA implements IService { @Override public void say (String word) { System.out.println(this .getClass().toString() + " say: " + word); } } public class ServiceImplB implements IService { @Override public void say (String word) { System.out.println(this .getClass().toString() + " say: " + word); } }
/resource/META-INF/services/com.xinchen.spi.IService
1 2 com.xinchen.spi.ServiceImplA com.xinchen.spi.ServiceImplB
Client类
1 2 3 4 5 6 7 8 9 10 11 12 13 public class App { static ServiceLoader<IService> services = ServiceLoader.load(IService.class); public static void main (String[] args) { for (IService service:services){ service.say("Hello World!" ); } } }
源码解析 java.util.ServiceLoader
中的Fied区域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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;
从ServiceLoader.load(IService.class)
进入源码中
1 2 3 4 5 public static <S> ServiceLoader<S> load (Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
在ServiceLoader.load(service, cl)
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static <S> ServiceLoader<S> load (Class<S> service,ClassLoader loader) { return new ServiceLoader<>(service, loader); } 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(); } public void reload () { providers.clear(); lookupIterator = new LazyIterator(service, loader); } private LazyIterator (Class<S> service, ClassLoader loader) { this .service = service; this .loader = loader; }
当我们通过迭代器获取对象实例的时候,首先在成员变量providers
中查找是否有缓存的实例对象
如果存在则直接返回,否则则调用lookupIterator
延迟加载迭代器进行加载
迭代器判断的代码如下
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 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(); } }; }
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 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 ; } 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(); }
总结 优点
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点
参考 我是SPI,我让框架更加优雅了!
The Java™ Tutorials
聊聊Dubbo(五):核心源码-SPI扩展
深入理解Java SPI机制