上週處理了一個線上問題,通過排查發現是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) { // 包裝調用代碼片斷 } }
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; }
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就完成了: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; } }
若是對上層的返回結果須要統一封裝,也能夠在攔截器裏作。