前言
在之前的文章Dubbo暴露服务过程中提出了问题:@SPI这些东西究竟是什么?
在Dubbo开发手册之扩展点加载中有这么解释过:
Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。
什么是SPI
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 在面向对象设计里,我们不会针对实现编程,模块间面向接口编程来防止强耦合。java spi机制实现了一种放在程序以外的方式去动态装配模块,这就是java的服务发现。类似于ioc的思想,将模块装配放在程序外,比如xml等方式。
Dubbo框架就是借鉴了这种机制,在jdk的基础上进行了改进。
java SPI机制约定
java的spi是通过ServiceLoader来加载,根据官方文档来看一下SPI机制的约定:
- Service实现类必须有一个无参构造器
- 在META-INF/services/目录中提供一个文件名称为Service接口全限定名的文件,文件内容为Service接口实现类全限定名,编码格式为UTF-8
- 使用java.util.ServiceLoader来动态加载Service接口的实现类。
SPI示例
代码地址传送门
目录结构如下:
接口定义:
public interface HelloWorld {
void sayHello();
}
两个实现:
public class HelloWorldENimpl implements HelloWorld {
@Override
public void sayHello() {
System.out.println("hello,world!");
}
}
public class HelloWorldCNimpl implements HelloWorld {
@Override
public void sayHello() {
System.out.println("你好,世界!");
}
}
配置服务发现,在META-INF/service目录下创建文件:com.crw.demo.spi.HelloWorld,内容为接口实现类全名:
com.crw.demo.spi.impl.HelloWorldENimpl
com.crw.demo.spi.impl.HelloWorldCNimpl
编写调用端:
public class Run {
public static void main(String[] args) {
ServiceLoader<HelloWorld> loads = ServiceLoader.load(HelloWorld.class);
for (HelloWorld load : loads) {
load.sayHello();
}
}
}
运行结果如下:
ServiceLoader源码分析
从 ServiceLoader.load(Class\<S> service) 方法点进去看一下
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); // 获取类加载器
return ServiceLoader.load(service, cl);
}
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); // 创建一个懒加载的提供者发现器
}
可以看到,实际上是交给了一个私有静态内部类处理new LazyIterator(service, loader); 通过名字就像是懒加载,所以我们看看什么时候类加载器会加载SPI实现服务。
答案是遍历的时候。
ServiceLoader实现了iterator接口:
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(); // 实际上是调用LazyIterator.hasNext()方法。
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // 实际上是调用LazyIterator.next()方法。
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
在客户端遍历的时候,首先调用了hasNext()方法,hasNext调用了LazyIterator.hasNext(),其实际上又调用了内部方法 hasNextService() :
private boolean hasNextService() {
if (nextName != null) { // 如果有服务提供者名称,直接返回
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName(); // META-INF/services/xxx.xxx.xxx.XxxImpl
// 获取配置文件加载路径
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()); //解析配置路径,用utf-8格式读取配置
}
nextName = pending.next(); // 服务提供者名称赋值
return true;
}
看一眼解析完的结构,在遍历的时候会读取配置,把服务提供者名称一次性获取:
接着,在客户端遍历的时候调用了next()方法, LazyIterator.next()方法里做了如下事情
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
}
返回了一个服务提供者实例,就这样完成了一次SPI调用。
总结
这篇文章主要介绍了jdk SPI机制:
- java如何编写一个SPI服务的。
- ServiceLoader源码如何实现SPI服务。
本篇主要是为了Dubbo实现spi而做了铺垫。在看ServiceLoader的源码时,主要还是利用了java的类加载器 ClassLoader ,这些之后会单独写一写。鉴于鄙人才疏学浅,以上文章如有不对的地方希望大家予以指出,共同进步。