Dubbo(一)-SPI 機制之javaSPI基礎
一 、java 的 SPI 機制
SPI 是什麼
SPI 全稱 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴充套件的 API,它可以用來啟用框架擴充套件和替換元件。是“介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制
流程架構圖:
在 java 程式碼中,我們編寫介面實現類,往往是事先確定的,在啟動時候載入類具體的實現類,一旦我們需要變更選擇某一實現類,我們就需要修改程式碼。為了實現這一個可以動態的選擇實現的方式,就出現了 SPI 技術,簡單說:SPI 其實就是一種服務發現機制。其核心思想就是結偶
SPI 的應用場景
- 日誌模組之日誌門面,可以選擇不同的實現進行載入
- 資料庫驅動載入介面實現類的載入 JDBC 載入不同型別資料庫的驅動
- Dubbo 中的服務發現機制
SPI 的使用
SPI 的應用分 4 步:
- 建立介面類
- 編寫介面實現類
- 編輯配置檔案。
- 程式執行起來
全他媽廢話
- 第一步建立介面類,我們這邊先定義一個 SayWord 的介面,定義了一個 saySomething 的方法。可以說不同的話語。SayWord 介面程式碼
public interface SayWord {
String saySomething();
}
複製程式碼
- 第二步建立實現類,我這邊定義類兩個實現,一個是中文的,一個是英文的。 SayChineseWord 實現程式碼
public class SayChineseWord implements SayWord {
@Override
public String saySomething() {
return "你好啊";
}
}
複製程式碼
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
複製程式碼
- 全部 ok 後就可以寫程式跑起來了~~~ Main 程式碼
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);
}
}
}
複製程式碼
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());
}
}
複製程式碼