Fork me on GitHub

Dubbo扩展机制实现(一)之java SPI

前言

在之前的文章Dubbo暴露服务过程中提出了问题:@SPI这些东西究竟是什么?
Dubbo开发手册之扩展点加载中有这么解释过:

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

所以在分析Dubbo扩展机制前,先看看jdk的SPI。

什么是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 ,这些之后会单独写一写。鉴于鄙人才疏学浅,以上文章如有不对的地方希望大家予以指出,共同进步。

-------------本文结束,感谢您的阅读-------------
贵在坚持,如果您觉得本文还不错,不妨打赏一下~
0%