遠端呼叫程式碼封裝雜談
上週處理了一個線上問題,經過排查發現是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;
}
}
如果對上層的返回結果需要統一封裝,也可以在攔截器裡做