原始碼剖析 | 螞蟻金服 mPaaS 框架下的 RPC 呼叫歷程

螞蟻金服移動開發平臺mPaaS發表於2018-10-18

背景

mPaaS-RPC 是支付寶原生的 RPC 呼叫庫。

在客戶端開發過程中,只需要簡單的呼叫庫裡封裝的函式即可完成一次資料請求過程,但是遇到異常情況往往會面對各種異常碼卻不知所云。所以這篇文章帶領大家瞭解一下 mPaaS-RPC 的呼叫過程以及各種異常碼產生的原因。

1. 使用方法

在 Android 端,RPC 的呼叫是很簡單的,大概分為以下幾個過程:

1.1 定義 RPC 介面

首先需要定義 RPC 介面,包括介面名,介面特性(是否需要登入,是否需要簽名等),介面的定義方式如下:

public interface LegoRpcService {
    @CheckLogin
    @SignCheck
    @OperationType("alipay.lego.card.style")
    LegoCardPB viewCard(MidPageCardRequestPB var1);
}
複製程式碼

當客戶端呼叫 viewCard 方法時,框架就會請求 alipay.lego.card.style 對應的 RPC 介面,介面的引數定義在 MidPageCardRequestPB 中。

這個介面呼叫前需要檢查是否登入以及客戶端簽名是否正確,因此如此簡單的一個介面就定義了一個 RPC 請求。

1.2 呼叫 RPC 請求

定義完介面後,需要呼叫 RPC 請求,呼叫方法如下:

//建立引數物件
MidPageCardRequestPB request = new MidPageCardRequestPB();
//填充引數
...

//獲取 RpcService
RpcService rpcService = (RpcService)LauncherApplicationAgent.getInstance().getMicroApplicationContext().findServiceByInterface(RpcService.class.getName());
//獲取Rpc請求代理類
LegoRpcService service = rpcService.getRpcProxy(LegoRpcService.class);
//呼叫方法
LegoCardPB result = service.viewFooter(request);
複製程式碼

呼叫過程大概就分為以上幾步。

值得注意的是,整個過程中我們並沒有去實現 LegoRpcService 這個介面,而是通過 rpcService.getRpcProxy 獲取了一個代理,這裡用的了 Java 動態代理的概念,後面的內容會涉及。

2. 原始碼解析

接下來我們就來看看這套框架是怎麼執行的。

2.1 建立引數物件

引數物件是一個 PB 的物件,這個物件的序列化和反序列化過程需要和服務端對應起來。簡單來說,就是這個引數在客戶端序列化,作為請求的引數傳送請求,然後服務端收到請求後反序列化,根據引數執行請求,返回結果。

MidPageCardRequestPB request = new MidPageCardRequestPB();
複製程式碼

2.2 獲取 RPCService

RPCService 的具體實現是 mpaas-commonservice-git Bundle 中的 RPCServiceImpl

這個新增的過程是在 mPaaS 啟動時,呼叫 CommonServiceLoadAgent 的 load 方法。

        @Override
    public final void load() {
        ...
        registerLazyService(RpcService.class.getName(), RpcServiceImpl.class.getName());
        ...
    }
複製程式碼

RpcServiceImpl 中 getRpcProxy 方法呼叫的是 RpcFactory 的 getRpcProxy 方法。

    @Override
    public <T> T getRpcProxy(Class<T> clazz) {
        return mRpcFactory.getRpcProxy(clazz);
    }
複製程式碼

2.3 獲取 RPC 請求代理類

mRpcFactory 這個物件在 mPaas-Rpc Bundle 中。

     public <T> T getRpcProxy(Class<T> clazz) {
        LogCatUtil.info("RpcFactory","clazz=["+clazz.getName()+"]");
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz},
            new RpcInvocationHandler(mConfig,clazz, mRpcInvoker));
    }
複製程式碼

這裡就是根據介面建立動態代理的過程,這是 Java 原生支援的一個特性。

