淺談dubbo的ExceptionFilter異常處理

本文原地址:http://blog.csdn.net/mj158518/article/details/51228649

背景

我們的項目使用了dubbo進行不同系統之間的調用。
每個項目都有一個全局的異常處理,對於業務異常,我們會拋出自定義的業務異常(繼承RuntimeException)。
全局的異常處理會根據不同的異常類型進行不同的處理。
最近我們發現,某個系統調用dubbo請求,provider端(服務提供方)拋出了自定義的業務異常,但consumer端(服務消費方)拿到的並不是自定義的業務異常。
這是爲什麼呢?還需要從dubbo的ExceptionFilter說起。

ExceptionFilter

如果Dubbo的 provider端 拋出異常(Throwable),則會被 provider端 的ExceptionFilter攔截到,執行以下invoke方法:
[java] view plain copy
  1. /* 
  2.  * Copyright 1999-2011 Alibaba Group. 
  3.  *   
  4.  * Licensed under the Apache License, Version 2.0 (the "License"); 
  5.  * you may not use this file except in compliance with the License. 
  6.  * You may obtain a copy of the License at 
  7.  *   
  8.  *      http://www.apache.org/licenses/LICENSE-2.0 
  9.  *   
  10.  * Unless required by applicable law or agreed to in writing, software 
  11.  * distributed under the License is distributed on an "AS IS" BASIS, 
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
  13.  * See the License for the specific language governing permissions and 
  14.  * limitations under the License. 
  15.  */  
  16. package com.alibaba.dubbo.rpc.filter;  
  17.   
  18. import java.lang.reflect.Method;  
  19.   
  20. import com.alibaba.dubbo.common.Constants;  
  21. import com.alibaba.dubbo.common.extension.Activate;  
  22. import com.alibaba.dubbo.common.logger.Logger;  
  23. import com.alibaba.dubbo.common.logger.LoggerFactory;  
  24. import com.alibaba.dubbo.common.utils.ReflectUtils;  
  25. import com.alibaba.dubbo.common.utils.StringUtils;  
  26. import com.alibaba.dubbo.rpc.Filter;  
  27. import com.alibaba.dubbo.rpc.Invocation;  
  28. import com.alibaba.dubbo.rpc.Invoker;  
  29. import com.alibaba.dubbo.rpc.Result;  
  30. import com.alibaba.dubbo.rpc.RpcContext;  
  31. import com.alibaba.dubbo.rpc.RpcException;  
  32. import com.alibaba.dubbo.rpc.RpcResult;  
  33. import com.alibaba.dubbo.rpc.service.GenericService;  
  34.   
  35. /** 
  36.  * ExceptionInvokerFilter 
  37.  * <p> 
  38.  * 功能: 
  39.  * <ol> 
  40.  * <li>不期望的異常打ERROR日誌(Provider端)<br> 
  41.  *     不期望的日誌即是,沒有的接口上聲明的Unchecked異常。 
  42.  * <li>異常不在API包中,則Wrap一層RuntimeException。<br> 
  43.  *     RPC對於第一層異常會直接序列化傳輸(Cause異常會String化),避免異常在Client出不能反序列化問題。 
  44.  * </ol> 
  45.  *  
  46.  * @author william.liangf 
  47.  * @author ding.lid 
  48.  */  
  49. @Activate(group = Constants.PROVIDER)  
  50. public class ExceptionFilter implements Filter {  
  51.   
  52.     private final Logger logger;  
  53.       
  54.     public ExceptionFilter() {  
  55.         this(LoggerFactory.getLogger(ExceptionFilter.class));  
  56.     }  
  57.       
  58.     public ExceptionFilter(Logger logger) {  
  59.         this.logger = logger;  
  60.     }  
  61.       
  62.     public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {  
  63.         try {  
  64.             Result result = invoker.invoke(invocation);  
  65.             if (result.hasException() && GenericService.class != invoker.getInterface()) {  
  66.                 try {  
  67.                     Throwable exception = result.getException();  
  68.   
  69.                     // 如果是checked異常,直接拋出  
  70.                     if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {  
  71.                         return result;  
  72.                     }  
  73.                     // 在方法簽名上有聲明,直接拋出  
  74.                     try {  
  75.                         Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());  
  76.                         Class<?>[] exceptionClassses = method.getExceptionTypes();  
  77.                         for (Class<?> exceptionClass : exceptionClassses) {  
  78.                             if (exception.getClass().equals(exceptionClass)) {  
  79.                                 return result;  
  80.                             }  
  81.                         }  
  82.                     } catch (NoSuchMethodException e) {  
  83.                         return result;  
  84.                     }  
  85.   
  86.                     // 未在方法簽名上定義的異常,在服務器端打印ERROR日誌  
  87.                     logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  88.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  89.                             + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);  
  90.   
  91.                     // 異常類和接口類在同一jar包裏,直接拋出  
  92.                     String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());  
  93.                     String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());  
  94.                     if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){  
  95.                         return result;  
  96.                     }  
  97.                     // 是JDK自帶的異常,直接拋出  
  98.                     String className = exception.getClass().getName();  
  99.                     if (className.startsWith("java.") || className.startsWith("javax.")) {  
  100.                         return result;  
  101.                     }  
  102.                     // 是Dubbo本身的異常,直接拋出  
  103.                     if (exception instanceof RpcException) {  
  104.                         return result;  
  105.                     }  
  106.   
  107.                     // 否則,包裝成RuntimeException拋給客戶端  
  108.                     return new RpcResult(new RuntimeException(StringUtils.toString(exception)));  
  109.                 } catch (Throwable e) {  
  110.                     logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()  
  111.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  112.                             + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  113.                     return result;  
  114.                 }  
  115.             }  
  116.             return result;  
  117.         } catch (RuntimeException e) {  
  118.             logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  119.                     + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  120.                     + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  121.             throw e;  
  122.         }  
  123.     }  
  124.   
  125. }  

