螞蟻金服SOFA-Boot整合SOFA-RPC(中篇)

零壹技術棧發表於2018-08-30

前言

上篇文章簡單地介紹了 SOFA-Boot 的功能特性,對 Readiness 健康檢查的配置舉例說明。重點介紹瞭如何在 SOFA-Boot 中引入 SOFA-RPC 中介軟體,給出了基於 bolt、rest 和 dubbo 等不同協議通道的服務釋出與消費的全流程。

螞蟻金服SOFA-Boot整合SOFA-RPC(中篇)

本文將進一步介紹 SOFA-RPC 中介軟體提供的豐富而強大的功能,包括單向呼叫、同步呼叫、Future呼叫、回撥,泛化呼叫,過濾器配置等。

其他文章

正文

1. 呼叫方式

SOFA-RPC 提供單向呼叫、同步呼叫、非同步呼叫和回撥四種呼叫機制。為了區分四者的不同之處,這裡給出 SOFA 官方提供的原理圖。

螞蟻金服SOFA-Boot整合SOFA-RPC(中篇)

下面給出詳細闡述和配置說明:

1.1. 單向方式

當前執行緒發起呼叫後,不關心呼叫結果,不做超時控制,只要請求已經發出,就完成本次呼叫。目前支援 bolt 協議。

配置說明

使用單向方式需要在服務引用的時候通過 sofa:global-attrs 元素的 type 屬性宣告呼叫方式為 oneway ,這樣使用該服務引用發起呼叫時就是使用的單向方式了。

<sofa:reference id="helloOneWayServiceReference" interface="com.ostenant.sofa.rpc.example.invoke.HelloOneWayService">
    <sofa:binding.bolt>
        <sofa:global-attrs type="oneway"/>
    </sofa:binding.bolt>
</sofa:reference>
複製程式碼

適用場景

單向呼叫不保證成功,而且發起方無法知道呼叫結果。因此通常用於可以重試,或者定時通知類的場景,呼叫過程是有可能因為網路問題,機器故障等原因,導致請求失敗。業務場景需要能接受這樣的異常場景,才可以使用。

1.2. 同步方式

當前執行緒發起呼叫後,需要在指定的超時時間內,等到響應結果,才能完成本次呼叫。如果超時時間內沒有得到結果,那麼會丟擲超時異常。

配置說明

服務介面與實現類

SOFA-RPC 預設採用的就是同步呼叫,可以省略 sofa:global-attrs 配置項。

服務端釋出配置

<bean id="helloSyncServiceImpl" class="com.ostenant.sofa.rpc.example.invoke.HelloSyncServiceImpl"/>
<sofa:service ref="helloSyncServiceImpl" interface="com.ostenant.sofa.rpc.example.invoke.HelloSyncService">
    <sofa:binding.bolt/>
</sofa:service>
複製程式碼

客戶端引用配置

<sofa:reference id="helloSyncServiceReference" interface="com.ostenant.sofa.rpc.example.invoke.HelloSyncService">
    <sofa:binding.bolt/>
</sofa:reference>
複製程式碼

服務端啟動入口