簡單來說,動態代理即 JVM 會根據介面生成一個代理類,呼叫介面方法時,會先呼叫代理類的 invoke 方法,在 invoke 方法中你可以根據實際情況來做操作從而實現動態代理的作用。

2.4 呼叫方法

動態代理類便呼叫 RpcInvocationHandler 方法: 即呼叫定義的 RPC 介面時,會呼叫到 RpcInvocationHandler 的 invoke 方法:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws RpcException {
        return mRpcInvoker.invoke(proxy, mClazz, method, args, buildRpcInvokeContext(method));
    }
複製程式碼

invoke 方法呼叫的是 mRpcInvoker 的 invoke 方法,而 mRpcInvoker 是建立 RpcInvocationHandler 傳過來的。invoke 方法在原來引數的基礎上又傳遞了 mClazzbuildRpcInvokeContext(method)

mClazz 很好理解,因為 RpcInvocationHandler 是對應某個類,而 mRpcInvoker 是個單例:它並不知道是在代理哪個類的方法,所以需要明確告訴它。

buildRpcInvokeContext(method) 從名字來看是一個上下文物件,這裡面儲存了請求的上下文資訊。

接下來就到了 RpcInvoker 的 invoke 方法:介紹一下 RPC 框架的設計理念,在 invoke 方法中只定義流程,不做具體操作。框架中會註冊很多 Interceptor,每個流程交給都交給 Interceptor 去做。這個思想在網路層的設計架構上很常見,比如很著名的 Spring。遺憾的是,我覺得這套 RPC 框架在後期開發過程中,這個思想有點稍稍被打亂,而 invoke 方法針對一些細節進行了優化處理。

言歸正傳,回到這個 invoke 方法,我總結了一些呼叫流程,如下圖所示:

簡單來說,即依次呼叫了 preHandle,singleCall 和 postHandle。下面我們來看下程式碼:

     public Object invoke(Object proxy, Class<?> clazz, Method method,
                         Object[] args, InnerRpcInvokeContext invokeContext) throws RpcException {
            ...
            preHandle(proxy, clazz, method, args, method.getAnnotations(),invokeContext);// 前置攔截
           ...
          try{
                response = singleCall(method, args, RpcInvokerUtil.getOperationTypeValue(method, args), id, invokeContext,protoDesc);
                returnObj = processResponse(method,response,protoDesc);
        } catch (RpcException exception) {
            exceptionHandle(proxy, response!=null?response.getResData():null, clazz, method, args, method.getAnnotations(), exception, invokeContext);// 異常攔截
        }
          ...
        postHandle(proxy, response!=null?response.getResData():null, clazz, method, args, method.getAnnotations(), invokeContext);// 後置攔截
         ...
        return returnObj;
    }
複製程式碼

2.5 前置攔截

首先來看下前置攔截 preHandle 方法:

private void preHandle(final Object proxy, final Class<?> clazz, final Method method,
                           final Object[] args, Annotation[] annotations, InnerRpcInvokeContext invokeContext) throws RpcException {
        handleAnnotations(annotations, new Handle() {
            @Override
            public boolean handle(RpcInterceptor rpcInterceptor, Annotation annotation)
                    throws RpcException {
                if (!rpcInterceptor.preHandle(proxy, RETURN_VALUE, new byte[]{}, clazz, method,
                        args, annotation, EXT_PARAM)) {
                    throw new RpcException(RpcException.ErrorCode.CLIENT_HANDLE_ERROR,
                            rpcInterceptor + "preHandle stop this call.");
                }
                return true;
            }
        });

        RpcInvokerUtil.preHandleForBizInterceptor(proxy, clazz, method, args, invokeContext, EXT_PARAM, RETURN_VALUE);

        //mock RPC 限流
        RpcInvokerUtil.mockRpcLimit(mRpcFactory.getContext(),method, args);
    }
