微信公众号:吉姆餐厅ak
学习更多源码知识,欢迎关注。
最近学习dubbo源码的时候,接触了spi机制。如果不了解这的话,dubbo源码或许看起来你会迷惑。
那么什么是SPI机制呢?实际项目中又是怎么使用的呢?
当我们开发项目中如果需要第三方的服务支持,可以直接写死到代码里面,指定具体实现。但这种方式耦合太强,不利于切换到其它服务,好的方法是写一个配置文件指定服务的实现方,这个时候java的spi机制就能发挥作用了。比如我们经常用的数据库驱动链接方式:java.sql.Driver
,该接口即通过SPI方式实现。
SPI的全名为Service Provider Interface,中文名称:服务提供接口。普通开发人员可能不熟悉,因为这个一般在源码中用的比较多,主要为了解耦,针对厂商或者插件。
简单来说接口和实现类的关系通过配置文件维护,实现方式就是通过serviceLoader加载各个jar包中指定接口的实现类。所以有些实现类即使实现了接口,配置文件不指定的话,通过spi机制,也是找不到加载不到的。
先来看一个示例,然后再分析源码。
定义一个接口:
public interface Spi {
void test();
}
定义一个实现:
public class SpiServiceImpl implements Spi{
@Override
public void test() {
System.out.println("spi execute");
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
ServiceLoader services = ServiceLoader.load(Spi.class);
Iterator iterator = services.iterator();
while (iterator.hasNext()){
iterator.next().test();
}
}
}
最后在META-INF中定义services文件,这里一定要写接口全路径。
注意:接口和services文件一定是定义在两个不同jar包中。
运行中SpiServiceImpl
的main方法,运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
spi execute
首先来看 ServiceLoader 初始化方法:
public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
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);
}
这里将获取到应用类加载器,和接口类型一起作为构造参数。另外会在 reload() 中创建一个用于懒加载的迭代器LazyIterator
。
继续来看一下Iterator
获取的迭代器的实现:
public Iterator iterator() {
return new Iterator() {
//创建一个缓存迭代器
Iterator> knownProviders
= providers.entrySet().iterator();
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();
}
};
}
整体的逻辑比较简单,首先用缓存迭代器进行迭代,如果没有,用懒加载迭代器遍历。
另外看一下资源定位和加载的方法:
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//这里加载 META-INF/services/ 下的资源文件
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
}
源码就分析到这里。
其实可以看出,SPI机制和策略模式有点相似,都是通过接口获取不同的实现类。但是策略模式针对的是业务中的接口和实现解耦,而SPI机制是来解决项目与项目解耦 。但是SPI机制也有缺点,就是不能根据类型区分实现类,只能获取全部的实现类进行遍历。如果项目中一些不必要的实现类,也会一并加载,不够灵活。所以Dubbo SPI
机制是专门针对Jdk SPI
机制的扩展,后续详细讲解。