Java和dubbo中的SPI機制學習
阿新 • • 發佈:2018-12-25
為了實現在模組裝配時的時候不在程式中動態指明,需要提供一種服務發現機制,為某個介面尋找服務實現的機制,就是將裝配的控制權轉移到程式之外,在模組化設計中這個機制尤其重要。
Java SPI(Service Provider Interface)的具體約定如下:當服務的提供者提供了服務介面的一種實現之後,在jar包的META-INF/services/ 目錄中同時建立一個以服務介面命名的檔案,該檔案中的內容就是實現該服務介面的具體實現類。
Java中提供了一個用於服務實現查詢的工具類:java.util.ServiceLoader。
該例項用於演示在maven環境下的service宣告定義,居然在idea下無法除錯啟動... resources下指定目錄 META-INF/services目錄(被寫死在程式碼中):
該例項用於演示在maven環境下的service宣告定義,居然在idea下無法除錯啟動... resources下指定目錄 META-INF/services目錄(被寫死在程式碼中):
public final class ServiceLoader<S
implements Iterable<S
{
private static final String PREFIX = "META-INF/services/";
注意需要將服務宣告的檔名稱定義為: example.spi.service.Service,與介面名稱一致,其中的內容包括:
不同的實現類需要以換行符分隔,在SpiMain的主方法中,使用ServiceLoader進行載入操作:example.spi.service.PrintServiceImpl example.spi.service.EchoServiceImpl
public static void main(String[] args) {
ServiceLoader<Service> serviceLoader = ServiceLoader.load(Service.class);
for (Service service : serviceLoader) {
service.printInfo();
}
}
在ServiceLoader的load方法中,會初始化ServiceLoader.LazyIterator,實現了標準的迭代器介面Iterator(以及hasNext, next方法),其中hasNextService()方法中會從當前的ClassLoader載入 PREFIX("META-INF/services/") + service名稱(class名稱),
在parse方法中會根據檔案進行逐行解析,如果下一步存在對應的實現,該方法返回true,接著就可以訪問nextService方法來獲得下一個服務實現: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; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
將nextName(下一行實現的類名稱)進行例項化,注意需要實現對應介面並有無參建構函式,並將實現放到providers這個Map中,以便下次直接使用(除非進行reload操作,否則不會更新該表,而reload操作是在ServiceLoader啟動時初始化的)。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 }
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
雖然ServiceLoader也算是使用了延遲載入,但只能通過遍歷所有獲取,將介面的實現類全部載入並例項化一遍,而且只能通過Iterator形式獲取,不能根據某個引數來獲取。
在dubbo中大量使用了該方式進行服務發現和服務註冊,並進行了一定的擴充套件,實現了 com.alibaba.dubbo.common.extension.ExtensionLoader 類,內部註冊服務的目錄遷移為 META-INF/dubbo/internal型別:
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
在ExtensionLoader中,不僅會對其中的服務進行初始化,還可以像IOC一樣,對引用的其他服務進行set操作,這部分參考ExtensionLoader.injectExtension(T instance)類,進行注入的型別必須為set開頭方法,引數只有一個,方法為public,而且引數必須為介面(只有介面ExtensionFactory才能根據該介面進行查詢服務實現類)。
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
如果介面的實現由多個,則此時採取的策略是,並不去注入一個具體的實現者,而是注入一個動態生成的實現者,這個動態生成的實現者的邏輯是確定的,能夠根據不同的引數來使用不同的實現者實現相應的方法。這個動態生成的實現者的class就是ExtensionLoader的Class<?> cachedAdaptiveClass
在查詢SPI Annotation,使用dubbo配置的擴充套件方式進行註冊,例如在獲取AdaptiveExtensionFactory時,使用的建構函式用於載入擴充套件點:
public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
}
而getExtension()方法能夠查詢具體的objectFactory(SPI,Spring)可以從對應的檔案宣告以及spring容器中查詢具體的bean。
對於ExtensionFactory會從三個地方載入extensionClass:
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
String value = defaultAnnotation.value();
if(value != null && (value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if(names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if(names.length == 1) cachedDefaultName = names[0];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
分別為 /META-INF/dubbo/internal, /META-INF/dubbo/,META-INF/services。
可以看出dubbo的擴充套件機制雖然與SPI比較類似,但額外增加了其他功能,例如可以根據介面名稱來獲取服務,服務宣告檔案支援A=B的方式,此時A為名稱B為實現類;支援擴充套件IOC依賴注入功能,可以為Service之間的依賴關係注入相關的服務並保證單例。