Dubbo的高階特性:服務管控篇

2023-07-19 12:01:16

王有志,一個分享硬核Java技術的互金摸魚俠
加入Java人的提桶跑路群:共同富裕的Java人

上一篇,我們已經介紹了 DUbbo 在服務治理方面提供的特性,今天我們一起來看看 Dubbo 在其它方面提供的特性。同服務治理篇一樣,本文的目的在於學會使用 Dubbo 在服務管控方面提供的特性,依舊不涉及任何實現原理。

工程結構

嗯~~

是這樣的,因為電腦過於拉胯,而且 IDEA 著實有些吃記憶體了,所以我將測試工程按照子專案合併到一起了,目前我使用的工程結構是這樣的:

子模組名由兩部分組成:設定方式+功能,如: XMLProvider ,表示以 XML 設定方式為主的服務提供方。

Tips:IDEA 快要追上「記憶體雄獅」 CLion 了。

本地存根(Stub)

使用 Dubbo 時,服務使用方只整合了介面,所有的實現全都在服務提供方,但部分場景中,我們希望服務使用方完成一些邏輯的處理,以此來減少 RPC 互動帶來的效能消耗,例如:將引數校驗放在服務使用方去做,減少一次與服務呼叫方的網路互動。

這種場景中,我們可以使用 Dubbo 提供的本地存根特性。我們有如下的服務提供方的工程結構:

xml-provider-api 模組中定義了對外提供服務的介面 XMLProviderService ,程式碼如下:

public interface XMLProviderService {
  String say(String message);
}

以及介面存根 XMLProviderServiceStub,程式碼如下:

public class XMLProviderServiceStub implements XMLProviderService {
  
  private final XMLProviderService xmlProviderService;
  
  public XMLProviderServiceStub(XMLProviderService xmlProviderService) {
    this.xmlProviderService = xmlProviderService;
  }
  
  @Override
  public String say(String message) {
    if (StringUtils.isBlank(message)) {
      return "message不能為空!";
    }
    
    try {
      return this.xmlProviderService.say(message);
    } catch (Exception e) {
      return "遠端呼叫失敗:" + e.getMessage();
    }
  }
}

接著我們在服務使用方的工程中設定介面存根:

<dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" stub="com.wyz.api.stub.XMLProviderServiceStub"/>

Tips:使用本地存根,要求存根的實現類必須有傳入 Proxy 範例(服務使用方生成的 Proxy 範例)的建構函式。

本地偽裝(Mock)

本地偽裝即我們在《Dubbo的高階特性:服務治理篇》中提到的服務降級,我們今天再稍微做一個補充。本地偽裝是本地存根的一個子集,本地存根可以處理 RPC 呼叫環節中各種各樣的錯誤和異常,而本地偽裝則專注於處理 RpcException (如網路失敗,響應超時等)這種需要容錯處理的異常。

我們為 XMLProviderService 新增一個本地偽裝服務 XMLProviderServiceMock,工程結構如下:

XMLProviderServiceMock 的程式碼如下:

public class XMLProviderServiceMock implements XMLProviderService {
  
  @Override
  public String say(String message) {
    return "服務出錯了!";
  }
}

組態檔可以按如下方式設定:

<dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" mock="true"/>

這種設定中,要求 Mock 的實現必須按照「介面名+Mock 字尾」的方式進行命名;如果不想使用這種命名方式,可以使用全限名:

<dubbo:reference id="xmlProviderService" interface="com.wyz.api.XMLProviderService" mock="com.wyz.api.mock.XMLProviderServiceMock"/>

Tips:再「重複」一遍 Mock 的原因是,上一篇中出了一點錯誤,本應在<dubbo:reference>標籤中做的設定,我寫到了<dubbo:service>標籤中,產生錯誤的原因還是沒有動手在專案中歇一歇,哎,真應了那句「紙上得來終覺淺,絕知此事要躬行」。

引數回撥

Dubbo 支援引數回撥功能,使服務提供方可以「反向」呼叫服務使用方,該功能是基於長連結生成的反向代理實現的,效果類似於非同步呼叫。我們舉個支付的例子:

XMLProvider 工程的 xml-provider-api 模組中新增 PaymentService 介面,同時新增 PaymentNotifyService 用於通知PaymentService 的結果:

public interface PaymentService {
  void payment(String cardNo, PaymentNotifyService paymentNotifyService);
}

public interface PaymentNotifyService {
  void paymentNotify(String message);
}

XMLProvider 工程的 xml-provider-service 模組中實現 PaymentService 介面:

