1. 程式人生 > >遠端呼叫程式碼封裝雜談

遠端呼叫程式碼封裝雜談

上週處理了一個線上問題,經過排查發現是RPC遠端呼叫超時,框架丟擲的超時異常沒有被捕捉,導致資料進入中間態,無法推進後續處理。好在影響不大,及時修復掉了。
關於這部分的程式碼規範,之前也有所思考,正好有這個契機做一下整理。

討論背景和範圍

做應用分層架構時,有一種實踐方式是將代表外部服務的類如UserService,包裝成一個UserServiceClient類,上層業務呼叫統一使用UserServiceClient,是一種簡單的代理模式。
本文的討論例項,即UserService、UserServiceClient以及其實現UserServiceClientImpl,形式化的定義如下:

// 遠端RPC介面
public interface UserService {
    /**
     * 使用者查詢
     */
    ResultDTO<UserInfo> query(QueryReequest param);
    
    /**
     * 使用者建立
     */
    ResultDTO<String> create(CreateRequest param);
}
// 本地介面
public interface UserServiceClient {
    /**
     * 使用者查詢
     */
    Result<UserInfo> query(QueryReequest param);
    
    /**
     * 使用者建立
     */
    Result<String> create(CreateRequest param);
}
// 本地介面實現
public classe UserServiceClientImpl implement  UserServiceClient {

    @Autorwire
    private UserService userSerivce;
    /**
     * 使用者查詢
     */
    @override
    Result<UserInfo> query(QueryReequest param) {
        // 包裝呼叫程式碼片段
    }

    /**
     * 使用者建立
     */
    @override
    Result<String> create(CreateRequest param) {
        // 包裝呼叫程式碼片段
    }
}

一、不做任何處理/不封裝

Client類沒有任何的處理,僅僅是對Servie類的呼叫及原樣返回。

// 本地介面實現
public classe UserServiceClientImpl implement  UserServiceClient {

    @Autorwire
    private UserService userSerivce;
    /**
     * 使用者查詢
     */
    @override
    Result<UserInfo> query(QueryReequest param) {
        return userSerivce.query(param);
    }

    /**
     * 使用者建立
     */
    @override
    Result<String> create(CreateRequest request) {
        return userSerivce.create(param);
    }
}

非常不推薦,原因可以和後續的幾種形式中對比來看。

這種寫法實際上跟lombok提供的@Delegate註解是一樣的,這個註解一樣不推薦。

@Component
public class UserServiceClient {
    @Autowired
    @Delegate
    private UserService userService;
}

二、結果統一再封裝

RPC呼叫的目標可能是不同的系統,呼叫的封裝結果也有所不同。為了便於上層業務處理,減少對外部的感知,可以定義一個通用的Result類來包裝。

// 本地介面實現
public classe UserServiceClientImpl implement  UserServiceClient {

    @Autorwire
    private UserService userSerivce;
    /**
     * 使用者查詢
     */
    @override
    Result<UserInfo> query(QueryReequest param) {
        ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
        Result<UserInfo> result = new Result<>(); 
        // 封裝呼叫結果
        result.setSuccess(result.isSuccess());
        result.setData(result.getData());
        // 錯誤碼、錯誤堆疊等填充,略
        return result;
    }

    /**
     * 使用者建立
     */
    @override
    Result<String> create(CreateRequest request) {
        // 略
    }
}

三、只取結果不封裝

上層處理時,對封裝的結果判斷會比較冗餘。如果在Client就能區分使用意圖,可以將非預期的結果封裝成業務異常,預期結果直接返回。
特定場景的返回結果可以用不同的業務異常區分。

// 本地介面實現
public classe UserServiceClientImpl implement  UserServiceClient {

    @Autorwire
    private UserService userSerivce;
    /**
     * 使用者查詢
     */
    @override
    UserInfo query(QueryReequest param) {
        ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
        if(rpcResult == null) {
            throw new BizException("呼叫結果為空!");
        }
        if(rpcResult != null && rpcResult.isSuccess()) {
            return rpcResult.getData();
        }
        if("XXX".equals(rpcResult.getErrorCode())) {
            throw new XXXBizException("呼叫結果失敗,異常碼XXX");
        } else {
            throw new BizException("呼叫結果失敗");
        }
    }

    /**
     * 使用者建立
     */
    @override
    String create(CreateRequest request) {
        // 略
    }
}

