dubbo的ExceptionFilter异常处理

200 阅读6分钟

转载自:blog.csdn.net/mj158518/ar…

背景

我们的项目使用了 dubbo进行不同系统之间的调用。

每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException)。

全局的异常处理会根据不同的异常类型进行不同的处理。

最近我们发现,某个系统调用dubbo请求,provider端(服务提供方)抛出了自定义的业务异常,但consumer端(服务消费方)拿到的并不是自定义的业务异常。

这是为什么呢?还需要从 dubbo的ExceptionFilter说起。

ExceptionFilter

如果Dubbo的  provider端 抛出异常(Throwable),则会被 provider端 的ExceptionFilter拦截到,执行以下invoke方法:\


  

    

     

    

    

     

      /*
     

    

    

     

    

    

     

       * Copyright 1999-2011 Alibaba Group.
     

    

    

     

    

    

     

       *  
     

    

    

     

    

    

     

       * Licensed under the Apache License, Version 2.0 (the "License");
     

    

    

     

    

    

     

       * you may not use this file except in compliance with the License.
     

    

    

     

    

    

     

       * You may obtain a copy of the License at
     

    

    

     

    

    

     

       *  
     

    

    

     

    

    

     

       *      http://www.apache.org/licenses/LICENSE-2.0
     

    

    

     

    

    

     

       *  
     

    

    

     

    

    

     

       * Unless required by applicable law or agreed to in writing, software
     

    

    

     

    

    

     

       * distributed under the License is distributed on an "AS IS" BASIS,
     

    

    

     

    

    

     

       * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     

    

    

     

    

    

     

       * See the License for the specific language governing permissions and
     

    

    

     

    

    

     

       * limitations under the License.
     

    

    

     

    

    

     

       */
     

    

    

     

    

    

     

      package com.alibaba.dubbo.rpc.filter;
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     

      import java.lang.reflect.Method;
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.Constants;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.extension.Activate;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.logger.Logger;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.logger.LoggerFactory;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.utils.ReflectUtils;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.common.utils.StringUtils;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.Filter;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.Invocation;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.Invoker;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.Result;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.RpcContext;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.RpcException;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.RpcResult;
     

    

    

     

    

    

     

      import com.alibaba.dubbo.rpc.service.GenericService;
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     

      /**
     

    

    

     

    

    

     

       * ExceptionInvokerFilter
     

    

    

     

    

    

     

       * <p>
     

    

    

     

    

    

     

       * 功能:
     

    

    

     

    

    

     

       * <ol>
     

    

    

     

    

    

     

       * <li>不期望的异常打ERROR日志(Provider端)<br>
     

    

    

     

    

    

     

       *     不期望的日志即是,没有的接口上声明的Unchecked异常。
     

    

    

     

    

    

     

       * <li>异常不在API包中,则Wrap一层RuntimeException。<br>
     

    

    

     

    

    

     

       *     RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。
     

    

    

     

    

    

     

       * </ol>
     

    

    

     

    

    

     

       * 
     

    

    

     

    

    

     

       * @author william.liangf
     

    

    

     

    

    

     

       * @author ding.lid
     

    

    

     

    

    

     

       */
     

    

    

     

    

    

     

      @Activate(group = Constants.PROVIDER)
     

    

    

     

    

    

     

      public 
      class ExceptionFilter implements Filter {
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     
    
      private 
      final Logger logger;
     

    

    

     

    

    

     
    
     

    

    

     

    

    

     
    
      public ExceptionFilter() {
     

    

    

     

    

    

     
        
      this(LoggerFactory.getLogger(ExceptionFilter.class));
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
    
     

    

    

     

    

    

     
    
      public ExceptionFilter(Logger logger) {
     

    

    

     

    

    

     
        
      this.logger = logger;
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
    
     

    

    

     

    

    

     
    
      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;
     

    

    

     

    

    

     

              }
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     

      }
     

    

  

代码分析

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

\

逻辑0


  

    

     

    

    

     

      if (result.hasException() && GenericService.class != invoker.getInterface()) {
     

    

    

     

    

    

     
    
      //...
     

    

    

     

    

    

     

      }
     

    

    

     

    

    

     

      return result;
     

    

  

调用结果有异常且未实现GenericService接口,进入后续判断逻辑,否则直接返回结果。


  

    

     

    

    

     

      /**
     

    

    

     

    

    

     

       * 通用服务接口
     

    

    

     

    

    

     

       * 
     

    

    

     

    

    

     

       * @author william.liangf
     

    

    

     

    

    

     

       * @export
     

    

    

     

    

    

     

       */
     

    

    

     

    

    

     

      public 
      interface GenericService {
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     
    
      /**
     

    

    

     

    

    

     

           * 泛化调用
     

    

    

     

    

    

     

           * 
     

    

    

     

    

    

     

           * @param method 方法名,如:findPerson,如果有重载方法,需带上参数列表,如:findPerson(java.lang.String)
     

    

    

     

    

    

     

           * @param parameterTypes 参数类型
     

    

    

     

    

    

     

           * @param args 参数列表
     

    

    

     

    

    

     

           * @return 返回值
     

    

    

     

    

    

     

           * @throws Throwable 方法抛出的异常
     

    

    

     

    

    

     

           */
     

    

    

     

    

    

     

          Object $invoke(String method, String[] parameterTypes, Object[] args) 
      throws GenericException;
     

    

    

     

    

    

     
 
     

    

    

     

    

    

     

      }
     

    

  

泛接口实现方式主要用于服务器端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的远程服务Mock框架,可通过实现GenericService接口处理所有服务请求。
不适用于此场景,不在此处探讨。

\

逻辑1


  

    

     

    

    

     

      // 如果是checked异常,直接抛出
     

    

    

     

    

    

     

      if (! (exception 
      instanceof RuntimeException) && (exception 
      instanceof Exception)) {
     

    

    

     

    

    

     
    
      return result;
     

    

    

     

    

    

     

      }
     

    

  

不是RuntimeException类型的异常,并且是受检异常(继承Exception),直接抛出。
provider端想抛出受检异常,必须在api上明确写明抛出受检异常;consumer端如果要处理受检异常,也必须使用明确写明抛出受检异常的api。
provider端api新增 自定义的 受检异常, 所有的 consumer端api都必须升级,同时修改代码,否则无法处理这个特定异常。


consumer端DecodeableRpcResult的decode方法会对异常进行处理

\

此处会抛出IOException,上层catch后会做toString处理,放到mErrorMsg属性中:


  

    

     

    

    

     

      try {
     

    

    

     

    

    

     

          decode(channel, inputStream);
     

    

    

     

    

    

     

      } 
      catch (Throwable e) {
     

    

    

     

    

    

     
    
      if (log.isWarnEnabled()) {
     

    

    

     

    

    

     

              log.warn(
      "Decode rpc result failed: " + e.getMessage(), e);
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     

          response.setStatus(Response.CLIENT_ERROR);
     

    

    

     

    

    

     

          response.setErrorMessage(StringUtils.toString(e));
     

    

    

     

    

    

     

      } 
      finally {
     

    

    

     

    

    

     

          hasDecoded = 
      true;
     

    

    

     

    

    

     

      }
     

    

  


DefaultFuture判断请求返回的结果,最后抛出RemotingException:


  

    

     

    

    

     

      private Object returnFromResponse() throws RemotingException {
     

    

    

     

    

    

     

          Response res = response;
     

    

    

     

    

    

     
    
      if (res == 
      null) {
     

    

    

     

    

    

     
        
      throw 
      new IllegalStateException(
      "response cannot be null");
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
    
      if (res.getStatus() == Response.OK) {
     

    

    

     

    

    

     
        
      return res.getResult();
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
    
      if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
     

    

    

     

    

    

     
        
      throw 
      new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     
    
      throw 
      new RemotingException(channel, res.getErrorMessage());
     

    

    

     

    

    

     

      }
     

    

  


DubboInvoker捕获RemotingException,抛出RpcException:


  

    

     

    

    

     

      try {
     

    

    

     

    

    

     
    
      boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
     

    

    

     

    

    

     
    
      boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
     

    

    

     

    

    

     
    
      int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY,Constants.DEFAULT_TIMEOUT);
     

    

    

     

    

    

     
    
      if (isOneway) {
     

    

    

     

    

    

     
        
      boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, 
      false);
     

    

    

     

    

    

     

              currentClient.send(inv, isSent);
     

    

    

     

    

    

     

              RpcContext.getContext().setFuture(
      null);
     

    

    

     

    

    

     
        
      return 
      new RpcResult();
     

    

    

     

    

    

     

          } 
      else 
      if (isAsync) {
     

    

    

     

    

    

     

              ResponseFuture future = currentClient.request(inv, timeout) ;
     

    

    

     

    

    

     

              RpcContext.getContext().setFuture(
      new FutureAdapter<Object>(future));
     

    

    

     

    

    

     
        
      return 
      new RpcResult();
     

    

    

     

    

    

     

          } 
      else {
     

    

    

     

    

    

     

              RpcContext.getContext().setFuture(
      null);
     

    

    

     

    

    

     
        
      return (Result) currentClient.request(inv, timeout).get();
     

    

    

     

    

    

     

          }
     

    

    

     

    

    

     

      } 
      catch (TimeoutException e) {
     

    

    

     

    

    

     
    
      throw 
      new RpcException(RpcException.TIMEOUT_EXCEPTION, 
      "Invoke remote method timeout. method: " + invocation.getMethodName() + 
      ", provider: " + getUrl() + 
      ", cause: " + e.getMessage(), e);
     

    

    

     

    

    

     

      } 
      catch (RemotingException e) {
     

    

    

     

    

    

     
    
      throw 
      new RpcException(RpcException.NETWORK_EXCEPTION, 
      "Failed to invoke remote method: " + invocation.getMethodName() + 
      ", provider: " + getUrl() + 
      ", cause: " + e.getMessage(), e);
     

    

    

     

    

    

     

      }
     

    

  


调用栈:
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

\

异常示例:


  

    

     

    

    

     

      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}
     

    

    

     

    

    

     

      java.io.IOException: Response data error, expect Throwable, but get {cause=(
      this Map), detailMessage=
      null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@
      23b84919}
     

    

    

     

    

    

     

      	at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:
      94)
     

    

  

