一文帶你徹底了解Dubbo的SPI機制
文章目錄
Java SPI使用
Dubbo為何自己實現(xiàn)一套SPI
Dubbo SPI初體驗
SPI
IOC
Aop
什么是包裝類?
AOP增強
@Adaptive
一些需要注意的地方
@Activate
結尾
在分布式系統(tǒng)中服務的調用,就要涉及到RPC。而提起RPC,想到最多的就是dubbo。了解dubbo的工作原理,有助于我們更好的使用它。
打開下載的dubbo工程,我發(fā)現(xiàn)在dubbo的各個子模塊,有很多這樣的類似代碼:ExtensionLoader.getExtensionLoader(xxx.class),經過我一番百度后發(fā)現(xiàn)他們指向一個名次:SPI。官網(wǎng)中表示Dubbo采用微內核+SPI,使得有特殊需求的接入方可以自定義擴展,做定制的二次開發(fā)。好像解釋了什么,但是又好像還不清楚它是干什么的,不過不要急,接下來我們就從例子使用到dubbo的源碼來剖析spi的神秘面紗。
Java SPI使用
在了解dubbo spi之前,我們就不得不提一下Java SPI。SPI的全稱是service provider interface,起初是提供給廠商做插件開發(fā)的 ,它使用了策略模式, 一個接口多種實現(xiàn)。我們只聲明接口, 具體的實現(xiàn)并不在程序中直接確定, 而是由程序之外的配置掌控 。啥意思呢?我們搞個例子就明白了:
//定義一個接口
public interface DoWork {
void doWork();
}
//兩個實現(xiàn)類
public class WriteBug implements DoWork {
@Override
public void doWork() {
System.out.println("寫bug");
}
}
public class WriteCode implements DoWork {
@Override
public void doWork() {
System.out.println("寫程序");
}
}
在META-INF.services目錄下創(chuàng)建一個文件,文件名即為接口的全路徑名:com.alibaba.dubbo.demo.provider.JavaSPI.DoWork:這個樣子;
文件中的內容:
com.alibaba.dubbo.demo.provider.JavaSPI.WriteBug
com.alibaba.dubbo.demo.provider.JavaSPI.WriteCode
準備工作已經做完了,接下來測試一下效果如何:
public static void main(String[] s){
//獲取到DoWork的所有實現(xiàn)
ServiceLoader<DoWork> serviceServiceLoader = ServiceLoader.load(DoWork.class);
for (DoWork doWork : serviceServiceLoader){
doWork.doWork();
}
}
運行結果:
寫bug
寫程序
可以看到執(zhí)行了全部實現(xiàn)類的方法,因此我們可以理解為spi實際上就是在文件中記錄接口都有哪些實現(xiàn)類,然后根據(jù)接口名來實例化它的實現(xiàn)類。
Dubbo為何自己實現(xiàn)一套SPI
但是實際上dubbo并沒有采用Java的SPI,而是自己實現(xiàn)了一套SPI機制。既然有Java的SPI,為什么dubbo不用呢?
我們觀察上面的例子,可以發(fā)現(xiàn)其實Java SPI實際上是將接口所有的實現(xiàn)類都加載出來了,如果項目過大,那么會加載全部的實現(xiàn)類,那肯定會有一些實現(xiàn)類實現(xiàn)類沒有用上,這樣就造成了浪費。
另一方面,Java的SPI功能也比較單一,dubbo的spi在此基礎上還實現(xiàn)了ioc、aop等功能,這些我會在下面從源碼的角度來分析這些功能。
Dubbo SPI初體驗
既然我們上面看了Java的SPI如何使用,那這個dubbo的spi我們也是需要看看的。
在dubbo中,約定文件是放在以下三個目錄中:
META-INF/services/
META-INF/dubbo/
META-INF/dubbo/internal/
大部分還是比較類似的,先定義一個接口、兩個實現(xiàn)類,要注意接口上面需要加@SPI注解的。
//注意加上@SPI注解
@SPI
public interface DoWork {
void doWork();
}
public class WriteBug implements DoWork {
@Override
public void doWork() {
System.out.println("寫bug");
}
}
public class WriteCode implements DoWork{
@Override
public void doWork() {
System.out.println("寫程序");
}
}
dubbo spi文件內容與java spi文件內容略微不太相同,可以理解為key-value的形式,value就是實現(xiàn)類的全路徑名,這個key呢可以理解為這個類名的簡稱,這個要記得,下面會用得到:
bug=com.alibaba.dubbo.demo.provider.DubboSPI.WriteBug
code=com.alibaba.dubbo.demo.provider.DubboSPI.WriteCode
public static void main(String[] args) {
ExtensionLoader<DoWork> extensionLoader = ExtensionLoader.getExtensionLoader(DoWork.class);
//這個code就是上面說的"簡稱"
DoWork code = extensionLoader.getExtension("code");
code.doWork();
DoWork bug = extensionLoader.getExtension("bug");
bug.doWork();
}
ExtensionLoader類包含了整個SPI的核心方法,包括像下面的獲取@Adaptive、@Active注解的信息等,都是在這個類中實現(xiàn)的。
getExtensionLoader方法可以獲得對應接口的Extension加載器,這個方法也是比較簡單的;
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);//從緩存中拿
if (loader == null) {
//緩存中沒有的話,就去new一個放到這個map中,然后再獲取返回
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
其實就是從緩存中拿,如果緩存中有,那么就取緩存中的,緩存中沒有,那么就new一個放進去。
ok,現(xiàn)在獲取到了擴展實現(xiàn)類加載器,接下來就看如何通過執(zhí)行getExtension(很重要,可以標記為五星重要程度,后面還會用到的)來獲取到對應的實例。
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
return getDefaultExtension();
}
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {//雙檢鎖
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
代碼行數(shù)雖然比較多,實際上都是從緩存中取,看是否有值,沒有值的話就去創(chuàng)建。因為我們現(xiàn)在是首次加載,那肯定是沒值的,所以就要到這個createExtension方法中:
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);//獲取名稱對應的class
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//通過反射來創(chuàng)建實例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);//依賴注入
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {//對于那些wrapper類型的,進行包裝一下
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);
}
}
這里的核心方法就兩個:getExtensionClasses、injectExtension。這兩個方法在后面的@Adaptive、@Active注解中都會用到。
首先是這個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;
}
可以看到他其實也是這個從緩存中拿,沒有的話就創(chuàng)建,核心方法就是這個loadExtensionClasses方法。
private Map<String, Class<?>> loadExtensionClasses() {
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];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
還記得最開始的時候說的dubbo會掃描三個目錄下的文件么?沒錯就是這三個loadDirectory方法里面的目錄。
最終調用的loadClass加載方法:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {//如果是自適應的話,只會有一個
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else if (isWrapperClass(clazz)) {//包裝類的話,可以放到set中,這個是可以有多個的。
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {//說明既不是@Adaptive,也不是裝飾類,他只是普通的或者是active。
clazz.getConstructor();//如果沒有默認構造,那么就報錯
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);//設置class與名字的映射
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);//記錄名字與類的映射
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
在這里我給大家總結一下這個方法:先看這個類上面有沒有@Adaptive注解。如果有@Adaptive注解,那么就將它緩存起來,原則上一個接口只允許有一個@Adaptive,這是為啥呢?我們可以看上面的代碼,如果有多個@Adaptive注解的話,他是會拋出異常的。
然后再看這個接口的實現(xiàn)類是否有以Wrapper結尾的實現(xiàn)類,比如Protocol的其中一個實現(xiàn)類:ProtocolFilterWrapper,如果是的話,就會放到一個set集合中。如果這兩種都不是的話,那么就是普通的擴展類,將他們存到一個map中返回就可以了。在存到這個map的時候,是以類對應的簡寫為key,類的路徑為value。
IOC
我們可以先看下這個injectExtension中有這么一段方法:
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())) {
/
可以看到,它是先獲取到這個實例對應類中的全部方法,然后呢,依次循環(huán)遍歷,對于那些以set開頭、只有一個參數(shù)、并且為public的方法,我們就認為這是需要對這個實例進行屬性注入的部分。
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);
}
}
這個其實就是要獲取到需要注入的屬性,就是將第四個字符設置為小寫,后面的不動進行分割。比如setUserName,分割完畢后就是userName,這樣有屬性,又有類路徑,就可以通過反射對原先的實例進行屬性注入,也就完成了依賴注入。
Aop
這里分為兩部分:判斷是否為包裝類以及對目標對象進行增強。
什么是包裝類?
舉個簡單的栗子:
public class Son implements Parent {
private Parent parent;
public Son(Parent parent) {
this.parent = parent;
}
}
在dubbo中是這個樣子的:
也就是實現(xiàn)類的構造方法的參數(shù),為他自己所實現(xiàn)的接口。而且在dubbo中。包裝類都是以Wrapper結尾的。
緩存包裝類
緩存包裝類是在ExtensionLoader.loadClass中:
else if (isWrapperClass(clazz)) {//包裝類的話,可以放到set中,這個是可以有多個的。
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
}
private boolean isWrapperClass(Class<?> clazz) {
try {
Constructor<?> constructor = clazz.getConstructor(type);//看類的構造參數(shù)中,有沒有對應接口的參數(shù),如果有,就說明是包裝類
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
通過遍歷所有的實現(xiàn)類,找出這些包裝類,然后放入緩存中。
AOP增強
@SuppressWarnings("unchecked")
private T createExtension(String name) {
Map<String, Class<?>> extensionClasses = getExtensionClasses();
Class<?> clazz = extensionClasses.get(name);//獲取名稱對應的class
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//通過反射來創(chuàng)建實例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
injectExtension(instance);//依賴注入
//獲取包裝類集合
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
//遍歷
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);
}
}
@Adaptive
@SPI注解那里存的是默認值,這個會在動態(tài)生成編譯的類中,會設置默認值
從上面的例子中可以看到,通過SPi,我們可以每次只加載對應接口的實現(xiàn)類,這樣可以減少加載全部實現(xiàn)類帶來的開銷。但是我又有一個問題了,我不想在啟動項目的時候加載這些擴展類,而是希望在運行時根據(jù)參數(shù),來動態(tài)加載擴展類,這個該怎么做呢?
此時@Adaptive注解就派上用場了。
@Adaptive有兩種用法:一種是添加到類上,另一種則是添加到方法上。
如果添加到類上,表示該類是接口的適配器。但是實際上,注解添加到類上的是很少的,大部分都是添加到方法上的。目前只有AdaptiveExtensionFactory和AdaptiveCompiler是添加到類上的。
在這里我們就以這個AdaptiveExtensionFactory為例,來看下這個:
AdaptiveExtensionFactory的類上有這個@Adaptive注解,在執(zhí)行構造方法的時候,就會將ExtensionLoader對應的實現(xiàn)類加載到一個List中緩存起來,這樣的話在getExtension的時候,傳入要取的class,key,就可以直接從這個list中取出對應的值。
2.事實上大部分情況下都是注解到方法上的,他會根據(jù)接口的信息,來動態(tài)拼接成一個代理類。為了更清楚了解@Adaptive的實現(xiàn),我們就從源碼的角度,以RegistryFactory為例子來看下這個過程到底是啥樣的:
方法起始于ExtensionLoader.getAdaptiveExtension:
public T getAdaptiveExtension() {
//從緩存中拿,沒有的話就創(chuàng)建
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
return (T) instance;
}
private T createAdaptiveExtension() {
//主要方法為getAdaptiveExtensionClass
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();//通過這個方法來獲取到Adaptive注解的類。
if (cachedAdaptiveClass != null) {//如果有,那么就說明有@Adaptive注解添加到類上了。
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();//沒有的話就創(chuàng)建。注解在方法上的話,那么就需要進行編譯。
}
這里我省略了一些無關緊要的代碼,保留了核心代碼,getExtensionClasses這個方法是不是感覺很熟悉呢?沒錯,在上面我們也介紹過他的源碼了,這里就不多說了,直接ctrl+f搜索就可以了。
因為這個接口上的@Adaptive是在接口上的,所以cachedAdaptiveClass是為空的,這里就需要執(zhí)行createAdaptiveExtensionClass去創(chuàng)建實現(xiàn)類了。
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);
}
這個code里面就是包含了代理實現(xiàn)類的所有信息,他長這個樣子:
看起來不太直接,而且也沒辦法調試,我們可以將他復制下來,ctrl+alt+l進行格式化一下,這樣在下次啟動程序的時候,斷點就可以進入這個類中了。
進入這個方法中,根據(jù)傳入的URL中的參數(shù)值,來調用getExtension方法,最終得到需要的擴展類。
PS:在dubbo3.0的時候項目中就已經放了這些$Adaptive的類了。
一些需要注意的地方
當@Adaptive添加到方法上的時候,要保證方法中的參數(shù)至少有一個為URL。
但是我們發(fā)現(xiàn)有的方法中的參數(shù),只有一個invoker,比如這樣的:
這個方法中就沒有URL,那這是咋回事?
通過查看拼接生成的代理類,我們發(fā)現(xiàn),通過Invoker是可以得到URL的:
所以從某種意義上來說的話,填入了Invoker也相當于填入了URL。
關于SPI注解與@Adaptive注解中的值。
SPI注解后面的值是默認的值,當URL中的值為空的時候,就使用默認的值,比如上面圖片中的:
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
這個dubbo就是默認值,也是@SPI中的值。
而@Adaptive添加到方法上時,后面的參數(shù)表示在加載時優(yōu)先使用的值。
@Activate
這個注解是擴展點自動激活加載,在這個注解上可以設置group、value、order等值。
這些值主要表示注解在的類、方法上所屬的分組、執(zhí)行順序等,當傳入的參數(shù)符合直接中的值的時候,可以進行激活。
這個注解對應的源碼,其實大部分在上面都已經說過了,總的來說,并不是特別的難,源碼在這里,大家可以先看一下:
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
//1
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
if (isMatchGroup(group, activate.group())) {
if (!names.contains(name)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
//2
T ext = getExtension(name);
exts.add(ext);
}
}
}
//排序
Collections.sort(exts, ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<T>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
//3
T ext = c(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
他的核心方法就是我在里面標記的這幾個,其余的呢,都是循環(huán)啦,取值之類的操作,沒有什么太多的復雜邏輯。
結尾
以上就是dubbo spi的核心內容了,因為spi貫穿了dubbo的所有核心功能,所以要想看懂dubbo的源碼,那么就需要先了解SPI這個前置任務。大家可以一邊對照著文章,一邊打斷點,這樣對SPI也會了解更深入。
作者:java知路
歡迎關注微信公眾號 :java知路