什么是spi,全称为 Service Provider Interface,是java的一种服务发现机制。
当服务的提供者提供了相应的实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。文件内就是实现类的全路径,这样就ServiceLoader加载时就可以通过反射加载具体的实现,完成模块的注入。
下面举一个列子:
我们先定义一个接口:
public interface Active {
String getName();
}
然后定义它的两个实现类
public class ActiveImpl implements Active {
@Override
public String getName() {
return "ActiveImpl";
}
}
public class ActiveImpl2 implements Active {
@Override
public String getName() {
return "ActiveImpl2";
}
}
在META-INF/services/下建立Active的全限定名文件com.example.alipay.active.Active
在文件中输入实现类的全限定名
com.example.alipay.active.impl.ActiveImpl
com.example.alipay.active.impl.ActiveImpl2
然后在测试类中运行
public static void main(String[] args) {
Class<Active> activeClass = Active.class;
ServiceLoader<Active> serviceLoader=ServiceLoader.load(Active.class);
Iterator<Active> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
Active next = iterator.next();
System.out.println(next.getName());
}
}

这样就轻松的实现了一个java中的spi,根据上诉信息,总结出spi的几个要点:
- 定义接口和接口实现类
- 接口实现类所在的jar包放在主程序的classpath中
- 在META-INF/services/建立接口的全限定名,并且在里面输入实现类
- 使用ServiceLoader.load(Class<> service)加载接口类,输出相应的实现
ServiceLoader实现原理:
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(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();
}
在ServiceLoader.load(Class<> service) 方法中,只是对一个初始值的设置
public final class ServiceLoader<S> implements Iterable<S>
//配置文件的路径
private static final String PREFIX = "META-INF/services/";
//加载的服务类或接口
private final Class<S> service;
//已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//类加载器
private final ClassLoader loader;
//内部类,真正加载服务类
private LazyIterator lookupIterator;
}
ServiceLoader的迭代器
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
在测试方法中进行第一行获取迭代值的时候,其实是没有值的,在java spi中实行的是懒加载策略,就是在你需要的时候才进行加载
Iterator<Active> iterator = serviceLoader.iterator();
当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
在调用hasNext的时候,当acc为空的时候,调用了hasNextService()方法。
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;
}
-
String fullName = PREFIX + service.getName();进行接口的全限定路径查找

pending = parse(service, configs.nextElement());找到实现类的全路径
在进行next迭代的时候,其实最后也是进行hasNextService进行判定,然后取出nextName,此值为在hasNextService时存储的类的全限定名,hasNextService如上面代码所示
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
}
然后在nextService中,通过反射加载实例
c = Class.forName(cn, false, loader);
然后把类全限定名和实例放进Map中,从而迭代器中也存储了实例
providers.put(cn, p);
全部实例迭代完之后,你会发现,迭代器中已经存储了实现类的实例
探究一下JDBC是怎么加载自己的实现类的
在rt.jar类库中找到DriverManager类,类中的静态代码块
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
我们找到mysql驱动包
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这样就成功的找到了Drive接口具体的实现类,后面就用DriverManager进行管理。
问题来了,JDBC的Driver接口在rt.jar包中定义的,rt.jar包中使用的是启动类加载器,那启动类加载器并不能直接加载我们用户自定义的类,那怎么做才能找到我们的实现类呢?
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
不知道读者注意到
ClassLoader cl = Thread.currentThread().getContextClassLoader();
这个没有,在初始化的时候,获取到当前线程的类加载器实现加载过程,在后面加载类的时候,通过cl中的类加载器进行加载,在DriverManager中用的是启动类加载器,所以在这个地方也是启动类加载加载的Driver的实现类。 在深入理解java虚拟机中提到了这一点,破坏了双亲委派模型。启动类加载器本不应该认识这些代码,现在用一种取巧的方法获取到这些代码。进行了逆向加载。
java spi存在的问题:
- 第一就是有些时候我想要对单个实现类进行获取,比如key,value的形式,但是java spi并不提供这种操作。
- 每次对ServiceLoader进行操作的时候,都会进行重新加载,实现类通常是无状态的,是否可以存储到spring这样的容器中
- 线程不安全
在dubbo中有着自己的spi实现,在后面我们会进行详细的分析。