SPI簡介
SPI是Service Provider Interface的縮寫,即服務提供接口(翻譯出來好繞口,還是不翻譯的好),實質上是接口,作用是對外提供服務。
SPI是Java的一種插件機制,可以不用修改源代碼實現新功能的擴展。
主要有如下幾個步驟:
- 實現SPI接口
- 在項目的META-INF/services文件夾下,新建一個以SPI接口命名的文件, 文件里面配置上SPI接口的實現類
- 使用java.util.ServiceLoader加載。
由于本篇文章主要講解Dubbo是如何使用SPI的,如果想要具體了解Java的SPI,可以參考下面兩篇文章:
- JavaSPI機制學習筆記
-
Introduction to the Service Provider Interfaces
當然還可以看 java.util.ServiceLoader 源碼,注釋中也有詳細的說明。
Dubbo SPI
回到正題,SPI在dubbo應用的地方很多,專業一點講叫做微內核機制;
如下圖:
我們拿其中一個標簽進行講解,我們在使用dubbo框架時,會配置<dubbo:protocol />標簽,告訴dubbo服務的主機、端口、可接收的最大連接數、使用哪個協議,協議的傳輸控制器(netty,servlet,jetty等)、線程池類型大小等信息。dubbo協議默認使用的是netty網絡傳輸框架,當然還可以使用mina、grizzly,只需要配置transporter、server、client為相應的值即可。那dubbo是如何根據不同的配置使用不同的網絡傳輸框架的呢,當然是通過SPI啦。java spi有一個配置文件,那dubbo是否也有呢?在dubbo-rpc包下的dubbo-rpc-dubbo子包下,發現了一個配置文件
我們來看下配置文件的內容:
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
配置了一個鍵值對,key為dubbo,值為org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol,在其它幾個子包下,也有名稱叫做org.apache.dubbo.rpc.Protocol的配置文件,說明Protocol插口有幾個對應的插件
可以猜測一下,當<dubbo:protocol />僅僅配置了name="dubbo",port="20880"時,會加載哪一個協議插件呢,根據名稱,可以猜測,加載的DubboProtocol插件。那dubbo是怎樣做到的呢,我們來一探究竟。
Dubbo為使用SPI做的準備工作:
三個注解
- SPI:這個注解使用在接口上,標識接口是否是extension(擴展或插口),可以接收一個默認的extension名稱
- Adaptive: 這個注解可以使用在類或方法上,決定加載哪一個extension,值為字符串數組,數組中的字符串是key值,比如new String[]{"key1","key2"};先在URL中尋找key1的值,如果找到,則使用此值加載extension,如果key1沒有,則尋找key2的值,如果key2也沒有,則使用接口SPI注解的值,如果接口SPI注解,沒有配置默認值,則將接口名按照首字母大寫分成多個部分,然后以'.'分隔,例如org.apache.dubbo.xxx.YyyInvokerWrapper接口名會變成yyy.invoker.wrapper,然后以此名稱做為key到URL尋找,如果仍沒有找到,則拋出IllegalStateException異常;Adaptive注解用在類上,表示此類是它實現接口(插口)的自適應插件
- Activate:這個注解可以使用在類或方法上,用以根據URL的key值判斷當前extension是否生效,當一個extension有多個實現時,可以加載特定的extension實現類,例如extension實現類上有注解@Activate("cache, validation"),則當URL上出現"cache”或“validation" key時,當前extension才會生效
ExtensionLoader
顧名思義,ExtensionLoader用于加載extension,它的作用有三點:1.自動加載extension;2.自動包裝(wrap) extension;3.創建自適應的(adaptive)extension;
旅途開始
先看下上篇文章中Provider端的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<!-- 提供方應用信息,用于計算依賴關系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast廣播注冊中心暴露服務地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo協議在20880端口暴露服務 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 聲明需要暴露的服務接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一樣實現服務 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>
還是先從ClassPathXmlApplicationContext加載spring配置文件說起,上回我們說到ClassPathXmlApplicationContext會使用XmlBeanDefinitionReader將xml文件解析成BeanDefiniton集合,當解析<dubbo:protocol />標簽時,會將其解析成org.apache.dubbo.config.ProtocolConfig對象(為什么?請看上回分解最后,protocol key 實例化DubboBeanDefinitionParser時傳入的參數),解析<dubbo:service />時,會將其解析成org.apache.dubbo.config.spring.ServiceBean對象。在解析xml時,會調用AbstractApplicationContext的refresh()方法
ServiceBean是ServiceConfig的子類,所以在創建ServiceBean對象的時候,會去先實例化父類,ServiceConfig中有一個static final成員變量protocol
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
ExtensionLoader終于出場了,想要獲取插件,得分兩步走,第一步得到Protocol的插件加載對象extensionLoader,然后由這個加載對象獲得對應的插件。
先來看第一步:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
//一些檢查的代碼,省略
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
EXTENSION_LOADERS保存的是目前已經保存的插口的加載類,顯然第一次加載的時候,Protocol還沒有自己的插件加載類,那么需要實例化一個。實例化加載對象之后,用這個對象去加載插件。
public T getAdaptiveExtension() {
//從已經緩存的自適應對象中獲得,第一次調用時還沒有創建自適應類,所以instance為null
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//創建一個自適應類
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
主要關注 instance = createAdaptiveExtension();這句,createAdaptiveExtension()方法是什么樣的呢?
private T createAdaptiveExtension() {
try {
//得到自適應類并實現化,然后注入屬性值
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
getAdaptiveExtensionClass():
private Class<?> getAdaptiveExtensionClass() {
//1.獲取所有實現Protocol插口的插件類
getExtensionClasses();
//2.如果有自適應插件類,則返回
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//3.如果沒有,則創建插件類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
先來看上面的第1步,getExtensionClasses()
private Map<String, Class<?>> getExtensionClasses() {
//從緩存中獲取插件類,第一次肯定沒有
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
//實際的加載插件類方法
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
//ExtensionLoader中的三個常量,加載插件的目錄,第一個熟悉吧,是java spi的默認目錄
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private Map<String, Class<?>> loadExtensionClasses() {
//獲取插口上SPI注解的值,默認值只能有一個,如果多于一個,則拋異常
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((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];
}
}
//加載以上三個目錄下的實現了相應插口的插件類(本例中插口是Protocol)
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
實現Protocol插口的共有四個插件:
再來看上面getAdaptiveExtensionClass方法的第2步,這一句是判斷有沒有自適應類,在加載配置的插件過程中,會判斷此插件類是不是自適應插件類,判斷的依據就是插件類上是否有注解@Adaptive,Protocol的這四個插件類上都沒有此注解,所以沒有自適應插件,則會走到第3步,創建一個自適應插件類
private Class<?> createAdaptiveExtensionClass() {
//生成類代碼
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
//得到編輯器,并將類代碼編譯成字節碼
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
//來看看生成類代碼的過程,以生成Protocol插件類代碼為例
private String createAdaptiveExtensionClassCode() {
StringBuilder codeBuilder = new StringBuilder();
//得到Protocol接口所有方法
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
for (Method m : methods) {
if (m.isAnnotationPresent(Adaptive.class)) {
hasAdaptiveAnnotation = true;
break;
}
}
// // 如果方法上沒有@Adaptive注解,則不能創建自適應插件類
if (!hasAdaptiveAnnotation)
throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");
codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
//類名為Protocol$Adaptive實現了Protocol接口
codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");
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;
// find URL getter method
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 adaptive 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();
// value is not set, use the value generated from class name as the 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("org.apache.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]))
//如果方法參數類型名稱為"org.apache.dubbo.rpc.Invocation"則從url獲取以此參數類型名為key的值,獲取不到則取默認擴展名,即Protocol接口上注解SPI的值“dubbo”
if (hasInvocation)
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
else
//否則,取從url中取以方法上注解adaptive的值為key對應的值
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(");");
}
codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
for (int i = 0; i < pts.length; i++) {
if (i > 0) {
codeBuilder.append(", ");
}
codeBuilder.append(pts[i].getCanonicalName());
codeBuilder.append(" ");
codeBuilder.append("arg").append(i);
}
codeBuilder.append(")");
if (ets.length > 0) {
codeBuilder.append(" throws ");
for (int i = 0; i < ets.length; i++) {
if (i > 0) {
codeBuilder.append(", ");
}
codeBuilder.append(ets[i].getCanonicalName());
}
}
codeBuilder.append(" {");
codeBuilder.append(code.toString());
codeBuilder.append("\n}");
}
codeBuilder.append("\n}");
if (logger.isDebugEnabled()) {
logger.debug(codeBuilder.toString());
}
return codeBuilder.toString();
}
我們來看下生成的插件類Protocol$Adaptive代碼:
package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
public void destroy()
{throw new UnsupportedOperationException("method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException("method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
}
public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
if (arg1 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}
public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
}
可以看出此類可以根據url中參數protocol值加載對應的插件,如果url中沒有,則加載名為"dubbo"對應的插件,而從前面加載的四個插件可以看出,名稱為dubbo的插件類為org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.
寫到這里總算將SPI加載的過程大體上講述了一篇,Dubbo中還有許多類似的插件,原理基本相同;除了有的插口有自適應插件,比如org.apache.dubbo.common.compiler.Compiler和org.apache.dubbo.common.extension.ExtensionFactory,自適應插件類上都有注解@Adaptive,比如Compile的自適應插件AdaptiveCompiler,ExtensionFactory的自適應插件AdaptiveExtensionFactory.
為什么要提供自適應插件,而不是都在運行時生成?
答:
(1)解決雞生蛋,蛋生雞的問題,上面createAdaptiveExtensionClass方法中,在第1步生成Protocol$Adaptive類后,會使用編譯器將其編譯成字節碼,但是編譯器本身也是插件化的,可以有好幾種編譯器,所以需要提供一個已經存在的自適應編譯器(AdaptiveCompiler),然后在編譯的時候,使用此編譯器找到Compile接口上SPI注解中配置的默認的編譯器進行編譯。
(2)解決對象生成方式不同導致的加載問題;Dubbo中對象的生成一類是由Spring容器創建,一類是根據插件文件的配置動態加載;所以要想獲取這兩部分對象,需要使用不同的方式;而AdaptiveExtensionFactory就是為了解決這個問題,在獲取對象時,分別從Spring容器和ExtensionLoader中查找。