spi的定义
什么是spi,全称为 Service Provider Interface,是java的一种服务发现机制。
当服务的提供者提供了相应的实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。文件内就是实现类的全路径,这样就ServiceLoader加载时就可以通过反射加载具体的实现,完成模块的注入。
下面举一个列子:
我们先定义一个接口:
1 | public interface Active { |
然后定义它的两个实现类
1 | public class ActiveImpl implements Active { |
1 | public class ActiveImpl2 implements Active { |
在META-INF/services/下建立Active的全限定名文件com.example.alipay.active.Active
在文件中输入实现类的全限定名
1 | com.example.alipay.active.impl.ActiveImpl |

然后在测试类中运行
1 | public static void main(String[] args) { |

这样就轻松的实现了一个java中的spi,根据上诉信息,总结出spi的几个要点:
- 定义接口和接口实现类
- 接口实现类所在的jar包放在主程序的classpath中
- 在META-INF/services/建立接口的全限定名,并且在里面输入实现类
- 使用ServiceLoader.load(Class<> service)加载接口类,输出相应的实现
spi的实现原理
ServiceLoader实现原理:
1 | public void reload() { |
在ServiceLoader.load(Class<> service) 方法中,只是对一个初始值的设置
1 | public final class ServiceLoader<S> implements Iterable<S> |
ServiceLoader的迭代器
1 | Iterator<Map.Entry<String,S>> knownProviders |
在测试方法中进行第一行获取迭代值的时候,其实是没有值的,在java spi中实行的是懒加载策略,就是在你需要的时候才进行加载
1 | Iterator<Active> iterator = serviceLoader.iterator(); |

当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。
1 | public boolean hasNext() { |
在调用hasNext的时候,当acc为空的时候,调用了hasNextService()方法。
1 | private boolean hasNextService() { |
String fullName = PREFIX + service.getName();进行接口的全限定路径查找pending = parse(service, configs.nextElement());找到实现类的全路径1
2
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 }1
2
3
4
5
6

在进行next迭代的时候,其实最后也是进行hasNextService进行判定,然后取出nextName,此值为在hasNextService时存储的类的全限定名,hasNextService如上面代码所示c = Class.forName(cn, false, loader);1
2
然后在nextService中,通过反射加载实例providers.put(cn, p);1
2
然后把类全限定名和实例放进Map中,从而迭代器中也存储了实例static { loadInitialDrivers(); println("JDBC DriverManager initialized"); }1
2
3
4
5
6
7
8
9
全部实例迭代完之后,你会发现,迭代器中已经存储了实现类的实例

***
探究一下JDBC是怎么加载自己的实现类的
在rt.jar类库中找到DriverManager类,类中的静态代码块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!"); } } }1
2
3
4
5
6
7
8
9

在上面标红的地方加载Drive.class接口的实现类,那我们的实现类在哪里定义的呢,java并没有规定死我们的JDBC驱动实现
我们找到mysql驱动包

在mysql驱动中,在META-INF/services中找到了接口的实现,NonRegisteringDriver实现了Drive接口的所有方法,Driver实现类中把自己的实例注册到了DriverManager中
**DriverManager.registerDriver(new Driver());**public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }1
2
3
4
5
这样就成功的找到了Drive接口具体的实现类,后面就用DriverManager进行管理。
问题来了,JDBC的Driver接口在rt.jar包中定义的,rt.jar包中使用的是启动类加载器,那启动类加载器并不能直接加载我们用户自定义的类,正常情况下我们用户的定义的类是由应用类加载器加载的,那系统怎么做才能找到我们的实现类呢?不知道读者注意到
ClassLoader cl = Thread.currentThread().getContextClassLoader();
这个没有,在初始化的时候,获取到当前线程的类加载器实现加载过程,在后面加载类的时候,通过cl中的类加载器进行加载,在DriverManager中用的是启动类加载器,所以在这个地方也是启动类加载加载的Driver的实现类。
在深入理解java虚拟机中提到了这一点,破坏了双亲委派模型。启动类加载器本不应该认识这些代码,现在用一种取巧的方法获取到这些代码。进行了逆向加载。
spi的问题
java spi存在的问题:
- 第一就是有些时候我想要对单个实现类进行获取,比如key,value的形式,但是java spi并不提供这种操作。
- 每次对ServiceLoader进行操作的时候,都会进行重新加载,实现类通常是无状态的,是否可以存储到spring这样的容器中
- 线程不安全
在dubbo中有着自己的spi实现,在后面我们会进行详细的分析。