定义
SPI 全称(Service Provider Interface) 服务提供接口缩写,是一种服务发现机制。
它的本质是将接口的实现类的全限定名定义在配置文件中,并有服务器读取配置文件,并加载实现类。这样就可以在运行的时候,动态为接口替换实现类。
原理分析
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
举个栗子来说
先定义一个接口:
package com.rb.user.provider;
public interface UserProviderService {
String get();
}
定义两个实现类
package com.rb.user.provider.impl;
public class MobilePhoneUserProducer implements UserProviderService {
@Override
public String get() {
return " MobilePhone User Service ";
}
}
package com.rb.user.provider.impl;
public class EmailUserProducer implements UserProviderService {
@Override
public String get() {
return " Email User Service ";
}
}
在resources目录下创建文件夹META-INF.services,并在该文件夹下创建一个与UserProviderService接口package路径一样的名称文件 com.rb.user.provider.UserProviderService
文件写入两个实现类的全路径名
com.rb.user.provider.impl.MobilePhoneUserProducer
com.rb.user.provider.impl.EmailUserProducer
运行测试用例 UserTest:
package com.rb.service.user;
import java.util.ServiceLoader;
/**
* @author zzg
*/
public class UserTest {
public static void main(String[] args) {
ServiceLoader<UserProviderService> serviceLoader = ServiceLoader.load(UserProviderService.class);
for (UserProviderService userProviderService : serviceLoader) {
System.out.println("out : " + userProviderService.get());
}
// serviceLoader.forEach(UserProviderService::get);
}
}
// 输出结果:
out : Email User service
out : MobilePhone User service
Process finished with exit code 0
Java 底层源码:【只贴出了需要的代码】
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
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 class LazyIterator implements Iterator<S> {
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(); // This cannot happen
}
}
关键代码 提炼大致如下:
String fullName = PREFIX + service.getName();
configs = loader.getResources(fullName);
Class<?> c = Class.forName(cn, false, loader);
S p = service.cast(c.newInstance());
providers.put(cn, p);
其实通过读取配置文件中实现类的全路径类名,通过反射创建对象,并放入providers容器中。
总结:
通过调用ServiceLoader.load方法,创建一个新的 ServiceLoader,并实例化该类中的成员变量,利用迭代器接口获取对象实例,ServiceLoader先判断成员变量providers对象(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载。
优点
使用Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。
缺点
不能按需加载。虽然ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
获取某个实现类的方式不够灵活,只能通过Iterator 形式获取,不能根据某个参数来获取对应的实现类。
多个并发多线程使用ServiceLoader 类的实例是不安全的。
加载不到实现类时抛出并不是真正原因的异常,错误很难定位。