複製程式碼

前置攔截有三個過程:首先 handleAnnotations 處理方法的註解,然後執行業務層定義的攔截器,最後模擬 RPC 限流。

2.6 處理註解

這是前置攔截中最重要的一步,主要呼叫了 handleAnnotations 方法。handleAnnotations 方法給了一個回撥,回撥的引數是註解以及對應的 RpcInterceptor,返回後呼叫 RpcInterceptor 的 preHandle 方法。

介紹 handleAnnotations 之前先簡單說一下 RpcInterceptor,這個在框架中叫做攔截器:

public interface RpcInterceptor {
    public boolean preHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<?> clazz, Method method, Object[] args, Annotation annotation,ThreadLocal<Map<String,Object>> extParams) throws RpcException;

    public boolean postHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<?> clazz, Method method, Object[] args, Annotation annotation)
                                                                                            throws RpcException;

    public boolean exceptionHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<?> clazz, Method method, Object[] args,
                                   RpcException exception, Annotation annotation) throws RpcException;
}
複製程式碼

簡單來說,程式一開始會註冊幾個攔截器,每個攔截器對應一種註解。invoke 方法中處理註解的時候,會找到對應的攔截器,然後呼叫攔截器相應的方法。正如前面流程圖中所說的,如果攔截器返回 true,繼續往下執行,如果返回 false,則丟擲相應的異常。

接下來繼續說 handleAnnotations: handleAnnotations 其實就是查詢攔截器的方法,看一下具體實現:

private boolean handleAnnotations(Annotation[] annotations, Handle handle) throws RpcException {
            for (Annotation annotation : annotations) {
                Class<? extends Annotation> c = annotation.annotationType();
                RpcInterceptor rpcInterceptor = mRpcFactory.findRpcInterceptor(c);
                ret = handle.handle(rpcInterceptor, annotation);

            }
}
複製程式碼

我們呼叫了 mRpcFactory.findRpcInterceptor(c) 方法去查詢攔截器:mRpcFactory.findRpcInterceptor(c) 查詢了兩個地方:

  • 一個是 mInterceptors;
  • 另一個是 GLOBLE_INTERCEPTORS 裡。
public RpcInterceptor findRpcInterceptor(Class<? extends Annotation> clazz) {
        RpcInterceptor rpcInterceptor = mInterceptors.get(clazz);
        if (rpcInterceptor != null) {
            return rpcInterceptor;
        }
        return GLOBLE_INTERCEPTORS.get(clazz);
    }
複製程式碼

這兩個地方的攔截器其實是一樣的,因為 addRpcInterceptor 的時候會往這兩個地方都新增一次。

public void addRpcInterceptor(Class<? extends Annotation> clazz, RpcInterceptor rpcInterceptor) {
        mInterceptors.put(clazz, rpcInterceptor);
        addGlobelRpcInterceptor(clazz,rpcInterceptor);
    }
複製程式碼

而之前提到的 Spring 的處理方式是:每種情況,多個攔截器依次處理,擴充套件性比較好。

這裡攔截器的新增是在 commonbiz Bundle 中 CommonServiceLoadAgent 的 afterBootLoad 方法中,這個方法也是在 mPaaS 框架啟動的時候呼叫的。

rpcService.addRpcInterceptor(CheckLogin.class, new LoginInterceptor());
rpcService.addRpcInterceptor(OperationType.class, new CommonInterceptor());
rpcService.addRpcInterceptor(UpdateDeviceInfo.class, new CtuInterceptor(mMicroAppContext.getApplicationContext()));
複製程式碼

一共針對三種註解新增了三個攔截器,在文章的最後我們分析一下每種攔截器做了什麼操作:

handleAnnotations 中找到了對應的攔截器,就呼叫其 preHandle 方法進行前置攔截;

preHandle 中處理完註解的前置攔截,會在 preHandleForBizInterceptor 處理上下文物件中帶的攔截器;