public class PaymentServiceImpl implements PaymentService {
  @Override
  public void payment(String cardNo, PaymentNotifyService paymentNotifyService) {
    System.out.println("向卡號[" + cardNo + "]付錢!");
    // 業務邏輯
    paymentNotifyService.paymentNotify("付款成功");
  }
}

執行PaymentService#payment方法,並呼叫PaymentNotifyService#paymentNotify方法通知服務呼叫方執行結果。

XMLConsumer 工程中實現 PaymentNotifyService 介面:

public class PaymentNotifyServiceImpl implements PaymentNotifyService {
  @Override
  public void paymentNotify(String message) {
    System.out.println("支付結果:" + message);
  }
}

來看一下此時的工程結構:

接下來是 XML 的設定,引數回撥中,我們需要關注的是服務提供方 XMLProvider 工程的 xml-provider-service 模組的設定:

<bean id="paymentServiceImpl" class="com.wyz.service.impl.PaymentServiceImpl"/>
<dubbo:service interface="com.wyz.api.PaymentService" ref="paymentServiceImpl" callbacks="10">
  <dubbo:method name="payment">
    <dubbo:argument index="1" callback="true"/>
  </dubbo:method>
</dubbo:service>

設定通過第 4 行的<dubbo:argument index="1" callback="true"/>來確定 PaymentService#payment 方法中第 2 個(index 從 0 開始)引數是回撥引數 ;callbacks限制了同一個長連結下回撥的次數,而不是總共回撥的次數。

Tips:在實際的支付業務場景中,更傾向於非同步處理,比如服務提供方在接收到時,啟動新執行緒處理支付業務並呼叫通知介面,主執行緒返回成功接收支付請求。

非同步呼叫

非同步呼叫允許服務提供方立即返回響應,同時後臺繼續執行請求處理,當服務使用方請求響應結果時,服務提供方將結果返回。

DUbbo 支援兩種非同步呼叫方式:

  • 使用 CompletableFuture 介面

  • 使用 RpcContext

DUbbo 2.7 之後,DUbbo 以CompletableFuture 介面為非同步程式設計的基礎。

使用 CompletableFuture 實現非同步呼叫

我們先來看如何使用 CompletableFuture 實現非同步呼叫,宣告 CompletableFutureAsyncService 介面:

public interface CompletableFutureAsyncService {
  CompletableFuture<String> async(String message);
}

接著是介面實現:

public class CompletableFutureAsyncServiceImpl implements CompletableFutureAsyncService {
  @Override
  public CompletableFuture<String> async(String message) {
    return CompletableFuture.supplyAsync(() -> {
      System.out.println(Thread.currentThread().getName() + " say : " + message);
      
      try {
        TimeUnit.SECONDS.sleep(10);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      return "非同步呼叫成功!";
    });
  }
}

XML 設定與普通的 Dubbo RPC 介面設定相同,xml-provider-service 模組的設定:

<bean id="completableFutureAsyncServiceImpl" class="com.wyz.service.impl.CompletableFutureAsyncServiceImpl" />
<dubbo:service interface="com.wyz.api.CompletableFutureAsyncService" ref="completableFutureAsyncServiceImpl" />

XMLConsumer 模組的設定:

<dubbo:reference id="completableFutureAsyncService" interface="com.wyz.api.CompletableFutureAsyncService"/>

使用方式也非常簡單:

CompletableFuture<String> completableFuture = completableFutureAsyncService.async("Hello");
System.out.println(completableFuture.get());

Tips

  • Dubbo 中使用 CompletableFuture 與單獨使用 CompletableFuture 並無什麼差異~~

  • CompletableFutureAsyncServiceImpl 的實現中列印介面名稱的目的是為了清晰的展示出非同步呼叫的效果;

  • CompletableFuture#supplyAsync(Supplier<U> supplier)預設使用ForkJoinPool#commonPool()

  • 過載方法CompletableFuture#supplyAsync(Supplier<U> supplier, Executor executor)允許使用自定義執行緒池。

使用 AsyncContext 實現非同步呼叫

除了使用 CompletableFuture 外,還可以通過 Dubbo 定義的 AsyncContext 實現非同步呼叫。先來編寫介面和介面實現:

public interface RpcContextAsyncService {
  String async(String message);
}

public class RpcContextAsyncServiceImpl implements RpcContextAsyncService {
  
