遠程調用代碼封裝雜談

上週處理了一個線上問題,通過排查發現是RPC遠端調用超時,框架拋出的超時異常沒有被捕捉,致使數據進入中間態,沒法推動後續處理。好在影響不大,及時修復掉了。
關於這部分的代碼規範,以前也有所思考,正好有這個契機作一下整理。java

討論背景和範圍

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

// 遠程RPC接口
public interface UserService {
    /**
     * 用戶查詢
     */
    ResultDTO<UserInfo> query(QueryRequest 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) {
        // 包裝調用代碼片斷
    }
}

1、不作任何處理/不封裝

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註解是同樣的,這個註解同樣不推薦。ide

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

2、結果統一再封裝

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) {
        // 略
    }
}

3、只取結果不封裝

上層處理時,對封裝的結果判斷會比較冗餘。若是在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) {
        // 略
    }
}

4、對調用處增長異常處理

RPC調用會發生系統間交互,不免會出現超時,不少框架直接拋出超時異常。除此之外,被調用的業務系統接口可能因爲歷史緣由或者編碼問題,可能會直接把本身的異常拋給調用者。爲了保證本身系統的穩定性,須要對異常進行捕獲。
如何捕獲異常?並非簡單的catch(Exception e)就能搞定。在阿里巴巴出品的《Java開發手冊》中提到,要用Throwable來捕獲,緣由是:日誌

【強制】在調用 RPC、二方包、或動態生成類的相關方法時,捕捉異常必須使用 Throwable
類來進行攔截。
說明:經過反射機制來調用方法,若是找不到方法,拋出 NoSuchMethodException。什麼狀況會拋出
NoSuchMethodError 呢?二方包在類衝突時,仲裁機制可能致使引入非預期的版本使類的方法簽名不匹
配,或者在字節碼修改框架(好比:ASM)動態建立或修改類時,修改了相應的方法簽名。這些狀況,即
使代碼編譯期是正確的,但在代碼運行期時,會拋出 NoSuchMethodError。代碼規範

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

// 本地接口實現
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;
    }


}

若是對上層的返回結果須要統一封裝,也能夠在攔截器裏作。

相關文章
相關標籤/搜索