dubbo中SPI接口的定義如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface SPI { /** * 缺省擴展點名。 */ String value() default ""; }
dubbo默認的情況下,會依次從下面幾個文件中讀取擴展點。1.META-INF/dubbo/internal/ //dubbo內部實現的各種擴展都放在了這個目錄了。2.META-INF/dubbo/。3.META-INF/services/。只有打了@SPI注解的接口類dubbo才會去查找擴展點實現。
我們以Protocol為例,Protocol接口上打了SPI注解,默認的擴展點名稱為dubbo
@SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }
dubbo中內置了各種協議,如DubboProtocol,HttpProtocol,HessianProtocol等等。Dubbo默認rpc模塊默認protocol實現DubboProtocol,key為dubbo
ExtensionLoader類
1.ExtensionLoder.getExtensionLoader(Class<T> type)方法
每個定義的SPI接口,都會創建一個ExtensionLoader實例,存儲在ConcurrentMap<Class<?>,ExtensionLoader<?>> EXTENSION_LOADERS這個map對象中
2.ExtensionLoader使用loadExtensionClasses方法讀取擴展點中的實現類
loadExtensionClasses先讀取SPI注解的value值,如果value有值,就把這個值作為默認擴展實現的key。然后再以此讀取META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/下對應的文件。
3.我們以Protocal為例, loadFile逐行讀取com.alibaba.dubbo.rpc.Protocol文件中的內容,每行內容以key/value形式存儲。先判斷實現類上是否打上了@Adaptive注解,如果打上了該注解,將此類作為Protocol協議的設配類緩存起來,讀取下一行。如果實現類上沒有打上@Adaptive注解,判斷實現類是否存在參數為該接口的構造器,有的話作為包裝類存儲在該ExtensionLoader的Set<Class<?>> cachedWrapperClasses;集合中,這里用到了裝飾器模式。如果該類既不是設配類,也不是wrapper對象,那就是擴展點的具體實現對象,查找實現類上是否打了@Activate注解,有緩存到變量cachedActivates的map中將實現類緩存到cachedClasses中,以便於使用時獲取。如ProtocolFilterWrapper的實現如下:
public class ProtocolFilterWrapper implements Protocol { private final Protocol protocol; public ProtocolFilterWrapper(Protocol protocol) { if (protocol == null) { throw new IllegalArgumentException("protocol == null"); } this.protocol = protocol; } .......... }
4.獲取或則創建設配對象getAdaptiveExtension
如果cachedAdaptiveClass有值,說明有且僅有一個實現類打了@Adaptive, 實例化這個對象返回。如果cachedAdaptiveClass為空, 創建設配類字節碼。
為什么要創建設配類,一個接口多種實現,SPI機制也是如此,這是策略模式,但是我們在代碼執行過程中選擇哪種具體的策略呢。Dubbo采用統一數據模式com.alibaba.dubbo.common.URL(它是dubbo定義的數據模型不是jdk的類),它會穿插於系統的整個執行過程,URL中定義的協議類型字段protocol,會根據具體業務設置不同的協議。url.getProtocol()值可以是dubbo也是可以webservice, 可以是zookeeper也可以是redis。
設配類的作用是根據url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)選取具體的擴展點實現。
有上述的分析可知,能夠使用javasist生成設配類的條件:
1)接口方法中必須至少有一個方法打上了@Adaptive注解
2)打上了@Adaptive注解的方法參數必須有URL類型參數或者有參數中存在getURL()方法
createAdaptiveExtensionClass方法源碼如下:
private String createAdaptiveExtensionClassCode() { StringBuilder codeBuidler = new StringBuilder(); Method[] methods = type.getMethods(); boolean hasAdaptiveAnnotation = false; for (Method m : methods) { if (m.isAnnotationPresent(Adaptive.class)) { hasAdaptiveAnnotation = true; break; } } // 完全沒有Adaptive方法,則不需要生成Adaptive類 if (!hasAdaptiveAnnotation) throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); codeBuidler.append("package " + type.getPackage().getName() + ";"); codeBuidler.append("\nimport " + ExtensionLoader.class.getName() + ";"); codeBuidler.append("\npublic class " + type.getSimpleName() + "$Adaptive" + " implements " + type.getCanonicalName() + " {"); for (Method method : methods) { Class<?> rt = method.getReturnType(); Class<?>[] pts = method.getParameterTypes(); Class<?>[] ets = method.getExceptionTypes(); Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); StringBuilder code = new StringBuilder(512); if (adaptiveAnnotation == null) { code.append("throw new UnsupportedOperationException(\"method ") .append(method.toString()).append(" of interface ") .append(type.getName()).append(" is not adaptive method!\");"); } else { int urlTypeIndex = -1; for (int i = 0; i < pts.length; ++i) { if (pts[i].equals(URL.class)) { urlTypeIndex = i; break; } } // 有類型為URL的參數 if (urlTypeIndex != -1) { // Null Point check String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");", urlTypeIndex); code.append(s); s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); code.append(s); } // 參數沒有URL類型 else { String attribMethod = null; // 找到參數的URL屬性 LBL_PTS: for (int i = 0; i < pts.length; ++i) { Method[] ms = pts[i].getMethods(); for (Method m : ms) { String name = m.getName(); if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0 && m.getReturnType() == URL.class) { urlTypeIndex = i; attribMethod = name; break LBL_PTS; } } } if (attribMethod == null) { throw new IllegalStateException("fail to create adative class for interface " + type.getName() + ": not found url parameter or url attribute in parameters of method " + method.getName()); } // Null point check String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");", urlTypeIndex, pts[urlTypeIndex].getName()); code.append(s); s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");", urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod); code.append(s); s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod); code.append(s); } String[] value = adaptiveAnnotation.value(); // 沒有設置Key,則使用“擴展點接口名的點分隔 作為Key if (value.length == 0) { char[] charArray = type.getSimpleName().toCharArray(); StringBuilder sb = new StringBuilder(128); for (int i = 0; i < charArray.length; i++) { if (Character.isUpperCase(charArray[i])) { if (i != 0) { sb.append("."); } sb.append(Character.toLowerCase(charArray[i])); } else { sb.append(charArray[i]); } } value = new String[]{sb.toString()}; } boolean hasInvocation = false; for (int i = 0; i < pts.length; ++i) { if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { // Null Point check String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i); code.append(s); s = String.format("\nString methodName = arg%d.getMethodName();", i); code.append(s); hasInvocation = true; break; } } String defaultExtName = cachedDefaultName; String getNameCode = null; for (int i = value.length - 1; i >= 0; --i) { if (i == value.length - 1) { if (null != defaultExtName) { if (!"protocol".equals(value[i])) if (hasInvocation) getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName); else getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName); } else { if (!"protocol".equals(value[i])) if (hasInvocation) getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else getNameCode = String.format("url.getParameter(\"%s\")", value[i]); else getNameCode = "url.getProtocol()"; } } else { if (!"protocol".equals(value[i])) if (hasInvocation) getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName); else getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode); else getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode); } } code.append("\nString extName = ").append(getNameCode).append(";"); // check extName == null? String s = String.format("\nif(extName == null) " + "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");", type.getName(), Arrays.toString(value)); code.append(s); s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);", type.getName(), ExtensionLoader.class.getSimpleName(), type.getName()); code.append(s); // return statement if (!rt.equals(void.class)) { code.append("\nreturn "); } s = String.format("extension.%s(", method.getName()); code.append(s); for (int i = 0; i < pts.length; i++) { if (i != 0) code.append(", "); code.append("arg").append(i); } code.append(");"); } codeBuidler.append("\npublic " + rt.getCanonicalName() + " " + method.getName() + "("); for (int i = 0; i < pts.length; i++) { if (i > 0) { codeBuidler.append(", "); } codeBuidler.append(pts[i].getCanonicalName()); codeBuidler.append(" "); codeBuidler.append("arg" + i); } codeBuidler.append(")"); if (ets.length > 0) { codeBuidler.append(" throws "); for (int i = 0; i < ets.length; i++) { if (i > 0) { codeBuidler.append(", "); } codeBuidler.append(ets[i].getCanonicalName()); } } codeBuidler.append(" {"); codeBuidler.append(code.toString()); codeBuidler.append("\n}"); } codeBuidler.append("\n}"); if (logger.isDebugEnabled()) { logger.debug(codeBuidler.toString()); } return codeBuidler.toString(); }
5. 通過createAdaptiveExtensionClassCode()生成的java源代碼,要被java虛擬機加載執行必須得編譯成字節碼,dubbo提供兩種方式去執行代碼的編譯1)利用JDK工具類編譯2)利用javassit根據源代碼生成字節碼。
private Class<?> createAdaptiveExtensionClass() { String code = createAdaptiveExtensionClassCode(); ClassLoader classLoader = findClassLoader(); com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); }
在此順便介紹下@Adaptive注解打在實現類上跟打在接口方法上的區別:
如果有打在接口方法上,調ExtensionLoader.getAdaptiveExtension()獲取設配類,會先通過前面的過程生成java的源代碼,在通過編譯器編譯成class加載。但是Compiler的實現策略選擇也是通過ExtensionLoader.getAdaptiveExtension(),如果也通過編譯器編譯成class文件那豈不是要死循環下去了嗎?
ExtensionLoader.getAdaptiveExtension(),對於有實現類上去打了注解@Adaptive的dubbo spi擴展機制,它獲取設配類不在通過前面過程生成設配類java源代碼,而是在讀取擴展文件的時候遇到實現類打了注解@Adaptive就把這個類作為設配類緩存在ExtensionLoader中,調用是直接返回
6. 自動Wrap上擴展點的Wrap類
Dubbo是如何自動的給擴展點wrap上裝飾對象的呢?
在ExtensionLoader.loadFile加載擴展點配置文件的時候對擴展點類有接口類型為參數的構造器就是包轉對象,緩存到集合中去。
在調ExtensionLoader的createExtension(name)根據擴展點key創建擴展的時候, 先實例化擴展點的實現, 在判斷時候有此擴展時候有包裝類緩存,有的話利用包轉器增強這個擴展點實現的功能。具體實現如下:
private T createExtension(String name) { Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && wrapperClasses.size() > 0) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type + ") could not be instantiated: " + t.getMessage(), t); } }
7.ioc是spring的三大基礎功能之一, dubbo的ExtensionLoader在加載擴展實現的時候內部實現了個簡單的ioc機制來實現對擴展實現所依賴的參數的注入, dubbo對擴展實現中公有的set方法且入參個數為一個的方法,嘗試從對象工廠ObjectFactory獲取值注入到擴展點實現中去。
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; }
下面我們來看看ObjectFactory是如何根據類型和名字來獲取對象的,ObjectFactory也是基於dubbo的spi擴展機制的。它跟Compiler接口一樣設配類注解@Adaptive是打在類AdaptiveExtensionFactory上的不是通過javassist編譯生成的。
AdaptiveExtensionFactory持有所有ExtensionFactory對象的集合,dubbo內部默認實現的對象工廠是SpiExtensionFactory和SpringExtensionFactory,他們經過TreeMap排好序的查找順序是優先先從SpiExtensionFactory獲取,如果返回空在從SpringExtensionFactory獲取。
SpiExtensionFactory工廠獲取要被注入的對象,就是要獲取dubbo spi擴展的實現,所以傳入的參數類型必須是接口類型並且接口上打上了@SPI注解,返回的是一個設配類對象
public <T> T getExtension(Class<T> type, String name) { if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type); if (loader.getSupportedExtensions().size() > 0) { return loader.getAdaptiveExtension(); } } return null; }
SpringExtensionFactory,Dubbo利用spring的擴展機制跟spring做了很好的融合。在發布或者去引用一個服務的時候,會把spring的容器添加到SpringExtensionFactory工廠集合中去, 當SpiExtensionFactory沒有獲取到對象的時候會遍歷SpringExtensionFactory中的spring容器來獲取要注入的對象
public <T> T getExtension(Class<T> type, String name) { for (ApplicationContext context : contexts) { if (context.containsBean(name)) { Object bean = context.getBean(name); if (type.isInstance(bean)) { return (T) bean; } } } return null; }
ExtensionLoader整體活動圖如下: