使用SpringCloudAlibaba整合Dubbo

SpringCloudAlibaba整合Dubbo

Spring Cloud是一套較為完整的架構方案,而Dubbo隻是服務治理與RPC實現方案。

Dubbo的註冊中心采用瞭ZooKeeper,而Spring Cloud體系中的註冊中心並不支持ZooKeeper。直到Spring Cloud Alibaba的出現,才得以解決這樣的問題,Spring Cloud Alibaba中的Nacos來作為服務註冊中心,並且在此之下可以如傳統的Spring Cloud應用一樣地使用Ribbon或Feign來實現服務消費,並提供瞭Spring Cloud Alibaba下額外支持的RPC方案:Dubbo

構建服務接口

public interface HelloService {
    String hello(String name);
}

構建服務接口提供方

(1)創建一個Spring Boot項目,在pom.xml中引入第一步中構建的API包以及Spring Cloud Alibaba對Nacos和Dubbo的依賴

<dependency>
     <groupId>top.onething</groupId>
     <artifactId>product-api</artifactId>
     <version>1.0-SNAPSHOT</version>
 </dependency>
 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
     <version>2.2.1.RELEASE</version>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <version>2.2.5.RELEASE</version>
 </dependency>
 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
     <version>2.2.1.RELEASE</version>
 </dependency>
 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-dubbo</artifactId>
     <version>2.2.1.RELEASE</version>
 </dependency>
 <!--Dubbo Spring Cloud基於Spring Cloud Commons開發的,org.apache.http.client.HttpClient類確實存在於舊版本的httpclient中,
 
 但是它使用內部的org.apache.http.impl.client.HttpClientBuilder類在新版本中卻不存在-->
 
 <dependency>
     <groupId>org.apache.httpcomponents</groupId>
     <artifactId>httpclient</artifactId>
     <version>4.5.4</version>
     <scope>compile</scope>
 </dependency>

spring-cloud-starter-dubbo基於spring-cloud-starter-commons開發的,org.apache.http.client.HttpClient類確實存在於舊版本的httpclient中,但是它使用內部的org.apache.http.impl.client.HttpClientBuilder類在新版本中卻不存在。如果類路徑上沒有HttpClient,則HttpClientConfiguration自動配置將失敗 HttpClientConfiguration的自動配置因對HttpClient的舊依賴性而失敗,異常如下所示:

java.lang.IllegalStateException: Error processing condition on org.springframework.cloud.commons.httpclient.HttpClientConfiguration$ApacheHttpClientConfiguration.apacheHttpClientBuilder
Caused by: java.lang.ClassNotFoundException: org.apache.http.impl.client.HttpClientBuilder

(2)實現Dubbo接口

@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public String hello(String name) {
        return "hello " + name;
    }
}

註意:@Service註解不是Spring的,而是dubbo的,完全限定名為:org.apache.dubbo.config.annotation.Service

(3)配置Dubbo服務相關的信息

配置說明如下:

spring:
  application:
    name: dubbo-consumer
  cloud:
    nacos:
      discovery:
        server-addr: www.onething.top:8848  # nacos服務註冊中心的地址
        cluster-name: consumer-cluster  #集群名稱
        namespace: b7ef9579-df75-41c2-8a95-e04aa03c273a  #命名空間
      config:
        server-addr: www.onething.top:8848  # nacos配置中心地址
        namespace: b7ef9579-df75-41c2-8a95-e04aa03c273a
        file-extension: yaml  #配置文件的後綴名
        refresh-enabled: true  #默認true:自動刷新
dubbo:
  protocol:    #Dubbo 服務暴露的協議配置,其中子屬性 name 為協議名稱,port 為協議端口( -1 表示自增端口,從 20880 開始)
    name: dubbo
    port: -1  #dubbo協議缺省端口為20880,rmi協議缺省端口為1099,http和hessian協議缺省端口為80;如果沒有配置port,則自動采用默認端口,如果配置為-1,則會分配一個沒有被占用的端口。Dubbo 2.4.0+,分配的端口在協議缺省端口的基礎上增長,確保端口段可控
  registry:
    #dubbo服務註冊端口,註冊中心服務器地址,如果地址沒有端口缺省為9090,同一集群內的多個地址用逗號分隔,如:ip:port,ip:port
    #其中前綴spring-cloud說明:掛載到 Spring Cloud註冊中心
    address: spring-cloud://www.onething.top:8848
    #check: false  #關閉註冊中心是否啟動的相關檢查,false表示不檢查註冊中心是否啟動,就不會報錯
  cloud:
    subscribed-services: dubbo-product
  consumer:
    check: false  #關閉訂閱服務是否啟動的檢查【檢查時,沒有服務提供者會報錯】
  • dubbo.scan.base-packages: 指定 Dubbo 服務實現類的掃描基準包
  • dubbo.protocol: Dubbo 服務暴露的協議配置,其中子屬性 name 為協議名稱,port 為協議端口( -1 表示自增端口,從 20880 開始)
  • dubbo.registry: Dubbo 服務註冊中心配置,其中子屬性 address 的值 “spring-cloud://localhost”,說明掛載到 Spring Cloud 註冊中心

註意:如果使用Spring Boot 2.1及更高版本時候,需要增加配置spring.main.allow-bean-definition-overriding=true

(4)創建應用主類

@EnableDiscoveryClient【此註解可以省略】
@SpringBootApplication
public class DubboServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(DubboServerApplication.class, args);
    }
}

構建服務接口消費方法

(1)創建一個Spring Boot項目,在pom.xml中引入第一步中構建的API包以及Spring Cloud Alibaba對Nacos和Dubbo的依賴,具體內容與服務提供方一致

 <dependencies>
   <dependency>
        <groupId>top.onething</groupId>
        <artifactId>product-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.2.5.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-dubbo</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
</dependencies>

(2)配置Dubbo服務相關的信息

spring:
  application:
    name: dubbo-consumer
  cloud:
    nacos:
      discovery:
        server-addr: www.onething.top:8848  # nacos服務註冊中心的地址
        cluster-name: consumer-cluster  #集群名稱
        namespace: b7ef9579-df75-41c2-8a95-e04aa03c273a  #命名空間
      config:
        server-addr: www.onething.top:8848  # nacos配置中心地址
        namespace: b7ef9579-df75-41c2-8a95-e04aa03c273a
        group: test-group
        file-extension: yaml  #配置文件的後綴名
dubbo:
  protocol:    #Dubbo 服務暴露的協議配置,其中子屬性 name 為協議名稱,port 為協議端口( -1 表示自增端口,從 20880 開始)
    name: dubbo
    port: -1  #dubbo協議缺省端口為20880,rmi協議缺省端口為1099,http和hessian協議缺省端口為80;如果沒有配置port,則自動采用默認端口,如果配置為-1,則會分配一個沒有被占用的端口。Dubbo 2.4.0+,分配的端口在協議缺省端口的基礎上增長,確保端口段可控
  registry:
    #其中前綴spring-cloud說明:掛載到 Spring Cloud註冊中心
    address: spring-cloud://www.onething.top:8848  #dubbo服務註冊端口,註冊中心服務器地址,如果地址沒有端口缺省為9090,同一集群內的多個地址用逗號分隔,如:ip:port,ip:port
  cloud:
    subscribed-services: dubbo-product

(3)創建應用主類,並實現一個接口,在這個接口中調用Dubbo服務

@EnableDiscoveryClient
@SpringBootApplication
@RestController
public class DubboClientApplication {
 @Reference
 HelloService helloService;
   
    public static void main(String[] args) {
        SpringApplication.run(DubboClientApplication.class, args);
    }
    @GetMapping("/test")
    public String test() {
        return helloService.hello("didispace.com");
    }
}

註意:這裡的@Reference註解是org.apache.dubbo.config.annotation.Reference

SpringCloudAlibaba之Dubbo總結

GitHub地址:https://github.com/apache/dubbo

Dubbo的故事比較長瞭,Demo中也有調用的示例,所以示例就不補充瞭,我的gitee倉庫上也有一些可參考的示例。目前在這裡就隻針對目前的最新版本2.7.7,做一點總結把。

另外,關於Dubbo,出來這麼多年瞭,很多人更關註Dubbo的動態代理、底層Netty協議等一些問題。但是我覺得,其實更應該關註的是Dubbo在服務調用這個場景中做到瞭哪些,想到瞭哪些,這些應該是比手寫Dubbo更有價值的地方。這中間的關系就像架構師跟程序員之間的差距,並不是架構師代碼就寫得就比程序員好,而是架構師能考慮到各種情況,組合成系統化解決實際問題的解決方案。

Dubbo概述

官網的特性描述:

  • 面向接口代理的高性能RPC調用:提供高性能的基於代理的遠程調用能力,服務以接口為粒度,為開發者屏蔽遠程調用底層細節。
  • 智能負載均衡:內置多種負載均衡策略,智能感知下遊節點健康狀況,顯著減少調用延遲,提高系統吞吐量。
  • 服務自動註冊與發現:支持多種註冊中心服務,服務實例上下線實時感知。
  • 高度可擴展能力:遵循微內核+插件的設計原則,所有核心能力如Protocol,Transport,Serialization被設計為擴展點,平等對待內置實現和第三方實現。
  • 運行期流量調度:內置條件、腳本等路由策略,通過配置不同的路由規則,輕松實現灰度發佈、同機房優先等功能。
  • 可視化的服務治理與運維:提供豐富服務治理、運維工具,隨時查詢服務元數據,服務健康狀態即調用統計、實時下發路由策略、調整配置參數。

Dubbo架構圖

這個圖可以看到Dubbo的各角色職責,Registery負責服務發現與註冊,Provider往Registry上註冊服務信息,Consumer訂閱Registry上的服務信息,Registry會及時向Consumer進行推送。然後Consumer從服務列表中選擇一個Provider,進行服務調用。

Dubbo配置方式

Dubbo的服務有三種配置方式,API方式、基於Spring的XML方式和基於SpringBoot的Properties方式。三種方式的服務配置在Demo工程中都有實現,就不多說瞭。這裡要記錄一下的,是各種配置的優先級。

三種配置方式之間的關系

顯而易見,API方式其實已經不是服務化的配置方式瞭,是底層的實現方式,展示的是如何將Dubbo的幾個核心對象組裝成服務。正常情況下也是不太會用得到的,隻是在一些脫離Spring的環境中可以偶爾強行用一下,比如MapReduce、spark計算等一些情況可以強行用上一點。

而後兩種配置方式,其實效果是等價的。例如 dubbo.application.name=foo和<dubbo:registry name=“foo” />是等價的,而dubbo.registry.address=nacos://localhost:8848和<dubbo:registry address=”nacos://localhost:8848″這兩個也是等價的。而在多配置的時候,需要用ID進行區分時,dubbo.protocol.rmi.port=1099 equals to <dubbo:protocol id=“rmi” name=“rmi” port=“1099” />這兩種方式也是等價的。而且實際上,在SpringBoot工程中,也是可以通過引入@ImportResource註解,來指定dubbo的服務配置XML的。

而如果在一個SpringBoot工程中,將兩種配置方式整合在一起,那配置的優先級又是怎樣的呢?這時的優先級是{java -D}>{xml}>{properties}。並且,如果一個項目中有多個dubbo.properties(例如多個jar包中都有配置文件),那麼,dubbo會隨機的讀取一個配置文件,同時會拋出異常信息。官方的建議也是多使用xml方式進行配置。

另外,基於SpringCloud的properties配置,其實還有另一種基於註解的服務聲明方式,@Service暴露服務,@Reference調用服務,而這種方式,其實跟xml配置的方式也是等價的。

服務端與消費端的配置優先級

有很多服務屬性如retris(重試次數)、timeout(超時時間)、loadbalance(負載方法)等屬性,在服務提供端和服務消費端都可以進行配置。此時,服務消費端的配置是優先於服務提供端的。服務提供端的配置相當於是服務的默認屬性,因此,官方建議要在服務提供端盡量多的配置這些服務屬性。另外,官方還建議,在服務端盡量提供更詳細的服務配置,例如在配置服務時,如果可以的話,對每個method提供詳細的配置。例如:

<dubbo:protocol threads="200" /> 
<dubbo:service interface="com.alibaba.hello.api.HelloService" version="1.0.0" ref="helloService"
    executes="200" >
    <dubbo:method name="findAllPerson" executes="50" />
</dubbo:service>

這樣指定瞭線程池200,而findAllPerson的線程池為50.

指定dubbo的Cache File

官方建議要在registry中指定file。例如<dubbo:registry file=”${user.home}/output/dubbo.cache” />。這個Cache File可以緩存服務註冊信息,這樣,在註冊中心停止服務後,消費端依然可以進行服務調用。由於這個文件很重要,雖然不指定也會默認生成到本地,但是官方建議是要單獨指定緩存文件,並且註意不要有多個工程指定同一個緩存文件的情況,這樣會造成文件寫入權限競爭以及服務註冊信息被覆蓋的情況。

這個CacheFile機制可以允許消費者繞過註冊中心訪問服務提供者,而如果需要防止這種情況,可以使用dubbo的令牌驗證機制,這樣需要在註冊中心進行權限驗證才可以訪問。開啟方式也很簡單,在provider或者service指定token屬性即可:

<!--隨機token令牌,使用UUID生成-->
<dubbo:provider interface="com.foo.BarService" token="true" />
<!--固定token令牌,相當於密碼-->
<dubbo:provider interface="com.foo.BarService" token="123456" />

其他一些有意思的地方

官方的文檔中很詳細的說明瞭dubbo的許多特性,這裡挑一些有意思的特性。其他可以多參考官方文檔。

服務集群容錯機制

可以在service中指定集群容錯機制,例如 <dubbo:service cluster=“failsafe” />

  • 默認是Failover,這種模式當訪問出錯時(超時等),會自動尋找另外的提供者,發起重試。可以通過指定retries來指定重試次數–retries配置的次數不包含第一次請求。這通常用於讀操作,對於寫操作,容易造成服務冪等問題。
  • Failfast:隻發起一次服務調用,如果出錯就立即報錯。這種比較適用於寫操作。
  • Failsafe:如果服務調用出錯或者出現異常,則直接忽略。這種比較適用於寫日志這類允許失敗的操作。
  • Failback:故障自動恢復,後臺記錄失敗的請求,並定期重傳。這種比較適用於消息驅動的場景。
  • Forking: 同時向多個服務端發起請求,一旦有一個服務提供端返回成功,則服務調用成功。這種比較適用於實時性要求較高的讀操作。但是顯然,這樣會浪費更多的服務資源。可以使用fork屬性配置並行數。
  • BroadCast: 同時向所有服務提供端發起請求,依次請求,並會報告每一個錯誤。這通常用於通知所有服務端進行緩存更新之類的操作。

本地服務存根與本地偽裝 local stub, local mock

1、通常情況下,服務消費端隻有一個接口,對服務的調用實現全部依賴服務提供端。但是,有時候,服務提供端希望在服務消費端也做一些本地操作,例如做ThreadLocal緩存、參數檢查、調用失敗後返回Mock假數據等。這樣,有些在SpringCloud中需要在消費端用Hystrix做的事情,就可以用這個本地服務存根,在服務端直接幫消費端處理完瞭,這樣可以讓消費端更進一步忽略服務調用細節。例如,在服務提供端做如下配置:

<dubbo:service interface="com.foo.BarService" stub="true" /> or
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />

然後服務提供端就可以通過提供一個本地存根方法,這樣BarServiceStub的sayHello方法會在消費端的本地執行,讓消費端在服務調用失敗後返回容錯數據。

public class BarServiceStub implements BarService {
    private final BarService barService;
    // 構造函數傳入真正的遠程代理對象
    public BarServiceStub(BarService barService){
        this.barService = barService;
    }
    public String sayHello(String name) {
        // 此代碼在客戶端執行, 你可以在客戶端做ThreadLocal本地緩存,或預先驗證參數是否合法,等等
        try {
            return barService.sayHello(name);
        } catch (Exception e) {
            // 你可以容錯,可以做任何AOP攔截事項
            return "容錯數據";
        }
    }
}

2、針對Mock這種容錯場景,dubbo可以使用mock屬性進行更簡單的配置;

<dubbo:reference interface="com.foo.BarService" mock="true" /> or
<dubbo:reference interface="com.foo.BarService" mock="com.foo.BarServiceMock" />

這樣,在barServiceMock中,就不用try cache,而隻用關註cache後的處理。

public class BarServiceMock implements BarService {
    public String sayHello(String name) {
        // 你可以偽造容錯數據,此方法隻在出現RpcException時被執行
        return "容錯數據";
    }
}

這樣,服務消費者可以省掉很多不必要的try cache操作。

3、在mock屬性中,可以直接返回一些指定的對象,從而連實現代碼都可以省略調瞭。例如:

<dubbo:reference interface="com.foo.BarService" mock="return null" />

mock屬性可以返回的對象有:

  • empty:空對象,代表基本類型的默認值,或者空集合等。
  • null: java中的null對象
  • boolean: true 或者 false
  • JSON 格式: 一個對象的JSON反序列化對象。

還有, mock屬性中,還可以簡單的拋出異常

<dubbo:reference interface="com.foo.BarService" mock="throw" /> //拋出一個RPCException異常
<dubbo:reference interface="com.foo.BarService" mock="throw com.foo.MockException" /> //拋出一個指定異常

4、這個版本的dubbo中,mock屬性還支持force和fail的mock行為。

fail跟默認流程一致,即表示當消費端遠程調用出現異常時才進行。

而force則表示不走遠程調用,直接返回mock數據。這在調試時還是有點用的。

force和fail都支持與上面的return或者throw進行組合。、

<dubbo:reference interface="com.foo.BarService" mock="force:return fake" /> or
<dubbo:reference interface="com.foo.BarService" mock="force:throw com.foo.MockException" />

5、這個local mock,在處理服務調用異常時,是挺強大的。官方建議,盡量在方法級別上使用mock,而不要在整個服務上使用。像這樣,隻在sayHello方法上使用mock.

<dubbo:reference id="demoService" check="false" interface="com.foo.BarService">
    <dubbo:parameter key="sayHello.mock" value="force:return fake"/>
</dubbo:reference>

6、註意下,stub和mock在服務的提供端和消費端都可以指定,應該還是消費端的優先級高於提供端。

泛化服務 GenericService

Dubbo的服務總是面向接口的,意味著服務調用總是需要一個固定的接口對象,這樣很安全,但是同時也不夠靈活。而Dubbo可以通過GenericService泛華服務來增加服務靈活性。

1、服務提供端:

在服務提供端,可以用org.apache.dubbo.rpc.service.GenericService替代所有的接口實現。 例如,先按如下方式提供一個服務:

<bean id="genericService" class="com.foo.MyGenericService" />
<dubbo:service interface="com.foo.BarService" ref="genericService" />

而指定的MyGenericService,可以通過繼承GenericService來實現擴展邏輯:

public class MyGenericService implements GenericService {
    @Override
    public Object $invoke(String methodName, String[] parameterTypes, Object[] args) throws GenericException {
        if ("sayHello".equals(methodName)) {
            return "Welcome " + args[0];
        }
    }
}

這樣就可以在不需要接口的情況下,響應服務端的 barService.sayHello(String args)這樣的服務請求瞭。

這種泛化服務方式同樣支持API方式的服務暴露,跟xml的配置基本是對等的。

2、泛化服務調用

泛化服務調用,隻需要在reference中指定generic為true就行。

<dubbo:reference id="barService" interface="com.foo.BarService" generic="true" />

泛化服務沒有瞭預先定義好的方法,需要使用GenericService的$invoke方法來進行調用

GenericService barService = (GenericService) applicationContext.getBean("barService");
Object result = barService.$invoke("sayHello", new String[] { "java.lang.String" }, new Object[] { "World" });

這種調用方式同樣支持API格式的服務調用。

泛化方法的返回對象,如果是個POJO的對象,則會以Map結構返回POJO的信息,類似於下面的結構

Map<String, Object> map = new HashMap<String, Object>(); 
// 註意:如果參數類型是接口,或者List等丟失泛型,可通過class屬性指定類型。
map.put("class", "com.xxx.PersonImpl"); 
map.put("name", "xxx");  // Person的屬性
map.put("password", "yyy");

服務管理控制臺

Dubbo使用Nacos作為註冊中心的話,Nacos已經可以充當服務控制臺。

而另外有一個項目則可替代原有的Dubbo Admin作為Dubbo的服務管理控制臺。github地址:https://github.com/apache/dubbo-admin 一個基於Vue.js和Verify+SpringBoot做的前後端分離的管理控制臺。

國人的官網看著舒適,示例豐富,說明也很簡單詳盡,目前版本的Dubbo還有些如分佈式事務之類的功能尚未實現,期待Dubbo更強大把。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: