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~

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