代碼分析

按邏輯順序進行分析,滿足其中一個即返回,不再繼續執行判斷。

邏輯0

[java] view plain copy
  1. if (result.hasException() && GenericService.class != invoker.getInterface()) {  
  2.     //...  
  3. }  
  4. return result;  
調用結果有異常且未實現GenericService接口,進入後續判斷邏輯,否則直接返回結果。
[java] view plain copy
  1. /** 
  2.  * 通用服務接口 
  3.  *  
  4.  * @author william.liangf 
  5.  * @export 
  6.  */  
  7. public interface GenericService {  
  8.   
  9.     /** 
  10.      * 泛化調用 
  11.      *  
  12.      * @param method 方法名,如:findPerson,如果有重載方法,需帶上參數列表,如:findPerson(java.lang.String) 
  13.      * @param parameterTypes 參數類型 
  14.      * @param args 參數列表 
  15.      * @return 返回值 
  16.      * @throws Throwable 方法拋出的異常 
  17.      */  
  18.     Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;  
  19.   
  20. }  
泛接口實現方式主要用於服務器端沒有API接口及模型類元的情況,參數及返回值中的所有POJO均用Map表示,通常用於框架集成,比如:實現一個通用的遠程服務Mock框架,可通過實現GenericService接口處理所有服務請求。
不適用於此場景,不在此處探討。

邏輯1

[java] view plain copy
  1. // 如果是checked異常,直接拋出  
  2. if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {  
  3.     return result;  
  4. }  
不是RuntimeException類型的異常,並且是受檢異常(繼承Exception),直接拋出。
provider端想拋出受檢異常,必須在api上明確寫明拋出受檢異常;consumer端如果要處理受檢異常,也必須使用明確寫明拋出受檢異常的api。
provider端api新增 自定義的 受檢異常, 所有的 consumer端api都必須升級,同時修改代碼,否則無法處理這個特定異常。

consumer端DecodeableRpcResult的decode方法會對異常進行處理


此處會拋出IOException,上層catch後會做toString處理,放到mErrorMsg屬性中:
[java] view plain copy
  1. try {  
  2.     decode(channel, inputStream);  
  3. catch (Throwable e) {  
  4.     if (log.isWarnEnabled()) {  
  5.         log.warn("Decode rpc result failed: " + e.getMessage(), e);  
  6.     }  
  7.     response.setStatus(Response.CLIENT_ERROR);  
  8.     response.setErrorMessage(StringUtils.toString(e));  
  9. finally {  
  10.     hasDecoded = true;  
  11. }  

DefaultFuture判斷請求返回的結果,最後拋出RemotingException:
[java] view plain copy
  1. private Object returnFromResponse() throws RemotingException {  
  2.     Response res = response;  
  3.     if (res == null) {  
  4.         throw new IllegalStateException("response cannot be null");  
  5.     }  
  6.     if (res.getStatus() == Response.OK) {  
  7.         return res.getResult();  
  8.     }  
  9.     if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {  
  10.         throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());  
  11.     }  
  12.     throw new RemotingException(channel, res.getErrorMessage());  
  13. }  

DubboInvoker捕獲RemotingException,拋出RpcException:
[java] view plain copy
  1. try {  
  2.     boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);  
  3.     boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);  
  4.     int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY,Constants.DEFAULT_TIMEOUT);  
  5.     if (isOneway) {  
  6.         boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);  
  7.         currentClient.send(inv, isSent);  
  8.         RpcContext.getContext().setFuture(null);  
  9.         return new RpcResult();  
  10.     } else if (isAsync) {  
  11.         ResponseFuture future = currentClient.request(inv, timeout) ;  
  12.         RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));  
  13.         return new RpcResult();  
  14.     } else {  
  15.         RpcContext.getContext().setFuture(null);  
  16.         return (Result) currentClient.request(inv, timeout).get();  
  17.     }  
  18. catch (TimeoutException e) {  
  19.     throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);  
  20. catch (RemotingException e) {  
  21.     throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);  
  22. }  

