Dubbo(一)-SPI 機制之javaSPI基礎

fightcrap發表於2019-01-31

Dubbo(一)-SPI 機制之javaSPI基礎

一 、java 的 SPI 機制

SPI 是什麼

SPI 全稱 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴充套件的 API,它可以用來啟用框架擴充套件和替換元件。是“介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制

流程架構圖:

spi框架流程圖

在 java 程式碼中,我們編寫介面實現類,往往是事先確定的,在啟動時候載入類具體的實現類,一旦我們需要變更選擇某一實現類,我們就需要修改程式碼。為了實現這一個可以動態的選擇實現的方式,就出現了 SPI 技術,簡單說:SPI 其實就是一種服務發現機制。其核心思想就是結偶

SPI 的應用場景

  • 日誌模組之日誌門面,可以選擇不同的實現進行載入
  • 資料庫驅動載入介面實現類的載入 JDBC 載入不同型別資料庫的驅動
  • Dubbo 中的服務發現機制

SPI 的使用

SPI 的應用分 4 步:

  1. 建立介面類
  2. 編寫介面實現類
  3. 編輯配置檔案。
  4. 程式執行起來

全他媽廢話

  • 第一步建立介面類,我們這邊先定義一個 SayWord 的介面,定義了一個 saySomething 的方法。可以說不同的話語。SayWord 介面程式碼
public interface SayWord {
    String saySomething();
}
複製程式碼
public class SayChineseWord implements SayWord {
    @Override
    public String saySomething() {
        return "你好啊";
    }
}
複製程式碼

SayEnglishWord 實現程式碼

public class SayEnglishWord implements SayWord {
    @Override
    public String saySomething() {
        return "Hello";
    }
}
複製程式碼
  • 第三步,編寫配置檔案, 配置檔案是有嚴格的要求的,第一 :檔案位置:META-INF/services 下面,而且目錄必須是在 classPath 下面,不然就找不到了。原因後續會解釋。 第二:檔名:必須和介面名稱一致(包括包路徑)。第三:檔案內容:實現類的 名稱(包括包路徑 ) 檔案路徑
com.pangxie.server.dubbo.spi.impl.SayChineseWord
com.pangxie.server.dubbo.spi.impl.SayEnglishWord
複製程式碼
ServiceLoader<SayWord> sayWords=ServiceLoader.load(SayWord.class);
        for(SayWord sayWord:sayWords){
            System.out.println(sayWord.saySomething());
        }
複製程式碼

原理解釋

其實從程式碼編寫中可以明白,核心類是ServiceLoader,這個是一個載入服務的一個類,那麼具體是怎麼實現的呢?來讓我們look一下 

成員組成: 大致分為5個成員遍量,分別為:service-介面class物件;loader-類載入器;acc-建立時候用來控制訪問許可權的上下文;providers-服務實現類列表;lookupIterator-懶載入的迭代器

// The class or interface representing the service being loaded
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;
複製程式碼

提供了唯一的一個靜態方法(使用都是它~,或者直接構造吧~):

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

複製程式碼

為嘛路徑要是META-INF/services下?在看原始碼就發現了路徑配置:

private static final String PREFIX = "META-INF/services/";
複製程式碼

load呼叫發生了啥?其實沒啥,就是構建了一個LazyIterator物件,然後就沒有然後了。所以構建的時候並沒有直接載入,只是儲存了基本資訊。

public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
複製程式碼

只有在呼叫迭代器的時候,判斷是否有有配置呼叫hasNextService方法會獲取例項資訊,但是這一步沒有載入。

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);
                }
            }
複製程式碼

在next方法中判斷有例項資訊後就利用Class.forName,並且例項化,後儲存在連結串列裡。

 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);
            }
複製程式碼

總結

  • 優點:SPI還是很簡單的,基於配置來改變實現類。避免了直接修改程式碼的情況,做到介面形式的解偶。就可以實現在不同情況下使用不同的框架來。
  • 缺點: 多個併發多執行緒使用ServiceLoader類的例項是不安全的。 需要使用迭代器才會載入,感覺怪怪的。

來寫一個ServiceLoader吧~

為了熟悉ServiceLoader的實現就隨便自己寫了一個,可以通過網路請求形式獲取配置,簡單的擴充套件下啦啦啦,再加個配置檔案變更監聽,就可以真的隨心所欲了!!! 程式碼連線

public class NewServiceLoader<S> {
    private static final String PREFIX = "META-INF/services/";

    private String prefix = "META-INF/services/";

    /**
     * 介面的class
     */
    private final Class<S> service;

    /**
     * 類載入器
     */
    private final ClassLoader loader;

    /**
     * 許可權上下文
     */
    private final AccessControlContext acc;

    /**
     * 提供者列表
     */
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    private HashSet<String> providersName = new HashSet<>();


    private NewServiceLoader(Class<S> svc, String prefix, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        this.prefix = prefix;
        reload();
    }

    public static <S> NewServiceLoader<S> load(Class<S> sClass, String urlFix) {
        return new NewServiceLoader<S>(sClass, urlFix, null);
    }

    public static <S> NewServiceLoader<S> load(Class<S> sClass) {
        return new NewServiceLoader<S>(sClass, PREFIX, null);
    }

    public static <S> NewServiceLoader<S> load(Class<S> sClass, ClassLoader classLoader) {
        return new NewServiceLoader<S>(sClass, PREFIX, classLoader);
    }


    public LinkedHashMap<String, S> getProviders() {
        //如果兩者長度不一致,說明沒有載入全例項,需要載入例項
        if (providers.size() != providersName.size()) {
            instanceClass(providersName, providers, service);
        }
        return providers;
    }

    public void setProviders(LinkedHashMap<String, S> providers) {
        this.providers = providers;
    }

    /**
     * 重新載入
     */
    private void reload() {
        //清除一下,然後解析url檔案
        providers.clear();
        providersName.clear();
        parse();
    }

    /**
     * 解析檔案內容
     */
    private void parse() {
        //載入遠端的或者當前的url
        BufferedReader bufferedReader = null;
        try {
            bufferedReader = new BufferedReader(new InputStreamReader(getUrlInfo()));
            String line=null;
            while ((line=bufferedReader.readLine())!=null) {
                providersName.add(line);
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 獲取路徑檔案的資源
     * @return
     * @throws IOException
     */
    private InputStream getUrlInfo() throws IOException {
        //如果不是http開頭的,那麼是類檔案路徑啦~
        if (!prefix.startsWith("http")) {
            return getClass().getClassLoader().getResource(prefix + service.getName()).openStream();

        }
        // TODO 區分本地機器檔案
        URL url = new URL(prefix + service.getName());
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5 * 1000);
        InputStream inStream = conn.getInputStream();
        return inStream;

    }


    /**
     * 例項化變數
     *
     * @param providersName
     * @param providers
     * @param sClass
     */
    private void instanceClass(HashSet<String> providersName, LinkedHashMap<String, S> providers, Class<S> sClass) {
        for (String className : providersName) {
            Class c = null;
            Object instance = null;
            try {
                c = Class.forName(className);
                instance = c.newInstance();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }

            //轉化類物件
            S s = sClass.cast(instance);
            providers.put(className, s);
        }
    }

}

複製程式碼

新的Main程式碼

public static void main(String[] args) {
        NewServiceLoader<SayWord> sayWords=NewServiceLoader.load(SayWord.class);
        LinkedHashMap<String,SayWord> linkedHashMap=sayWords.getProviders();
        for(SayWord sayWord:linkedHashMap.values()){
            System.out.println(sayWord.saySomething());
        }
    }
複製程式碼

相關文章