四、對呼叫處增加異常處理

RPC呼叫會發生系統間互動,難免會出現超時,很多框架直接丟擲超時異常。除此以外,被呼叫的業務系統介面可能由於歷史原因或者編碼問題,可能會直接把自己的異常拋給呼叫者。為了保證自己系統的穩定性,需要對異常進行捕獲。
如何捕獲異常?並不是簡單的catch(Exception e)就能搞定。在阿里巴巴出品的《Java開發手冊》中提到,要用Throwable來捕獲,原因是:

【強制】在呼叫 RPC、二方包、或動態生成類的相關方法時,捕捉異常必須使用 Throwable
類來進行攔截。
說明:通過反射機制來呼叫方法,如果找不到方法,丟擲 NoSuchMethodException。什麼情況會丟擲
NoSuchMethodError 呢?二方包在類衝突時,仲裁機制可能導致引入非預期的版本使類的方法簽名不匹
配,或者在位元組碼修改框架(比如:ASM)動態建立或修改類時,修改了相應的方法簽名。這些情況,即
使程式碼編譯期是正確的,但在程式碼執行期時,會丟擲 NoSuchMethodError。

這樣,一個完善的Client就完成了:

// 本地介面實現
public classe UserServiceClientImpl implement  UserServiceClient {

    @Autorwire
    private UserService userSerivce;
    /**
     * 使用者查詢
     */
    @override
    UserInfo query(QueryReequest param) {
        try {
            ResultDTO<UserInfo> rpcResult = userSerivce.query(param);
        } catch (Throwable t) {
            if(t instanceof XXXTimeoutException) {
                // 已知的特殊呼叫異常處理,如超時異常需要做自動重試,特殊處理
                throw new BizException("超時異常")
            }
            throw new BizException("呼叫異常", t)
        }
        if(rpcResult == null) {
            throw new BizException("呼叫結果為空!");
        }
        if(rpcResult != null && rpcResult.isSuccess()) {
            return rpcResult.getData();
        }
        if("XXX".equals(rpcResult.getErrorCode())) {
            throw new XXXBizException("呼叫結果失敗,異常碼XXX");
        } else {
            throw new BizException("呼叫結果失敗");
        }
    }

    /**
     * 使用者建立
     */
    @override
    String create(CreateRequest request) {
        // 略
    }
}

用攔截器封裝異常

對於外部呼叫,以及內部呼叫,都可以用攔截器做統一的處理。對於捕獲的異常的處理以及日誌的列印在攔截器中做,會讓程式碼編寫更加簡潔。
示例如下:

import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class RpcInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        String invocationSignature = method.getDeclaringClass().getSimpleName() + "." + method.getName();

        // 排除掉java原生方法
        for(Method m : methods) {
            if(invocation.getMethod().equals(m)) {
                return invocation.proceed();
            }
        }

        Object result = null;
        Objectp[] params = invocation.getArguments();

        try {

            result = invocation.proceed();
        } catch( Throwable e) {
            // 接各種異常,區分異常型別

            // 處理異常、列印日誌

        } finally {
            // 列印結果日誌, 列印時也要處理異常
        }
        return result;
}

設定代理

import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConitionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.Bean;
import org.springframework.context.ComponentScan;
import org.springframework.context.Configuration;



@Configuration
public class Interceptor {
    
    @Bean
    public RpcInterceptor rpcSerivceInterceptor() {
        RpcInterceptor rpcSerivceInterceptor = new RpcInterceptor();
        // 可以注入一些logger什麼的
        return rpcSerivceInterceptor;
    }

    @Bean
    public BeanNameAutoProxyCreator rpcServiceBeanAutoProxyCreator() {
        BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
        // 設定代理類的名稱
        beanNameAutoProxyCreator.setBeanNames("*RpcServiceImpl");

        // 設定攔截鏈名字
        beanNameAutoProxyCreator.setInterceptorName("rpcSerivceInterceptor");
        return beanNameAutoProxyCreator;
    }


}

如果對上層的返回結果需要統一封裝,也可以在攔截器裡做