調用棧:
FailOverClusterInvoker.doInvoke -...-> DubboInvoker.doInvoke -> ReferenceCountExchangeClient.request -> HeaderExchangeClient.request -> HeaderExchangeChannel.request -> AbstractPeer.send -> NettyChannel.send -> AbstractChannel.write -> Channels.write --back_to--> DubboInvoker.doInvoke -> DefaultFuture.get -> DefaultFuture.returnFromResponse -> throw new RemotingException

異常示例:
[java] view plain copy
  1. com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method triggerCheckedException in the service com.xxx.api.DemoService. Tried 1 times of the providers [192.168.1.101:20880] (1/1) from the registry 127.0.0.1:2181 on the consumer 192.168.1.101 using the dubbo version 3.1.9. Last error is: Failed to invoke remote method: triggerCheckedException, provider: dubbo://192.168.1.101:20880/com.xxx.api.DemoService?xxx, cause: java.io.IOException: Response data error, expect Throwable, but get {cause=(this Map), detailMessage=null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@23b84919}  
  2. java.io.IOException: Response data error, expect Throwable, but get {cause=(this Map), detailMessage=null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@23b84919}  
  3.     at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:94)  


邏輯2

[java] view plain copy
  1. // 在方法簽名上有聲明,直接拋出  
  2. try {  
  3.     Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());  
  4.     Class<?>[] exceptionClassses = method.getExceptionTypes();  
  5.     for (Class<?> exceptionClass : exceptionClassses) {  
  6.         if (exception.getClass().equals(exceptionClass)) {  
  7.             return result;  
  8.         }  
  9.     }  
  10. catch (NoSuchMethodException e) {  
  11.     return result;  
  12. }  
如果在provider端的api明確寫明拋出運行時異常,則會直接被拋出。

如果拋出了這種異常,但是consumer端又沒有這種異常,會發生什麼呢?
答案是和上面一樣,拋出RpcException。

因此如果consumer端不care這種異常,則不需要任何處理;
consumer端有這種異常(路徑要完全一致,包名+類名),則不需要任何處理;
沒有這種異常,又想進行處理,則需要引入這個異常進行處理(方法有多種,比如升級api,或引入/升級異常所在的包)。

邏輯3

[java] view plain copy
  1. // 異常類和接口類在同一jar包裏,直接拋出  
  2. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());  
  3. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());  
  4. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){  
  5.     return result;  
  6. }  
如果異常類和接口類在同一個jar包中,直接拋出。


邏輯4

[java] view plain copy
  1. // 是JDK自帶的異常,直接拋出  
  2. String className = exception.getClass().getName();  
  3. if (className.startsWith("java.") || className.startsWith("javax.")) {  
  4.     return result;  
  5. }  
以java.或javax.開頭的異常直接拋出。

邏輯5

[java] view plain copy
  1. // 是Dubbo本身的異常,直接拋出  
  2. if (exception instanceof RpcException) {  
  3.     return result;  
  4. }  
dubbo自身的異常,直接拋出。

邏輯6

[java] view plain copy
  1. // 否則,包裝成RuntimeException拋給客戶端  
  2. return new RpcResult(new RuntimeException(StringUtils.toString(exception)));  
不滿足上述條件,會做toString處理並被封裝成RuntimeException拋出。

核心思想

盡力避免反序列化時失敗(只有在jdk版本或api版本不一致時纔可能發生)。

如何正確捕獲業務異常

瞭解了ExceptionFilter,解決上面提到的問題就很簡單了。
有多種方法可以解決這個問題,每種都有優缺點,這裏不做詳細分析,僅列出供參考:
1. 將該異常的包名以"java.或者"javax. " 開頭
2. 使用受檢異常(繼承Exception)
3. 不用異常,使用錯誤碼
4. 把異常放到provider-api的jar包中
5. 判斷異常message是否以XxxException.class.getName()開頭(其中XxxException是自定義的業務異常)
6. provider實現GenericService接口
7. provider的api明確寫明throws XxxException,發佈provider(其中XxxException是自定義的業務異常)
8. 實現dubbo的filter,自定義provider的異常處理邏輯(方法可參考之前的文章給dubbo接口添加白名單——dubbo Filter的使用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章