1. 程式人生 > >Java和dubbo中的SPI機制學習

Java和dubbo中的SPI機制學習

為了實現在模組裝配時的時候不在程式中動態指明,需要提供一種服務發現機制,為某個介面尋找服務實現的機制,就是將裝配的控制權轉移到程式之外,在模組化設計中這個機制尤其重要。 Java SPI(Service Provider Interface)的具體約定如下:當服務的提供者提供了服務介面的一種實現之後,在jar包的META-INF/services/ 目錄中同時建立一個以服務介面命名的檔案,該檔案中的內容就是實現該服務介面的具體實現類。 Java中提供了一個用於服務實現查詢的工具類:java.util.ServiceLoader。

  該例項用於演示在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,與介面名稱一致,其中的內容包括:
example.spi.service.PrintServiceImpl
example.spi.service.EchoServiceImpl
   不同的實現類需要以換行符分隔,在SpiMain的主方法中,使用ServiceLoader進行載入操作:
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名稱),
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;
}
  在parse方法中會根據檔案進行逐行解析,如果下一步存在對應的實現,該方法返回true,接著就可以訪問nextService方法來獲得下一個服務實現:
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
}
  將nextName(下一行實現的類名稱)進行例項化,注意需要實現對應介面並有無參建構函式,並將實現放到providers這個Map中,以便下次直接使用(除非進行reload操作,否則不會更新該表,而reload操作是在ServiceLoader啟動時初始化的)。
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之間的依賴關係注入相關的服務並保證單例。