  @Override
  public String async(String message) {
    final AsyncContext asyncContext = RpcContext.startAsync();
    new Thread(() -> {
      asyncContext.signalContextSwitch();
      asyncContext.write(Thread.currentThread().getName() + " say : " + message);
    }).start();
    // 非同步呼叫中,這個返回值完全沒有意義
    return null;
  }
}

服務提供方的設定與其它 Dubbo 介面的設定並無不同:

<bean id="rpcContextAsyncServiceImpl" class="com.wyz.service.impl.RpcContextAsyncServiceImpl"/>
<dubbo:service interface="com.wyz.api.RpcContextAsyncService" ref="rpcContextAsyncServiceImpl"/>

接著是服務使用方的設定,需要新增 async 引數:

<dubbo:reference id="rpcContextAsyncService" interface="com.wyz.api.RpcContextAsyncService" async="true"/>

最後是在服務使用方中呼叫 RPC 介面:

rpcContextAsyncService.async("Thanks");

Future<String> future = RpcContext.getServiceContext().getFuture();
System.out.println(future.get());

泛化呼叫

Dubbo 的泛化呼叫提供了一種不依賴服務提供方 API (SDK)的而呼叫服務的實現方式。主要場景在於閘道器平臺的實現,通常閘道器的實現不應該依賴於其他服務的 API(SDK)。

Dubbo 官方提供了 3 種泛化呼叫的方式:

  • 通過API使用泛化呼叫
  • 通過 Spring 使用泛化呼叫(XML 形式)
  • Protobuf 物件泛化呼叫

這裡我們介紹以 XML 的形式設定泛化呼叫的方式。

準備工作

首先我們再準備一個服務提供的工程 GenericProvider,工程結構如下:

工程中定義了介面即實現類 GenericProviderService 和 GenericProviderServiceImpl,程式碼如下:

public interface GenericProviderService {
  String say(String message);
}

public class GenericProviderServiceImpl implements GenericProviderService {
  @Override
  public String say(String message) {
    return "GenericProvider say:" + message;
  }
}

generic-dubbo-provider.xml 中只需要正常設定 GenericProvider 提供的服務即可:

<bean id="genericProviderServiceImpl" class="com.wyz.service.impl.GenericProviderServiceImpl"/>
<dubbo:service interface="com.wyz.service.api.GenericProviderService" ref="genericProviderServiceImpl" generic="true"/>

application.yml 檔案的設定我們就不多贅述了。

服務使用方的設定

回到 XMLConsumer 工程中,先設定 Dubbo 服務參照,xml-dubbo-consumer.xml 中新增如下內容:

<dubbo:reference id="genericProviderService" generic="true" interface="com.wyz.service.api.GenericProviderService"/>

引數 generic 宣告這是一個泛化呼叫的服務。此時 IDEA 會將interface="com.wyz.service.api.GenericProviderService"標紅,提示「Cannot resolve class 'GenericProviderService' 」,這個我們不需要關注,因為com.wyz.service.api包下確實不存在 GenericProviderService 介面。

接著我們來使用 GenericProviderService 介面:

ApplicationContext context = SpringContextUtils.getApplicationContext();
// genericProviderService是XML中定義的服務id
GenericService genericService = (GenericService) context.getBean("genericProviderService");

// $invoke的3個引數分別為:方法名,引數型別,引數
Object result = genericService.$invoke("say", new String[]{"java.lang.String"}, new Object[]{"wyz"});
System.out.println(result);

這樣,我們就可以通過 ApplicationContext 獲取到 GenericProviderService 介面提供的服務了。

Tips:SpringContextUtils 用於獲取 ApplicationContext,程式碼如下:

@Component
public class SpringContextUtils implements ApplicationContextAware {
  private static ApplicationContext applicationContext = null;
  
  public static ApplicationContext getApplicationContext() {
    return SpringContextUtils.applicationContext;
  }
  
  @Override
  public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
    SpringContextUtils.applicationContext = applicationContext;
  }
}

結語

好了,到目前為止,我們已經一起認識並學習了 Dubbo 中常用特性的設定與使用,當然了,經歷了多年的發展,Dubbo 的提供的特性遠不止於此,如果想要了解更多內容,可以檢視阿里巴巴提供的檔案《Apache Dubbo微服務架構從入門到精通》。

下一篇,我們從服務註冊部分正式開啟對 Dubbo 實現原理的探索。


如果本文對你有幫助的話,還請多多點贊支援。如果文章中出現任何錯誤,還請批評指正。最後歡迎大家關注分享硬核 Java 技術的金融摸魚俠王有志,我們下次再見!