dubbo学习(五) -- java spi

前言

深入dubbo源码前最好先了解下java spi(service provider interface)机制, 简单来说, spi可以帮我们加载指定文件中描述的接口实现类. 嗯…就这? 是不是太简单了, 虽然我是个菜瓜, 那我也知道Class.forName呀~ 那我们来研究下~

java spi

demo

尽管千篇一律, 还是给出一个可运行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<GreetOrBye> greetOrByeServiceLoader = ServiceLoader.load(GreetOrBye.class);

        Iterator<GreetOrBye> 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年代码经验来看, 里面必定就是一个读文件, 反射创建对象的过程. 那么好吧~ 接下来证明自己

ServiceLoader

菜瓜惯例, 先看注释, 再看属性.

注释总结

注释中先定义了两个名词

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<S> service;
// 类加载器
private final ClassLoader loader;
// 安全相关
private final AccessControlContext acc;
// 缓存加载的类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒加载迭代器
private LazyIterator lookupIterator;

看名字和注释大家应该能猜出各个属性的作用, 我们通过源码来进一步了解下.

源码

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

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);
}

private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

load方法其实就是创建一个新的ServiceLoader对象(因为构造方法私有~), 使用的就是当前线程的类加载器, 整个构造过程就是一些赋值操作. 在reload方法中会清除map中缓存的对象, 并重新创建一个LazyIterator, LazyIterator构造方法中就只是赋值了. 既然说是懒加载了, 那么重要的操作当然都在使用的时候了.

public Iterator<S> iterator() {
    return new Iterator<S>() {

        // 已经加载的对象
        Iterator<Map.Entry<String,S>> 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<Boolean> action = new PrivilegedAction<Boolean>() {
            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~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章