Dubbo生產者丟擲自定義異常的問題
阿新 • • 發佈:2019-01-31
最近開始學習Dubbo框架,在工作中會把之前的業務遷移過來。
在原來的Spring MVC框架實現中,有使用到自定義異常的場景(自定義異常繼承RuntimeException)。而對於異常(包括自定義異常),在業務程式碼中都不做任何try-catch操作,而是由公用的Controller來處理異常。
在使用dubbo的過程中,在dubbo的service端定義有自定義異常進行throw的時候,卻發現在customer的Controller中無法instanceof,自己自定義的異常類被轉成了Runtime異常,有點不理解。在翻看dubbo原始碼的時候,發現確實如此:
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { try { Result result = invoker.invoke(invocation); if (result.hasException() && GenericService.class != invoker.getInterface()) { try { Throwable exception = result.getException(); // 如果是checked異常,直接丟擲 if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) { return result; } // 在方法簽名上有宣告,直接丟擲 try { Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes()); Class<?>[] exceptionClassses = method.getExceptionTypes(); for (Class<?> exceptionClass : exceptionClassses) { if (exception.getClass().equals(exceptionClass)) { return result; } } } catch (NoSuchMethodException e) { return result; } // 未在方法簽名上定義的異常,在伺服器端列印ERROR日誌 logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception); // 異常類和介面類在同一jar包裡,直接丟擲 String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface()); String exceptionFile = ReflectUtils.getCodeBase(exception.getClass()); if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){ return result; } // 是JDK自帶的異常,直接丟擲 String className = exception.getClass().getName(); if (className.startsWith("java.") || className.startsWith("javax.")) { return result; } // 是Dubbo本身的異常,直接丟擲 if (exception instanceof RpcException) { return result; } // 否則,包裝成RuntimeException拋給客戶端 return new RpcResult(new RuntimeException(StringUtils.toString(exception))); } catch (Throwable e) { logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); return result; } } return result; } catch (RuntimeException e) { logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e); throw e; } }
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
最後這一段是是把我們的自定義異常給重新封裝了一下了,所以我們就捕獲不到自定義異常了
官方提供的解決方法:
服務化最佳實踐 (+) (#) 分包 建議將服務介面,服務模型,服務異常等均放在API包中,因為服務模型及異常也是API的一部分, 同時,這樣做也符合分包原則:重用釋出等價原則(REP),共同重用原則(CRP) 如果需要,也可以考慮在API包中放置一份spring的引用配置,這樣使用方,只需在Spring載入過程中引用此配置即可, 配置建議放在模組的包目錄下,以免衝突,如:com/alibaba/china/xxx/dubbo-reference.xml 粒度 服務介面儘可能大粒度,每個服務方法應代表一個功能,而不是某功能的一個步驟,否則將面臨分散式事務問題,Dubbo暫未提供分散式事務支援。 服務介面建議以業務場景為單位劃分,並對相近業務做抽象,防止介面數量爆炸 不建議使用過於抽象的通用介面,如:Map query(Map),這樣的介面沒有明確語義,會給後期維護帶來不便。 版本 每個介面都應定義版本號,為後續不相容升級提供可能,如:<dubbo:service interface="com.xxx.XxxService" version="1.0" /> 建議使用兩位版本號,因為第三位版本號通常表示相容升級,只有不相容時才需要變更服務版本。 當不相容時,先升級一半提供者為新版本,再將消費者全部升為新版本,然後將剩下的一半提供者升為新版本。 相容性 服務介面增加方法,或服務模型增加欄位,可向後相容,刪除方法或刪除欄位,將不相容,列舉型別新增欄位也不相容,需通過變更版本號升級。 各協議的相容性不同,參見: 服務協議 列舉值 如果是完備集,可以用Enum,比如:ENABLE, DISABLE。 如果是業務種類,以後明顯會有型別增加,不建議用Enum,可以用String代替。 如果是在返回值中用了Enum,並新增了Enum值,建議先升級服務消費方,這樣服務提供方不會返回新值。 如果是在傳入引數中用了Enum,並新增了Enum值,建議先升級服務提供方,這樣服務消費方不會傳入新值。 序列化 服務引數及返回值建議使用POJO物件,即通過set,get方法表示屬性的物件。 服務引數及返回值不建議使用介面,因為資料模型抽象的意義不大,並且序列化需要介面實現類的元資訊,並不能起到隱藏實現的意圖。 服務引數及返回值都必需是byValue的,而不能是byRef的,消費方和提供方的引數或返回值引用並不是同一個,只是值相同,Dubbo不支援引用遠端物件。 異常 建議使用異常彙報錯誤,而不是返回錯誤碼,異常資訊能攜帶更多資訊,以及語義更友好, 如果擔心效能問題,在必要時,可以通過override掉異常類的fillInStackTrace()方法為空方法,使其不拷貝棧資訊, 查詢方法不建議丟擲checked異常,否則呼叫方在查詢時將過多的try...catch,並且不能進行有效處理, 服務提供方不應將DAO或SQL等異常拋給消費方,應在服務實現中對消費方不關心的異常進行包裝,否則可能出現消費方無法反序列化相應異常。 呼叫 不要只是因為是Dubbo呼叫,而把呼叫Try-Catch起來。Try-Catch應該加上合適的回滾邊界上。 對於輸入引數的校驗邏輯在Provider端要有。如有效能上的考慮,服務實現者可以考慮在API包上加上服務Stub類來完成檢驗。
所以在dubbo的service端想丟擲自定義異常,只能通過在service端的介面方法上宣告所要丟擲的異常,或者將異常類與介面同包,再或者是介面的實現類再實現dubbo的GenericService介面。
對於第一種方案沒有使用,因為它對程式碼的入侵比較嚴重。
第二種方案可以實現,可對於目前的業務框架,讓介面類和異常類同包則變得不太可能。
所以最後選擇了讓介面實現類再實現GenericService介面,而對於其需要實現的$invoke方法則沒有做任何的方法體處理,直接廢棄。
對於dubbo的service端自定義異常類的處理,有些不理解的就是,為什麼dubbo需要對自定義異常類做一次Runtime異常的轉化,而不是直接丟擲原異常型別。或者有沒有對dubbo更瞭解的朋友,有對自定義異常更好的處理方法。
發現實現這個GenericService介面不是很好用,最後還是每個介面宣告所要跑出的異常了例如:
package com.test.test.manager.api;
import com.test.entity.Area;
import com.test.common.exception.BaseRuntimeException;
public interface AreaApiManager {
Area selectByPrimaryKey1(Integer id) throws BaseRuntimeException;
}
package com.test.test.manager.impl.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import com.shihai.borrow.dao.api.AreaApiMapper;
import com.test.entity.Area;
import com.test.test.manager.api.AreaApiManager;
import com.test.common.exception.BaseRuntimeException;
@Transactional
public class AreaApiManagerImpl implements AreaApiManager {
@Autowired
AreaApiMapper areaApiMapper;
@Override
public Area selectByPrimaryKey1(Integer id) throws BaseRuntimeException {
// TODO Auto-generated method stub
return areaApiMapper.selectByPrimaryKey1(id);
}
}
通過以上的封裝在消費者端就能捕獲到異常了原文:http://my.oschina.net/boltwu/blog/493888?p={{page}}