上下文物件中的攔截器和註解攔截器道理是一樣的,這個階段我看了一下上下文物件 Context 中並沒有設定攔截器。如果有的話,即取出來然後依次呼叫對應方法。

preHandle 的最後一步是模擬網路限流,這是在測試中使用的,如果測試開啟 RPC 限流功能,那麼這裡會限制 RPC 的訪問來模擬限流的情況。

2.7 singlecall

處理完前置攔截,又回到 RpcInvoker 的 invoke 方法,接下來會呼叫 singleCall 去發起網路請求。

private Response singleCall(Method method, Object[] args,
                                String operationTypeValue, int id, InnerRpcInvokeContext invokeContext,RPCProtoDesc protoDesc) throws RpcException {
        checkLogin(method,invokeContext);
        Serializer serializer = getSerializer(method, args, operationTypeValue,id,invokeContext,protoDesc);
    if (EXT_PARAM.get() != null) {
        serializer.setExtParam(EXT_PARAM.get());
    }
    byte[] body = serializer.packet();
    HttpCaller caller = new HttpCaller(mRpcFactory.getConfig(), method, id, operationTypeValue, body,
    serializerFactory.getContentType(protoDesc), mRpcFactory.getContext(),invokeContext);
        addInfo2Caller(method, serializer, caller, operationTypeValue, body, invokeContext);
        Response response = (Response) caller.call();// 同步
        return response;
    }
複製程式碼

singleCall 一開始又 checkLogin,接著根據引數的型別獲取了序列化器 Serializer,並進行了引數的序列化操作,序列化後的引數作為整個請求的 body。然後一個 HttpCaller 被建立,HttpCaller 以下是網路傳輸層的封裝程式碼,這篇文章中暫時不關注。

在實際傳送請求之前,需要呼叫 addInfo2Caller 往 HttpCaller 中新增一些通用資訊,比如序列化版本,contentType,時間戳,還有根據 SignCheck 註解決定要不要在請求裡新增簽名資訊。

按照我的理解,其實這塊也應該在各個 Intercepter 中處理,不然把 SignCheck 這個註解單獨拿出來處理,程式碼實在是不好看。

最後我們可以去實際呼叫 caller.call 方法傳送請求了,然後收到服務端的回覆。

2.8 ProcessResponse

這樣就執行完 singleCall 方法,獲得了服務端的回覆。

過程又回到了 invoke 裡,獲得服務端 response 後,呼叫 processResponse 去處理這個回覆。處理回覆的過程其實就是把服務端的返回結果反序列化,是前面傳送請求的一個逆過程,程式碼如下:

private Object processResponse(Method method,
                                 Response response,RPCProtoDesc protoDesc) {
        Type retType = method.getGenericReturnType();
        Deserializer deserializer = this.serializerFactory.getDeserializer(retType,response,protoDesc);
        Object object = deserializer.parser();
        if (retType != Void.TYPE) {// 非void
            RETURN_VALUE.set(object);
        }
        return object;
    }
複製程式碼

在 preHandle,singleCall 和 processResponse 這三個過程中,如果有 RpcException 丟擲(處理過程中所有的異常情況都是以 RpcException 的形式丟擲),invoke 中會呼叫 exceptionHandle 異常。

2.9 異常處理

exceptionHandle 也是同樣地去三個 Interceptor 中找相應註釋的攔截器,並呼叫 exceptionHandle:如果返回 true,說明需要繼續處理,再把這個異常丟擲去,交給業務方處理;如果返回 false,則代表異常被吃掉了,不需要被繼續處理了。