SpringApplication springApplication = new SpringApplication(SyncServerApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端啟動入口

SpringApplication springApplication = new SpringApplication(SyncClientApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端呼叫

HelloSyncService helloSyncServiceReference = (HelloSyncService) applicationContext.getBean("helloSyncServiceReference");
System.out.println(helloSyncServiceReference.saySync("sync"));
複製程式碼

適用場景

同步呼叫是最常用的方式。注意要根據對端的處理能力,合理設定超時時間。

1.3. Future方式

Future 方式下,客戶端發起呼叫後不會等待服務端的結果,繼續執行後面的業務邏輯。服務端返回的結果會被 SOFA-RPC 快取,當客戶端需要結果的時候,需要主動獲取。目前支援 bolt 協議。

配置說明

服務介面和實現類

HelloFutureService.java

public interface HelloFutureService {
    String sayFuture(String future);
}
複製程式碼

HelloFutureServiceImpl.java

public class HelloFutureServiceImpl implements HelloFutureService {
    @Override
    public String sayFuture(String future) {
        return future;
    }
}
複製程式碼

服務端釋出配置

<bean id="helloFutureServiceImpl" class="com.ostenant.sofa.rpc.example.invoke.HelloFutureServiceImpl"/>
<sofa:service ref="helloFutureServiceImpl" interface="com.ostenant.sofa.rpc.example.invoke.HelloFutureService">
    <sofa:binding.bolt/>
</sofa:service>
複製程式碼

客戶端引用配置

使用 Future 方式需要在服務引用的時候通過 sofa:global-attrs 元素的 type 屬性宣告呼叫方式為 future

<sofa:reference id="helloFutureServiceReference" interface="com.ostenant.sofa.rpc.example.invoke.HelloFutureService">
    <sofa:binding.bolt>
        <sofa:global-attrs type="future"/>
    </sofa:binding.bolt>
</sofa:reference>
複製程式碼

這樣使用該服務引用發起呼叫時就是使用的 Future 方式了。

服務端啟動入口

SpringApplication springApplication = new SpringApplication(FutureServerApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端啟動入口

SpringApplication springApplication = new SpringApplication(FutureClientApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端獲取返回結果有兩種方式:

  • 其一,通過 SofaResponseFuture 直接獲取結果。第一個引數是獲取結果的超時時間,第二個參數列示是否清除執行緒上下文中的結果。
HelloFutureService helloFutureServiceReference = (HelloFutureService) applicationContext
    .getBean("helloFutureServiceReference");
helloFutureServiceReference.sayFuture("future");

try {
    String result = (String)SofaResponseFuture.getResponse(1000, true);
    System.out.println("Future result: " + result)
} catch (InterruptedException e) {
    e.printStackTrace();
}
複製程式碼
  • 其二,獲取原生 Future。該種方式會獲取 JDK 原生的 Future ,參數列示是否清除執行緒上下文中的結果。獲取結果的方式就是 JDK Future 的獲取方式。
HelloFutureService helloFutureServiceReference = (HelloFutureService) applicationContext
    .getBean("helloFutureServiceReference");
helloFutureServiceReference.sayFuture("future");

try {
    Future future = SofaResponseFuture.getFuture(true);
    String result = (String)future.get(1000, TimeUnit.MILLISECONDS);
    System.out.println("Future result: " + result)
} catch (InterruptedException e) {
    e.printStackTrace();
}
複製程式碼

適用場景

Future 方式適用於非阻塞程式設計模式。對於客戶端程式處理後,不需要立即獲取返回結果,可以先完成後續程式程式碼執行,在後續業務中,主動從當前執行緒上下文獲取呼叫返回結果。減少了網路 IO 等待造成的程式碼執行阻塞和延遲。

1.4. 回撥方式

當前執行緒發起呼叫,則本次呼叫馬上結束,可以馬上執行下一次呼叫。發起呼叫時需要註冊一個回撥,該回撥需要分配一個非同步執行緒池。待響應返回後,會在回撥的非同步執行緒池,來執行回撥邏輯。

配置說明

服務介面和實現類

HelloCallbackService.java

public interface HelloCallbackService {
    String sayCallback(String callback);
}
複製程式碼

HelloCallbackServiceImpl.java

public class HelloCallbackServiceImpl implements HelloCallbackService {
    @Override
    public String sayCallback(String string) {
        return string;
    }
}
複製程式碼

業務回撥類

客戶端回撥類需要實現 com.alipay.sofa.rpc.core.invoke.SofaResponseCallback 介面。

CallbackImpl.java

public class CallbackImpl implements SofaResponseCallback {
    @Override
    public void onAppResponse(Object appResponse, String methodName, RequestBase request) {
        System.out.println("callback client process:" + appResponse);
    }

    @Override
    public void onAppException(Throwable throwable, String methodName, RequestBase request) {
    }

    @Override
    public void onSofaException(SofaRpcException sofaException, String methodName, RequestBase request) {
    }
}
複製程式碼

SofaResponseCallback 介面提供了 3 個方法:

  • onAppResponse: 程式正常執行,則進入該回撥方法。
  • onAppException: 服務端程式丟擲異常,則進入該回撥方法。
  • onSofaException: 框架內部出現錯誤,則進入該回撥方法。

服務端釋出配置

<bean id="helloCallbackServiceImpl" class="helloFutureServiceReference" interface="com.ostenant.sofa.rpc.example.invoke.HelloCallbackServiceImpl"/>
<sofa:service ref="helloCallbackServiceImpl" interface="helloFutureServiceReference" interface="com.ostenant.sofa.rpc.example.invoke.HelloCallbackService">
    <sofa:binding.bolt/>
</sofa:service>
複製程式碼

客戶端引用配置

在服務引用的時候通過 sofa:global-attrs 元素的 type 屬性宣告呼叫方式為 callback ,再通過 callback-ref 宣告回撥的實現類。

<bean id="callbackImpl" class="com.ostenant.sofa.rpc.example.invoke.CallbackImpl"/>
<sofa:reference id="helloCallbackServiceReference"
                interface="com.ostenant.sofa.rpc.example.invoke.HelloCallbackService">
    <sofa:binding.bolt>
        <sofa:global-attrs type="callback" callback-ref="callbackImpl"/>
    </sofa:binding.bolt>
</sofa:reference>
複製程式碼

這樣使用該服務引用發起呼叫時,就是使用的回撥方式了。在結果返回時,由 SOFA-RPC 自動呼叫該回撥類的相應方法。

服務端啟動入口

SpringApplication springApplication = new SpringApplication(CallbackServerApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端啟動入口

SpringApplication springApplication = new SpringApplication(CallbackClientApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端發起呼叫

HelloCallbackService helloCallbackServiceReference = (HelloCallbackService) applicationContext
            .getBean("helloCallbackServiceReference");
helloCallbackServiceReference.sayCallback("callback");

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
複製程式碼

sayCallback() 的返回值不應該直接獲取。在客戶端註冊的回撥類中,返回值會以引數的形式傳入正確的方法,以回撥的形式完成後續邏輯處理。

適用場景

Callback 方式適用於非同步非阻塞程式設計模式。客戶端程式所線上程發起呼叫後,繼續執行後續操作,不需要主動去獲取返回值。服務端程式處理完成,將返回值傳回一個非同步執行緒池,由子執行緒通過回撥函式進行返回值處理。很大情況的減少了網路 IO 阻塞,解決了單執行緒的瓶頸,實現了非同步程式設計。

2. 泛化呼叫

泛化呼叫方式能夠在客戶端不依賴服務端的介面情況下發起呼叫,目前支援 bolt 協議。由於不知道服務端的介面,因此需要通過字串的方式將服務端的介面,呼叫的方法,引數及結果類進行描述。

配置說明

泛化引數類

SampleGenericParamModel.java

public class SampleGenericParamModel {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
複製程式碼

泛化返回類

SampleGenericResultModel.java

public class SampleGenericResultModel {
    private String name;
    private String value;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}
複製程式碼

服務介面和實現類

SampleGenericService.java

public interface SampleGenericService {
    SampleGenericResultModel sayGeneric(SampleGenericParamModel sampleGenericParamModel);
}
複製程式碼
  • SampleGenericParamModel:作為 sayGeneric() 的輸入引數型別,有一個 name 成員變數,作為真正的方法入參。

  • SampleGenericResultModel:作為 sayGeneric() 的返回結果型別,宣告瞭 namevalue 兩個成員變數,作為真實的返回值。

SampleGenericServiceImpl.java

public class SampleGenericServiceImpl implements SampleGenericService {
    @Override
    public SampleGenericResultModel sayGeneric(SampleGenericParamModel sampleGenericParamModel) {
        String name = sampleGenericParamModel.getName();
        SampleGenericResultModel resultModel = new SampleGenericResultModel();
        resultModel.setName(name);
        resultModel.setValue("sample generic value");
        return resultModel;
    }
}
複製程式碼

服務端釋出配置

<bean id="sampleGenericServiceImpl" class="com.ostenant.sofa.rpc.example.generic.SampleGenericServiceImpl"/>
<sofa:service ref="sampleGenericServiceImpl" interface="com.ostenant.sofa.rpc.example.generic.SampleGenericService">
    <sofa:binding.bolt/>
</sofa:service>
複製程式碼

客戶端引用配置

<sofa:reference id="sampleGenericServiceReference" interface="com.alipay.sofa.rpc.api.GenericService">
    <sofa:binding.bolt>
        <sofa:global-attrs generic-interface="com.ostenant.sofa.rpc.example.generic.SampleGenericService"/>
    </sofa:binding.bolt>
</sofa:reference>
複製程式碼

在泛化呼叫過程中,客戶端配置有兩點需要注意:

  • sofa:reference 指向的服務介面需要宣告為 SOFA-RPC 提供的泛化介面 com.alipay.sofa.rpc.api.GenericService
  • sofa:global-attrs 需要宣告屬性 generic-interface,value 為真實的服務介面名稱。

服務端啟動入口

SpringApplication springApplication = new SpringApplication(SampleGenericServerApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端啟動入口

SpringApplication springApplication = new SpringApplication(SampleGenericClientApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端發起呼叫

  • 獲取服務的泛化引用
GenericService sampleGenericServiceReference = (GenericService) applicationContext
    .getBean("sampleGenericServiceReference");
複製程式碼
  • 準備方法引數

由於客戶端沒有呼叫服務的引數類,因此通過 com.alipay.hessian.generic.model.GenericObjectGenericObject 進行描述。

// 準備方法引數
GenericObject genericParam = new GenericObject(
    "com.ostenant.sofa.rpc.example.generic.SampleGenericParamModel");
genericParam.putField("name", "Harrison");
複製程式碼

GenericObject 持有一個 Map<String, Object> 型別的變數,你能夠通過 GenericObject 提供的 putField() 方法,將引數類的屬性和值放到這個 Map 中,以此來描述引數類。

  • 發起泛化呼叫

通過 GenericService$genericInvoke(arg1, agr2, arg3) 方法可以發起服務的泛化呼叫,各個引數含義如下:

引數 含義 引數可選
arg1 目標方法名稱 必填
arg2 引數型別的陣列,要求嚴格遵循先後次序 必填
arg3 引數值的陣列,要求與引數型別陣列保持一致 必填
arg4 返回值的Class型別 可選

方式一:

GenericObject genericResult = (GenericObject) sampleGenericServiceReference.$genericInvoke(
    // 目標方法名稱
    "sayGeneric",
    // 引數型別名稱
    new String[] { "com.ostenant.sofa.rpc.example.generic.SampleGenericParamModel" },
    // 引數的值
    new Object[] { genericParam });

// 驗證返回結果
System.out.println("Type: " + genericResult.getType());
System.out.println("Name: " + genericResult.getField("name"));
System.out.println("Value: " + genericResult.getField("value"));
複製程式碼

方式二:

SampleGenericResultModel sampleGenericResult = sampleGenericServiceReference.$genericInvoke(
    // 目標方法名稱
    "sayGeneric",
    // 引數型別名稱
    new String[] { "com.ostenant.sofa.rpc.example.generic.SampleGenericParamModel" },
    // 引數的值
    new Object[] { genericParam },
    // 返回值的Class型別
    SampleGenericResultModel.class);

// 驗證返回結果
System.out.println("Type: " + sampleGenericResult.getClass().getName());
System.out.println("Name: " + sampleGenericResult.getName());
System.out.println("Value: " + sampleGenericResult.getValue());
複製程式碼

檢視控制檯輸出

兩種方式輸出如下:

Type: com.ostenant.sofa.rpc.example.generic.SampleGenericResultModel
Name: Harrison
Value: sample generic value
複製程式碼

3. 過濾器配置

SOFA-RPC 通過過濾器 Filter 來實現對請求和響應的攔截處理。使用者可以自定義 Filter 實現攔截擴充套件,目前支援 bolt 協議。開發人員通過繼承 com.alipay.sofa.rpc.filter.Filter 實現過濾器的自定義。

配置說明

服務介面與實現類

FilterService.java

public interface FilterService {
    String sayFilter(String filter);
}
複製程式碼

FilterServiceImpl.java

public class FilterServiceImpl implements FilterService {
    @Override
    public String sayFilter(String filter) {
        return filters;
    }
}
複製程式碼

服務端過濾器

在 Filter 實現類中,invoke() 方法實現具體的攔截邏輯,通過 FilterInvoker.invoke(SofaRequest) 觸發服務的呼叫,在該方法前後可以實現具體的攔截處理。

public class SampleServerFilter extends Filter {
    @Override
    public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException {
        System.out.println("SampleFilter before server process");
        try {
            return invoker.invoke(request);
        } finally {
            System.out.println("SampleFilter after server process");
        }
    }
}
複製程式碼

服務端釋出配置

服務端需要配置服務實現類、過濾器,然後在 sofa:servicesofa:global-attrs 標籤配置 filter 屬性,實現兩者的繫結。

<bean id="sampleFilter" class="com.ostenant.sofa.rpc.example.filter.SampleServerFilter"/>
<bean id="filterService" class="com.ostenant.sofa.rpc.example.filter.FilterServiceImpl"/>
<sofa:service ref="filterService" interface="com.ostenant.sofa.rpc.example.filter.FilterService">
    <sofa:binding.bolt>
        <sofa:global-attrs filter="sampleFilter"/>
    </sofa:binding.bolt>
</sofa:service>
複製程式碼

客戶端過濾器

public class SampleClientFilter extends Filter {
    @Override
    public SofaResponse invoke(FilterInvoker invoker, SofaRequest request) throws SofaRpcException {
        System.out.println("SampleFilter before client invoke");
        try {
            return invoker.invoke(request);
        } finally {
            System.out.println("SampleFilter after client invoke");
        }
    }
}
複製程式碼

客戶端引用配置

同樣的,客戶端過濾器需要在 sofa:referencesofa:global-attrs 標籤中配置 filter 屬性,實現客戶端引用類的呼叫攔截。

<bean id="sampleFilter" class="com.alipay.sofa.rpc.samples.filter.SampleClientFilter"/>
<sofa:reference id="filterServiceReference" interface="com.ostenant.sofa.rpc.example.filter.FilterService">
    <sofa:binding.bolt>
        <sofa:global-attrs filter="sampleFilter"/>
    </sofa:binding.bolt>
</sofa:reference>
複製程式碼

服務端啟動類

SpringApplication springApplication = new SpringApplication(FilterServerApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端啟動類

SpringApplication springApplication = new SpringApplication(FilterClientApplication.class);
ApplicationContext applicationContext = springApplication.run(args);
複製程式碼

客戶端呼叫

FilterService filterServiceReference = (FilterService) applicationContext.getBean("filterServiceReference");
try {
    // sleep 5s, 便於觀察過濾器效果
    Thread.sleep(5000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

String result = filterServiceReference.sayFilter("filter");
System.out.println("Invoke result: " + result);
複製程式碼

檢視攔截輸出

  • 服務端列印輸出
SampleFilter before server process
SampleFilter after server process
複製程式碼
  • 客戶端列印輸出
SampleFilter before client invoke
SampleFilter after client invoke
Invoke result: filter
複製程式碼

過濾器配置生效,總結過濾器攔截先後次序如下:

  1. 客戶端發起呼叫 -> 客戶端前置攔截 -> 服務端前置攔截
  2. 服務端方法執行
  3. 服務端後置攔截 -> 客戶端後置攔截 -> 客戶端接收返回值

小結

本文介紹了 SOFA-RPC 的集中呼叫方式,包括單向呼叫、同步呼叫、Future呼叫、回撥,引入了 SOFA-RPC 獨有的泛化呼叫機制,同時對過濾器的配置進行了簡單介紹。


歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章