
1. 背景
金融級移動開發平台 mPaaS[1](Mobile PaaS)為 App 開發、測試、營運及運維提供雲到端的一站式解決方案,能有效降低技術門檻、減少研發成本、提升開發效率,協助企業快速搭建穩定高品質的移動應用。其中移動網關服務(Mobile Gateway Service,簡稱 MGS)作為mPaas最重要的元件之一,連接配接了移動用戶端與服務端,簡化了移動端與服務端的資料協定和通訊協定,進而能夠顯著提升開發效率和網絡通訊效率。在我們日常運維過程中發現,很多使用者在使用用戶端RPC元件的時候,有很多不同場景的訴求,比如攔截請求添加業務請求标記,免登,傳回結果模拟,異常處理,限流等。本文旨在介紹通過利用RPC提供的攔截器機制,通過不同實際場景的描述,供業務參考使用。
2. RPC調用原理
當 App 在移動網關控制台接入背景服務後,調用RPC的示例代碼如下:
RpcDemoClient client = MPRpc.getRpcProxy(RpcDemoClient.class);
// 設定請求
GetIdGetReq req = new GetIdGetReq();
req.id = "123";
req.age = 14;
req.isMale = true;
// 發起 rpc 請求
String response = client.getIdGet(req);
值得好奇的是,整個調用過程中其實我們并沒有去實作 RpcDemoClient 這個接口,而是通過 MPRpc.getRpcProxy 擷取了一個代理,通過代理對象完成了調用。在這裡其實主要使用了 Java 動态代理的技術。當調用RPC接口的時候,會通過動态代理的RpcInvocationHandler,回調其實作的invoke方法,最終在invoke内實作資料的序列化處理最後通過網絡庫發到服務端。
public <T> T getRpcProxy(Class<T> clazz) {
LogCatUtil.info("RpcFactory", "clazz=[" + clazz.getName() + "]");
return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new RpcInvocationHandler(this.mConfig, clazz, this.mRpcInvoker));
}
在業務開發中,如果在某些情況下需要控制用戶端的網絡請求(攔截網絡請求,禁止通路某些接口,或者限流),可以通過 RPC 攔截器實作。
RpcService rpcService = getMicroApplicationContext().findServiceByInterface(RpcService.class.getName());
rpcService.addRpcInterceptor(OperationType.class, new CommonInterceptor());
3. 攔截器
3.1 原理
RPC目前采用了攔截器機制實作RPC的自定義處理,如下圖所示,業務可以通過設定自定義RpcIntercept實作在請求前,請求異常,請求傳回三個階段對RPC的定制處理。
圖1:rpc攔截器調用示意圖
4. preHandle場景
4.1 全局添加業務自定義請求header
典型使用場景:業務添加自定義的業務全局辨別或者其他統計字段
@Overridepublic boolean preHandle(Object proxy,
ThreadLocal<Object> retValue,byte[] retRawValue,
Class<?> aClass,
Method method,
Object[] args,
Annotation annotation,
ThreadLocal<Map<String, Object>> threadLocal)throws RpcException {//Do something...
RpcInvocationHandler handler = (RpcInvocationHandler) Proxy.getInvocationHandler(proxy);
handler.getRpcInvokeContext().addRequestHeader("header", "headerCustom");return true;}
4.2 阻斷目前請求rpc流程
典型使用場景:比如如果目前未登入,對需要登入的rpc先阻斷,統一提示登入
@Overridepublic boolean preHandle(Object proxy,
ThreadLocal<Object> retValue,byte[] retRawValue,
Class<?> aClass,
Method method,
Object[] args,
Annotation annotation,
ThreadLocal<Map<String, Object>> threadLocal)throws RpcException {//Do something...
String operationType = getOperationType(aClass, method, args);if ("operationType1".equals(operationType)) {boolean isLogin = false;if (!isLogin) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {@Overridepublic void run() {
Toast.makeText(LauncherApplicationAgent.getInstance().getApplicationContext(),"
目前未登入,請登入", Toast.LENGTH_SHORT).show();}});// 傳回給上層調用登入失敗的異常,上層做業務處理throw new RpcException(RpcException.ErrorCode.CLIENT_LOGIN_FAIL_ERROR, "login fail.");}}return true;}private String getOperationType(Class<?> aClass, Method method, Object[] args) {if (aClass == null || null == method) return "";
OperationType operationType = method.getAnnotation(OperationType.class);return operationType == null ? "" : operationType.value();}
5. postHandle場景
5.1 攔截接口傳回
典型使用場景:全局修改服務端的傳回結果,比如mock服務端的資料
@Overridepublic boolean postHandle(Object proxy,
ThreadLocal<Object> threadLocal,byte[] retRawValue,
Class<?> aClass,
Method method,
Object[] args,
Annotation annotation) throws RpcException {//Do something...// 場景:修改服務端傳回的資料,比如mock資料,或者修改服務端資料
String operationType = getOperationType(aClass, method, args);
LoggerFactory.getTraceLogger().debug(TAG, "postHandle:" + operationType);if ("operationType1".equals(operationType)) {
String value = JSON.parse(retRawValue).toString();
LoggerFactory.getTraceLogger().debug(TAG, "postHandle 原始傳回" + value);
String mockData = "{\"img\":\"imgPath\",\"User\":{\"name\":\"我是mock的資料\",\"age\":18}}";
Object mockObj = JSON.parseObject(mockData, method.getReturnType());
threadLocal.set(mockObj);return true;}return true;}private String getOperationType(Class<?> aClass, Method method, Object[] args) {if (aClass == null || null == method) return "";
OperationType operationType = method.getAnnotation(OperationType.class);return operationType == null ? "" : operationType.value();}
6. exceptionHandle場景
6.1 異常統一處理
比如登入态失效,服務端會統一傳回2000的錯誤碼,用戶端可以在exceptionHandle裡統一攔截進行登入态免登操作。
@Overridepublic boolean exceptionHandle(Object proxy, ThreadLocal<Object> retValue, byte[] bytes, Class<?> aClass, Method method, Object[] objects,
RpcException rpcException, Annotation annotation) throws RpcException {
String operationType = getOperationType(aClass, method, objects);if (RpcException.ErrorCode.CLIENT_LOGIN_FAIL_ERROR == rpcException.getCode()&&"operationType1".equals(operationType)) {// 1. 去免登
hasLogin = true;// 2. 免登後在幫上層重發請求,免登操作對上層業務無感覺try {
LoggerFactory.getTraceLogger().debug(TAG, "exceptionHandle. Start resend rpc begin " + operationType);// 重發請求
Object object = method.invoke(proxy, objects);
retValue.set(object);
LoggerFactory.getTraceLogger().debug(TAG, "exceptionHandle. Start resend rpc success");return false;} catch (Throwable e) {
LoggerFactory.getTraceLogger().error(TAG, "resend rpc occurs illegal argument exception", e);throw new RpcException(RpcException.ErrorCode.CLIENT_HANDLE_ERROR, e + "");}}return true;}
7. H5場景
由于H5場景中使用的jsapi的rpc,需要支援在js環境裡傳遞到native環境,是以在設計上,是統一通過operationType: alipay.client.executerpc 接口進行的轉發,是以針對H5發送的RPC請求,需要做特殊判斷,通過入參拿到真實的operationType接口,示例代碼如下。
7.1 擷取H5請求的接口名稱和入參
var params = [{"_requestBody":{"userName":"", "userId":0}}]var operationType = 'alipay.mobile.ic.dispatch'
AlipayJSBridge.call('rpc', {
operationType: operationType,
requestData: params,
headers:{}}, function (result) {
console.log(result);});
業務通過jsapi去請求rpc,如何擷取jsapi請求的rpc名稱,可以參考代碼如下
@Overridepublic boolean preHandle(Object o, ThreadLocal<Object> threadLocal, byte[] bytes, Class<?> aClass, Method method, Object[] objects, Annotation annotation, ThreadLocal<Map<String, Object>> threadLocal1) throws RpcException {
String operationType = getOperationType(aClass, method, objects);if ("alipay.client.executerpc".equals(operationType)) {// H5的rpc名稱
String rpcName = (String) objects[0];// 入參
String req = (String) objects[1];
LoggerFactory.getTraceLogger().debug(TAG, "operationType:" + rpcName + " " + req);} else {// Native的rpc}
LoggerFactory.getTraceLogger().debug(TAG, "operationType:" + operationType);return true;}
參考文檔
[1] mPaaS平台:
https://www.aliyun.com/product/mobilepaas/mpaas我們是阿裡雲智能全球技術服務-SRE團隊,我們緻力成為一個以技術為基礎、面向服務、保障業務系統高可用的工程師團隊;提供專業、體系化的SRE服務,幫助廣大客戶更好地使用雲、基于雲建構更加穩定可靠的業務系統,提升業務穩定性。我們期望能夠分享更多幫助企業客戶上雲、用好雲,讓客戶雲上業務運作更加穩定可靠的技術,您可用釘釘掃描下方二維碼,加入阿裡雲SRE技術學院釘釘圈子,和更多雲上人交流關于雲平台的那些事。