\

逻辑2


  

    

     

    

    

     

      // 在方法签名上有声明,直接抛出
     

    

    

     

    

    

     

      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;
     

    

    

     

    

    

     

      }
     

    

  

如果在provider端的api明确写明抛出运行时异常,则会直接被抛出。

\

如果抛出了这种异常,但是consumer端又没有这种异常,会发生什么呢?
答案是和上面一样,抛出RpcException。

因此如果consumer端不care这种异常,则不需要任何处理;
consumer端有这种异常(路径要完全一致,包名+类名),则不需要任何处理;
没有这种异常,又想进行处理,则需要引入这个异常进行处理(方法有多种,比如升级api,或引入/升级异常所在的包)。

\

逻辑3


  

    

     

    

    

     

      // 异常类和接口类在同一jar包里,直接抛出
     

    

    

     

    

    

     

      String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
     

    

    

     

    

    

     

      String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
     

    

    

     

    

    

     

      if (serviceFile == 
      null || exceptionFile == 
      null || serviceFile.equals(exceptionFile)){
     

    

    

     

    

    

     
    
      return result;
     

    

    

     

    

    

     

      }
     

    

  

如果异常类和接口类在同一个jar包中,直接抛出。

\

\

逻辑4


  

    

     

    

    

     

      // 是JDK自带的异常,直接抛出
     

    

    

     

    

    

     

      String className = exception.getClass().getName();
     

    

    

     

    

    

     

      if (className.startsWith(
      "java.") || className.startsWith(
      "javax.")) {
     

    

    

     

    

    

     
    
      return result;
     

    

    

     

    

    

     

      }
     

    

  

以java.或javax.开头的异常直接抛出。

\

逻辑5


  

    

     

    

    

     

      // 是Dubbo本身的异常,直接抛出
     

    

    

     

    

    

     

      if (exception 
      instanceof RpcException) {
     

    

    

     

    

    

     
    
      return result;
     

    

    

     

    

    

     

      }
     

    

  

dubbo自身的异常,直接抛出。

\

逻辑6


  

    

     

    

    

     

      // 否则,包装成RuntimeException抛给客户端
     

    

    

     

    

    

     

      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的使用


\