深入dubbo源码前最好先了解下java spi(service provider interface)机制, 简单来说, spi可以帮我们加载指定文件中描述的接口实现类. 嗯…就这? 是不是太简单了, 虽然我是个菜瓜, 那我也知道Class.forName呀~ 那我们来研究下~
尽管千篇一律, 还是给出一个可运行demo
// 首先你需要一个对外接口
public interface GreetOrBye {
String say(String name);
}
// 以及两个实现类
public class Bye implements GreetOrBye {
@Override
public String say(String name) {
return "bye " + name;
}
}
public class Greet implements GreetOrBye {
@Override
public String say(String name) {
return "hi " + name;
}
}
//然后是执行类
public class Launcher {
public static void say(String name) {
ServiceLoader greetOrByeServiceLoader = ServiceLoader.load(GreetOrBye.class);
Iterator iterator = greetOrByeServiceLoader.iterator();
while (iterator.hasNext()) {
GreetOrBye greetOrBye = iterator.next();
System.out.println(greetOrBye.say(name));
}
}
public static void main(String[] args) {
say("wahahah");
}
}
下面是需要指定的文件, 目录名称固定META-INF下的services, 文件名称为接口全限定名
文件内容是实现类的全限定名
运行结果
hi wahahah
bye wahahah
平平无奇~(古天乐?)
虽然还没有看ServiceLoader的代码, 但是以我40年代码经验来看, 里面必定就是一个读文件, 反射创建对象的过程. 那么好吧~ 接下来证明自己
菜瓜惯例, 先看注释, 再看属性.
注释中先定义了两个名词
service 指暴露的接口或者类, 通常是抽象类, 也可以是具体类, 但是不建议.
service provider 指实现类, 通常是一个代理类, 内容决定具体的实现类
然后提了几点要求
**service provider 必须提供无参构造方法 **
service provider 在META-INF/services中定义, 文件名称是service的全限定名称, 比如com.togo.spi.helloworld.GreetOrBye, 文件内容是每一行都是service provider的全限定名称, 比如com.togo.spi.helloworld.impl.Greet
如果一个service provider出现在多个文件或者在一个文件中出现多次, service loader会去重.
service provider和配置文件不一定要在一个jar中, 但是service provider必须可以被加载配置文件的loader访问到(这个我们到时候关注下)
service provider都是懒加载(按需加载)
ServiceLoader是线程不安全的
ServiceLoader实现了Iterable接口, 属性如下.
// 文件路径
private static final String PREFIX = "META-INF/services/";
// 暴露的接口类型
private final Class service;
// 类加载器
private final ClassLoader loader;
// 安全相关
private final AccessControlContext acc;
// 缓存加载的类
private LinkedHashMap providers = new LinkedHashMap<>();
// 懒加载迭代器
private LazyIterator lookupIterator;
看名字和注释大家应该能猜出各个属性的作用, 我们通过源码来进一步了解下.
public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static ServiceLoader load(Class service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class 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 service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
load方法其实就是创建一个新的ServiceLoader对象(因为构造方法私有~), 使用的就是当前线程的类加载器, 整个构造过程就是一些赋值操作. 在reload方法中会清除map中缓存的对象, 并重新创建一个LazyIterator, LazyIterator构造方法中就只是赋值了. 既然说是懒加载了, 那么重要的操作当然都在使用的时候了.
public Iterator iterator() {
return new Iterator() {
// 已经加载的对象
Iterator> knownProviders
= providers.entrySet().iterator();
// 先从已经加载的对象中找, 没有再从lookupIterator中找
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();
}
};
}
public boolean hasNext() {
if (acc == null) { // 默认走这里, AccessControlContext作者没关注~~
return hasNextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
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;
}
// 解析文件, 并将文件中的字符串存储到list中, 返回该list的iterator
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
next()方法也比较简单, 就是获取到类全限定名称后做了反射创建对象的操作, 跟我们开始的预测是一样.
至此java spi分析到这, 代码很简单, 重点还是学习思想-面向接口编程, 比如我们经常使用不同的数据库驱动代码, 在DriverManager中就有ServiceLoader的身影. 下一篇研究下dubbo增强版spi~