private void exceptionHandle(final Object proxy, final byte[] rawResult, final Class<?> clazz,
                                 final Method method, final Object[] args,
                                 Annotation[] annotations, final RpcException exception,InnerRpcInvokeContext invokeContext)
            throws RpcException {
        boolean processed = handleAnnotations(annotations, new Handle() {
            @Override
            public boolean handle(RpcInterceptor rpcInterceptor, Annotation annotation)
                    throws RpcException {
                if (rpcInterceptor.exceptionHandle(proxy, RETURN_VALUE, rawResult, clazz, method,
                        args, exception, annotation)) {
                    LogCatUtil.error(TAG, exception + " need process");
                    // throw exception;
                    return true;
                } else {
                    LogCatUtil.error(TAG, exception + " need not process");
                    return false;
                }
            }
        });
        if (processed) {
            throw exception;
        }

    }
複製程式碼

2.10 後置處理

處理完異常之後,invoke 方法繼續往下執行,下一步是呼叫 postHandle 進行後置攔截。流程跟前置攔截完全一樣,先是去找三個預設的攔截器處理,然後再去 invokeContext 去找業務定製的攔截器,目前這一塊沒有任何實現。

處理 preHandle,singeCall,exceptionHandle 和 postHandle 這幾個主要的流程,invoke 會呼叫 asyncNotifyRpcHeaderUpdateEvent 去通知關心 Response Header 的 Listener,然後列印返回結果的資訊,之後整個流程結束,返回請求結果。

3. 預設攔截器實現

預設攔截器一共有三個,針對三種不同的註解:

rpcService.addRpcInterceptor(CheckLogin.class, new LoginInterceptor());
rpcService.addRpcInterceptor(OperationType.class, new CommonInterceptor());
rpcService.addRpcInterceptor(UpdateDeviceInfo.class, new CtuInterceptor(mMicroAppContext.getApplicationContext()));
複製程式碼

3.1 LoginInterceptor

第一個 LoginInterceptor,顧名思義,就是檢查登入的攔截器。這裡我們只實現了前置攔截的方法 preHandle:

public boolean preHandle(Object proxy, ThreadLocal<Object> retValue, byte[] retRawValue, Class<?> clazz,
                             Method method, Object[] args, Annotation annotation,ThreadLocal<Map<String,Object>> extParams) throws RpcException {
        AuthService authService = AlipayApplication.getInstance().getMicroApplicationContext().getExtServiceByInterface(AuthService.class.getName());
        if (!authService.isLogin() && !ActivityHelper.isBackgroundRunning()) {//未登入
            LoggerFactory.getTraceLogger().debug("LoginInterceptor", "start login:" + System.currentTimeMillis());

            Bundle params = prepareParams(annotation);
            checkLogin(params);
            LoggerFactory.getTraceLogger().debug("LoginInterceptor", "finish login:" + System.currentTimeMillis());
            fail(authService);
        }
        return true;
    }
複製程式碼

檢查是否登入:

  • 如果沒登入,丟擲 CLIENT_LOGIN_FAIL_ERROR = 11 則異常。

3.2 CommonInterceptor

通用攔截器,攔截的是 OperationType 註解,這個註解的 value 是 RPC 請求的方法名,所以可以看出 CommonInterceptor 會處理所有的 RPC 請求,這也是為什麼叫 CommonInterceptor。

public boolean preHandle(Object proxy, ThreadLocal<Object> retValue, byte[] retRawValue, Class<?> clazz,
                             Method method, Object[] args, Annotation annotation, ThreadLocal<Map<String, Object>> extParams)
            throws RpcException {
        checkWhiteList(method, args);
        checkThrottle();
        ...
        writeMonitorLog(ACTION_STATUS_RPC_REQUEST, clazz, method, args);

        for (RpcInterceptor i : RpcCommonInterceptorManager.getInstance().getInterceptors()) {
            i.preHandle(proxy, retValue, retRawValue, clazz, method, args, annotation, extParams);
        }

        return true;
    }
複製程式碼
  • 第一步:檢查白名單:

在啟動的前3s內,為了保證效能,只有白名單的請求才能傳送。如果不在白名單,會丟擲 CLIENT_NOTIN_WHITELIST = 17

  • 第二步:檢查是否限流:

限流是服務端設定的,如果服務端設定了限流,則在某次請求的時候,服務端會返回 SERVER_INVOKEEXCEEDLIMIT=1002異常,這時候 CommonInterceptor 會從返回結果中取到限流到期時間

if(exception.getCode() == RpcException.ErrorCode.SERVER_INVOKEEXCEEDLIMIT){
            String control = exception.getControl();
            if(control!=null){
                mWrite.lock();
                try{
                    JSONObject jsonObject = new JSONObject(control);
                    if(jsonObject.getString("tag").equalsIgnoreCase("overflow")){
                        mThrottleMsg = exception.getMsg();
                        mControl = control;

                        //如果是own的異常,需要更新限流結束的時間
                        if ( exception.isControlOwn() ){
                            mEndTime = System.currentTimeMillis()+jsonObject.getInt("waittime")*1000;
                        }
                    }
                }
        }
複製程式碼
  • 第三步:寫監控日誌
writeMonitorLog(ACTION_STATUS_RPC_REQUEST, clazz, method, args);
複製程式碼
  • 第四步:處理業務定製的攔截器
for (RpcInterceptor i : RpcCommonInterceptorManager.getInstance().getInterceptors()) {
            i.preHandle(proxy, retValue, retRawValue, clazz, method, args, annotation, extParams);
        }
複製程式碼

前面我們說過 RPC 框架中一個攔截器是跟一個註解繫結的,比如 CommonIntercetor 是跟 operatorType 註解繫結的。但是如果業務方想定製 operatorType 註解的攔截器怎麼辦,就需要在 CommonIntercetor 下面再繫結攔截器列表。目前這裡沒有實現,可以忽略。

異常攔截 exceptionHandle 是用來處理服務端返回結果中的異常情況的,業務方可以根據自己的服務端返回結果進行定製。舉個例子,假如你本地的 session 失效了,請求服務端結果後,服務端返回了登入失效的狀態碼 SESSIONSTATUS_FAIL。收到這個異常後業務方可以進行相應的處理了,比如是否需要使用本地儲存的賬號密碼進行自動登入,或者彈出登入框請求使用者登入,或者直接返回讓業務方處理等等各種形式。

後置攔截 postHandle 做了一件事,記錄了服務端返回結果的日誌。

3.3 CtuInterceptor

CtuInterceptor 攔截器對應的是 UpdateDeviceInfo 這個註解。這個註解表示這個 RPC 請求需要裝置資訊。所以前置攔截的時候將裝置資訊寫入請求的引數裡面。

        RpcDeviceInfo rpcDeviceInfo = new RpcDeviceInfo();
        DeviceInfo deviceInfo = DeviceInfo.getInstance();
        // 新增一些裝置資訊到deviceInfo
        ....
複製程式碼

exceptionHandle 和 postHandle 都沒有處理。

以上就是系統預設的三個攔截器,和整個 mPaaS-RPC Bundle 中進行的流程。其實這樣看起來 mPaaS-RPC 只負責網路請求的封裝和傳送,整個流程還是很簡單的。然而網路請求返回後根據不同的錯誤碼進行不同的處理才是真正複雜的部分,這部分本來是交給具體業務方去處理的。

不過良心支付寶又提供了一層封裝 RPC-Beehive 元件,這層是在網路層框架和業務方之間的一層封裝,將通用的一些異常碼進行了處理,比如,請求是轉菊花,或者返回異常後顯示通用異常介面。

以上便是針對 mPaaS-RPC 的原始碼剖析,歡迎大家反饋想法或建議,一起討論修正。

往期閱讀

《開篇 | 模組化與解耦式開發在螞蟻金服 mPaaS 深度實踐探討》

《口碑 App 各 Bundle 之間的依賴分析指南》

關注我們公眾號,獲得第一手 mPaaS 技術實踐乾貨

QRCode

相關文章