dubbo异常处理(二)-浅谈dubbo的ExceptionFilter异常处理

背景

我们的项目使用了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.  * 

     

  38.  * 功能: 
  39.  * 
       
    1.  * 
    2. 不期望的异常打ERROR日志(Provider端)
       
    3.  *     不期望的日志即是,没有的接口上声明的Unchecked异常。 
    4.  * 
    5. 异常不在API包中,则Wrap一层RuntimeException。
       
    6.  *     RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。 
    7.  * 
     
  40.  *  
  41.  * @author william.liangf 
  42.  * @author ding.lid 
  43.  */  
  44. @Activate(group = Constants.PROVIDER)  
  45. public class ExceptionFilter implements Filter {  
  46.   
  47.     private final Logger logger;  
  48.       
  49.     public ExceptionFilter() {  
  50.         this(LoggerFactory.getLogger(ExceptionFilter.class));  
  51.     }  
  52.       
  53.     public ExceptionFilter(Logger logger) {  
  54.         this.logger = logger;  
  55.     }  
  56.       
  57.     public Result invoke(Invoker invoker, Invocation invocation) throws RpcException {  
  58.         try {  
  59.             Result result = invoker.invoke(invocation);  
  60.             if (result.hasException() && GenericService.class != invoker.getInterface()) {  
  61.                 try {  
  62.                     Throwable exception = result.getException();  
  63.   
  64.                     // 如果是checked异常,直接抛出  
  65.                     if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {  
  66.                         return result;  
  67.                     }  
  68.                     // 在方法签名上有声明,直接抛出  
  69.                     try {  
  70.                         Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());  
  71.                         Class[] exceptionClassses = method.getExceptionTypes();  
  72.                         for (Class exceptionClass : exceptionClassses) {  
  73.                             if (exception.getClass().equals(exceptionClass)) {  
  74.                                 return result;  
  75.                             }  
  76.                         }  
  77.                     } catch (NoSuchMethodException e) {  
  78.                         return result;  
  79.                     }  
  80.   
  81.                     // 未在方法签名上定义的异常,在服务器端打印ERROR日志  
  82.                     logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  83.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  84.                             + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);  
  85.   
  86.                     // 异常类和接口类在同一jar包里,直接抛出  
  87.                     String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());  
  88.                     String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());  
  89.                     if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){  
  90.                         return result;  
  91.                     }  
  92.                     // 是JDK自带的异常,直接抛出  
  93.                     String className = exception.getClass().getName();  
  94.                     if (className.startsWith("java.") || className.startsWith("javax.")) {  
  95.                         return result;  
  96.                     }  
  97.                     // 是Dubbo本身的异常,直接抛出  
  98.                     if (exception instanceof RpcException) {  
  99.                         return result;  
  100.                     }  
  101.   
  102.                     // 否则,包装成RuntimeException抛给客户端  
  103.                     return new RpcResult(new RuntimeException(StringUtils.toString(exception)));  
  104.                 } catch (Throwable e) {  
  105.                     logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()  
  106.                             + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  107.                             + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  108.                     return result;  
  109.                 }  
  110.             }  
  111.             return result;  
  112.         } catch (RuntimeException e) {  
  113.             logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()  
  114.                     + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()  
  115.                     + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);  
  116.             throw e;  
  117.         }  
  118.     }  
  119.   
  120. }  

代码分析

按逻辑顺序进行分析,满足其中一个即返回,不再继续执行判断。

逻辑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(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. }  

  23. 调用栈:
    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的使用)

    你可能感兴趣的:(javaWeb开发,dubbo,异常处理)