Dubbo如何通過SPI提高框架的可擴展性?
介紹
最近看了一下Dubbo的源碼,國人寫的框架和國外的果然是兩種不同的風格,Dubbo的源碼還是比較清晰容易懂的。Spring框架一個Bean的初始化過程就能繞死在源碼中.
Dubbo的架構是基于分層來設計的,每層執(zhí)行固定的功能,上層依賴下層,下層的改變對上層不可見,每層都是可以被替換的組件
Service和Config為API接口層,讓Dubbo使用者方便的發(fā)布和引用服務
其他各層均為SPI層,意味著每層都是組件化的,可以被替換
例如,注冊中心可以用Redis,Zookeeper。傳輸協議可以用dubbo,rmi,hessian等。
網絡通信可以用mina,netty。序列化可以用fastjson,hessian2,java原生的方式等
SPI 全稱為 Service Provider Interface,是一種服務發(fā)現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態(tài)為接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能
那么Dubbo的SPI是怎么實現的呢?先來了解一下Java SPI
Java SPI
Java SPI是通過策略模式實現的,一個接口提供多個實現類,而使用哪個實現類不在程序中確定,而是配置文件配置的,具體步驟如下
- 定義接口及其對應的實現類
- 在META-INF/services目錄下創(chuàng)建以接口全路徑命名的文件
- 文件內容為實現類的全路徑名
- 在代碼中通過java.util.ServiceLoader#load加載具體的實現類
寫個Demo演示一下
- public interface Car {
- void getBrand();
- }
- public class BenzCar implements Car {
- @Override
- public void getBrand() {
- System.out.println("benz");
- }
- }
- public class BMWCar implements Car {
- @Override
- public void getBrand() {
- System.out.println("bmw");
- }
- }
org.apache.dubbo.Car的內容如下
- org.apache.dubbo.BenzCar
- org.apache.dubbo.BMWCar
測試類
- public class JavaSpiDemo {
- public static void main(String[] args) {
- ServiceLoader<Car> carServiceLoader = ServiceLoader.load(Car.class);
- // benz
- // bmw
- carServiceLoader.forEach(Car::getBrand);
- }
- }
Dubbo SPI
用Dubbo SPI將上面的例子改造一下
- @SPI
- public interface Car {
- void getBrand();
- }
- public class BenzCar implements Car {
- @Override
- public void getBrand() {
- System.out.println("benz");
- }
- }
- public class BMWCar implements Car {
- @Override
- public void getBrand() {
- System.out.println("bmw");
- }
- }
org.apache.dubbo.quickstart.Car的內容如下
- benz=org.apache.dubbo.quickstart.BenzCar
- bmw=org.apache.dubbo.quickstart.BMWCar
測試類
- public class DubboSpiDemo {
- public static void main(String[] args) {
- ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
- Car car = extensionLoader.getExtension("benz");
- car.getBrand();
- }
- }
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE})
- public @interface SPI {
- String value() default "";
- }
@SPI標記接口是一個Dubbo SPI接口,即是一個擴展點,value屬性可以指定默認實現
Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。Dubbo SPI的優(yōu)點如下
- JDK標準的SPI會一次性實例化擴展點的所有實現。而Dubbo SPI能實現按需加載
- Dubbo SPI增加了對擴展點Ioc和Aop的支持
Dubbo SPI的實現步驟如下
- 定義接口及其對應的實現類,接口上加@SPI注解,表明這是一個擴展類
- 在META-INF/services目錄下創(chuàng)建以接口全路徑命名的文件
- 文件內容為實現類的全路徑名
- 在代碼中通過ExtensionLoader加載具體的實現類
Dubbo SPI 擴展點的特性自動包裝
擴展類的構造函數是一個擴展點,則認為這個類是一個Wrapper類,即AOP
用例子演示一下
- @SPI
- public interface Car {
- void getBrand();
- }
- public class BenzCar implements Car {
- @Override
- public void getBrand() {
- System.out.println("benz");
- }
- }
- public class CarWrapper implements Car {
- private Car car;
- public CarWrapper(Car car) {
- this.car = car;
- }
- @Override
- public void getBrand() {
- System.out.println("start");
- car.getBrand();
- System.out.println("end");
- }
- }
org.apache.dubbo.aop.Car內容如下(resources\META-INF\services目錄下)
- benz=org.apache.dubbo.aop.BenzCar
- org.apache.dubbo.aop.CarWrapper
測試類
- public class DubboSpiAopDemo {
- public static void main(String[] args) {
- ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
- Car car = extensionLoader.getExtension("benz");
- // start
- // benz
- // end
- car.getBrand();
- }
- }
BenzCar是一個擴展類,CarWrapper是一個包裝類,當獲取BenzCar的時候實際獲取的是被CarWrapper包裝后的對象,類似代理模式
自動加載
如果一個擴展類是另一個擴展類的成員變量,并且擁有set方法,框架會自動注入這個擴展點的實例,即IOC。先定義2個擴展點
org.apache.dubbo.ioc.Car(resources\META-INF\services目錄下)
- benz=org.apache.dubbo.ioc.BenzCar
org.apache.dubbo.ioc.Wheel(resources\META-INF\services目錄下)
- benz=org.apache.dubbo.ioc.BenzWheel
- @SPI
- public interface Wheel {
- void getBrandByUrl();
- }
- public class BenzWheel implements Wheel {
- @Override
- public void getBrandByUrl() {
- System.out.println("benzWheel");
- }
- }
- @SPI
- public interface Car {
- void getBrandByUrl();
- }
- public class BenzCar implements Car {
- private Wheel wheel;
- public void setWheel(Wheel wheel) {
- this.wheel = wheel;
- }
- @Override
- public void getBrandByUrl() {
- System.out.println("benzCar");
- wheel.getBrandByUrl();
- }
- }
測試demo
- public class DubboSpiIocDemo {
- public static void main(String[] args) {
- ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
- Car car = extensionLoader.getExtension("benz");
- car.getBrandByUrl();
- }
- }
我跑這個代碼的時候直接報異常,看了一下官網才發(fā)現dubbo是可以注入接口的實現的,但不像spring那么智能,
dubbo必須用URL(類似總線)來指定擴展類對應的實現類.。這就不得不提到@Adaptive注解了
自適應
使用@Adaptive注解,動態(tài)的通過URL中的參數來確定要使用哪個具體的實現類
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE, ElementType.METHOD})
- public @interface Adaptive {
- String[] value() default {};
- }
- @SPI
- public interface Wheel {
- @Adaptive("wheel")
- void getBrandByUrl(URL url);
- }
- public class BenzWheel implements Wheel {
- @Override
- public void getBrandByUrl(URL url) {
- System.out.println("benzWheel");
- }
- }
- @SPI
- public interface Car {
- void getBrandByUrl(URL url);
- }
- public class BenzCar implements Car {
- // 這個里面存的是代理對象
- private Wheel wheel;
- public void setWheel(Wheel wheel) {
- this.wheel = wheel;
- }
- @Override
- public void getBrandByUrl(URL url) {
- System.out.println("benzCar");
- // 代理類根據URL找到實現類,然后再調用實現類
- wheel.getBrandByUrl(url);
- }
- }
- public class DubboSpiIocDemo {
- public static void main(String[] args) {
- ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
- Car car = extensionLoader.getExtension("benz");
- Map<String, String> map = new HashMap<>();
- // 指定wheel的實現類為benz
- map.put("wheel", "benz");
- URL url = new URL("", "", 1, map);
- // benzCar
- // benzWheel
- car.getBrandByUrl(url);
- }
- }
可以看到BenzCar對象成功注入了BenzWheel。BenzCar中其實注入的是BenzWheel的代碼對象,這個代理對象會根據@Adaptive("wheel")獲取到wheel,然后從url中找到key為wheel的值,這個值即為實現類對應的key。
上面的注釋提到BenzCar里面注入的Wheel其實是一個代理對象(框架幫我們生成),在代理對象中根據url找到相應的實現類,然后調用實現類。
因為代理對象是框架在運行過程中幫我們生成的,沒有文件可以查看,所以用Arthas來查看一下生成的代理類
- curl -O https://alibaba.github.io/arthas/arthas-boot.jar
- java -jar arthas-boot.jar
- # 根據前面的序號選擇進入的進程,然后執(zhí)行下面的命令
- jad org.apache.dubbo.adaptive.Wheel$Adaptive
生成的Wheel
- package org.apache.dubbo.adaptive;
- import org.apache.dubbo.adaptive.Wheel;
- import org.apache.dubbo.common.URL;
- import org.apache.dubbo.common.extension.ExtensionLoader;
- public class Wheel$Adaptive
- implements Wheel {
- public void getBrandByUrl(URL uRL) {
- if (uRL == null) {
- throw new IllegalArgumentException("url == null");
- }
- URL uRL2 = uRL;
- String string = uRL2.getParameter("wheel");
- if (string == null) {
- throw new IllegalStateException(new StringBuffer().append("Failed to get extension (org.apache.dubbo.adaptive.Wheel) name from url (").append(uRL2.toString()).append(") use keys([wheel])").toString());
- }
- Wheel wheel = (Wheel)ExtensionLoader.getExtensionLoader(Wheel.class).getExtension(string);
- wheel.getBrandByUrl(uRL);
- }
- }
@Adaptive可以標記在類上或者方法上
標記在類上:將該實現類直接作為默認實現,不再自動生成代碼
標記在方法上:通過參數動態(tài)獲得實現類,比如上面的例子
用源碼演示一下用在類上的@Adaptiv,Dubbo為自適應擴展點生成代碼,如我們上面的WheelAdaptive類如下所示¨G30G∗∗@Adaptive可以標記在類上或者方法上∗∗標記在類上:將該實現類直接作為默認實現,不再自動生成代碼標記在方法上:通過參數動態(tài)獲得實現類,比如上面的例子用源碼演示一下用在類上的@Adaptiv,Dubbo為自適應擴展點生成代碼,如我們上面的WheelAdaptive,但生成的代碼還需要編譯才能生成class文件。我們可以用JavassistCompiler(默認的)或者JdkCompiler來編譯(需要配置),這個小小的功能就用到了@Adaptive
如果想用JdkCompiler需要做如下配置
- <dubbo:application compiler="jdk" />
Compiler類圖如下
- @SPI("javassist")
- public interface Compiler {
- Class<?> compile(String code, ClassLoader classLoader);
- }
Compiler用@SPI指定了默認實現類為javassist
源碼中獲取Compiler調用了如下方法
- org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
getAdaptiveExtension()會獲取自適應擴展類,那么這個自適應擴展類是誰呢?
是AdaptiveCompiler,因為類上有@Adaptive注解
- @Adaptive
- public class AdaptiveCompiler implements Compiler {
- private static volatile String DEFAULT_COMPILER;
- public static void setDefaultCompiler(String compiler) {
- DEFAULT_COMPILER = compiler;
- }
- /**
- * 獲取對應的Compiler,并調用compile做編譯
- * 用戶設置了compiler,就用設置了的,不然就用默認的
- */
- @Override
- public Class<?> compile(String code, ClassLoader classLoader) {
- Compiler compiler;
- ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
- String name = DEFAULT_COMPILER; // copy reference
- if (name != null && name.length() > 0) {
- // 用用戶設置的
- compiler = loader.getExtension(name);
- } else {
- // 用默認的
- compiler = loader.getDefaultExtension();
- }
- return compiler.compile(code, classLoader);
- }
- }
從compile方法可以看到,如果用戶設置了編譯方式,則用用戶設置的,如果沒有設置則用默認的,即JavassistCompiler
自動激活
使用@Activate注解,可以標記對應的擴展點默認被激活使用
- @Documented
- @Retention(RetentionPolicy.RUNTIME)
- @Target({ElementType.TYPE, ElementType.METHOD})
- public @interface Activate {
- // 所屬組,例如消費端,服務端
- String[] group() default {};
- // URL中包含屬性名為value的鍵值對,過濾器才處于激活狀態(tài)
- String[] value() default {};
- // 指定執(zhí)行順序,before指定的過濾器在該過濾器之前執(zhí)行
- @Deprecated
- String[] before() default {};
- // 指定執(zhí)行順序,after指定的過濾器在該過濾器之后執(zhí)行
- @Deprecated
- String[] after() default {};
- // 指定執(zhí)行順序,值越小,越先執(zhí)行
- int order() default 0;
- }
可以通過指定group或者value,在不同條件下獲取自動激活的擴展點。before,after,order是用來排序的,感覺一個order參數就可以搞定排序的功能,所以官方把before,after標記為@Deprecated
Dubbo Filter就是基于這個來實現的。Dubbo Filter是Dubbo可擴展性的一個體現,可以在調用過程中對請求進行進行增強
我寫個demo演示一下這個自動激活是怎么工作的
- @SPI
- public interface MyFilter {
- void filter();
- }
consumer組能激活這個filter
- @Activate(group = {"consumer"})
- public class MyConsumerFilter implements MyFilter {
- @Override
- public void filter() {
- }
- }
provider組能激活這個filter
- @Activate(group = {"provider"})
- public class MyProviderFilter implements MyFilter {
- @Override
- public void filter() {
- }
- }
consumer組和provide組都能激活這個filter
- @Activate(group = {"consumer", "provider"})
- public class MyLogFilter implements MyFilter {
- @Override
- public void filter() {
- }
- }
consumer組和provide組都能激活這個filter,同時url中指定key的value為cache
- @Activate(group = {"consumer", "provider"}, value = "cache")
- public class MyCacheFilter implements MyFilter {
- @Override
- public void filter() {
- }
- }
測試類如下
getActivateExtension有3個參數,依次為url, key, group
- public class ActivateDemo {
- public static void main(String[] args) {
- ExtensionLoader<MyFilter> extensionLoader = ExtensionLoader.getExtensionLoader(MyFilter.class);
- // url中沒有參數
- URL url = URL.valueOf("test://localhost");
- List<MyFilter> allFilterList = extensionLoader.getActivateExtension(url, "", null);
- /**
- * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
- * org.apache.dubbo.activate.MyProviderFilter@73a8dfcc
- * org.apache.dubbo.activate.MyLogFilter@ea30797
- *
- * 不指定組則所有的Filter都被激活
- */
- allFilterList.forEach(item -> System.out.println(item));
- System.out.println();
- List<MyFilter> consumerFilterList = extensionLoader.getActivateExtension(url, "", "consumer");
- /**
- * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
- * org.apache.dubbo.activate.MyLogFilter@ea30797
- *
- * 指定consumer組,則只有consumer組的Filter被激活
- */
- consumerFilterList.forEach(item -> System.out.println(item));
- System.out.println();
- // url中有參數myfilter
- url = URL.valueOf("test://localhost?myfilter=cache");
- List<MyFilter> customerFilter = extensionLoader.getActivateExtension(url, "myfilter", "consumer");
- /**
- * org.apache.dubbo.activate.MyConsumerFilter@53e25b76
- * org.apache.dubbo.activate.MyLogFilter@ea30797
- * org.apache.dubbo.activate.MyCacheFilter@aec6354
- *
- * 指定key在consumer組的基礎上,MyCacheFilter被激活
- */
- customerFilter.forEach(item -> System.out.println(item));
- System.out.println();
- }
- }
總結一下就是,getActivateExtension不指定組就是激活所有的Filter,指定組則激活指定組的Filter。指定key則從Url中根據key取到對應的value,假設為cache,然后把@Activate注解中value=cache的Filter激活
即group用來篩選,value用來追加,Dubbo Filter就是靠這個屬性激活不同的Filter的
ExtensionLoader的工作原理
ExtensionLoader是整個Dubbo SPI的主要實現類,有如下三個重要方法,搞懂這3個方法基本上就搞懂Dubbo SPI了。
加載擴展類的三種方法如下
- getExtension(),獲取普通擴展類
- getAdaptiveExtension(),獲取自適應擴展類
- getActivateExtension(),獲取自動激活的擴展類
getExtension()上面的例子中已經有了。自適應的特性上面已經演示過了,當獲取Wheel的實現類是框架會調用getAdaptiveExtension()方法。
代碼就不放了,這3個方法的執(zhí)行過程還是比較簡單的,如果你有看不懂的,可以看我給源碼加的注釋。
https://github.com/erlieStar/dubbo-analysis
理解了Dubbo SPI你應該就把Dubbo搞懂一半了,剩下就是一些服務導出,服務引入,服務調用的過程了
本文轉載自微信公眾號「Java識堂」,可以通過以下二維碼關注。轉載本文請聯系Java識堂公眾號。