Dubbo異常處理機制以及應用


背景

  我們的項目使用了Dubbo進行不同系統服務間的調用,當服務端發生異常時,我們希望把異常傳遞給消費端,由消費端對異常進行捕獲並處理。但在實際使用中,發現以往的異常處理在dubbo服務中並不能奏效。例如,自定義異常類BizException繼承RuntimeException,當服務端拋出這個異常時,消費端並不能捕獲它。

//自定義BizException
CLass BizException extends RuntimeException{
...
}

//服務端提供方法
public void provider(){
   ...//拋出BizException
}

//消費端
public void consumer(){
try{
    provider();//dubbo調用provider方法
}
catch(BizException){
      ...//捕獲異常處理
}

}

 

Dubbo異常處理機制

  當Dubbo的provider端拋出異常(Throwable),會被provider端的ExceptionFilter攔截到,執行以下invoke方法: (源碼位置:com.alibaba.dubbo.rpc.filter.ExceptionFilter)

/*
 * 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;
        }
    }

}
ExceptionFilter

  1、調用結果有異常且未實現GenericService接口,進入后續判斷邏輯,否則直接返回結果

  2、不是RuntimeException類型的異常,並且是受檢Checked異常(繼承Exception),直接拋出

  3、在方法簽名上聲明了異常,直接拋出

  4、未在方法簽名上定義的異常,在服務端打印ERROR日志

  5、異常類和接口類在同一jar包里,直接拋出

  6、JDK自帶的異常(以java.或javax.開頭)直接拋出

  7、Dubbo本身的異常,直接拋出(RpcException)

  8、不滿足上述條件時會被包裝成RuntimeException拋給consumer端

  由此可見,我們定義在公共包中的BizException在provider端拋出的時候會被Dubbo的異常過濾包裝成RuntimeException拋給consumer端,而consumer端捕獲的是BizException,所以並不會捕獲到它。

解決方法

  1、provider端實現GenericService接口:

  缺點:需要自己實現invoke方法。

  2、在provider端方法簽名上顯式聲明異常:

public void providerFunctionA(String str) throws BizException

  缺點:代碼侵入性高,基本所有接口都要寫。

  3、將自定義異常的包名以java.或javax.開頭

package java.common.core
Class BizException extends RuntimeException{
...
}

  缺點:不滿足項目命名規范。

  4、自定義異常聲明為受檢異常:

//繼承Exception
Class BizException1 extends Exception{
...
}
Class BizException2 extends Exception{
...
}
//服務端方法中拋出
function() throws BizException1,BizException2{
...
}

  缺點:項目規模擴大后,受檢異常過多會難以處理且同2情況代碼侵入性高。

  5、將異常類與接口類放在同一個jar包中。

最終方案

  這里采用方案5(將異常類與接口類放在同一個jar包中)作為最終方案,注意使用過程中應避免存在鏈式調用。

      1、在公共包中自定義父類異常,繼承RuntimeException

package com.xxx.common.core

Class BizException extends RuntimeExceptio{
    ...
}

      2、各個provider端定義子類異常,繼承BizException並與接口放在同個工程中

//放於服務端facade接口工程中
package com.xxx.user.facade.exceptions
//子類異常
Class UserBizException extends BizException{
...
}

      3、各個provider端方法拋出自己模塊的子類異常

package com.xxx.user.facade.service.impl

Class ProviderServiceImpl{
    //服務提供方法實現類
    public void provider(){
         ...
         throw UserBizException.XXXXXX;//拋出子類異常
         ...
     }

}

      4、consumer端統一捕獲父類異常BizException

//消費端方法
void consumer(){
  try{
     providerService.provider();//調用服務端方法
  }
  catch(BizException e){
     handleException();//異常捕獲處理
  }
}

  至此,provider端拋出的自定義異常可傳遞到consumer端被捕獲處理。但是在使用中我們還會發現一個問題,那就是當provider端拋出其他的非自定義異常(例如Dubbo自身的RpcException、系統其它運行時異常等)時,consumer端並沒能捕獲到進行后續處理。所以,這里添加一個攔截器來確保各個provider端拋出的異常都是自己模塊下的自定義異常。

      5、新增一個aop切面

<bean id="userBizExceptionFilter" class="com.xxx.user.facade.exceptions.UserBizExceptionFilter" />
<aop:config>
    <aop:aspect ref="userBizExceptionFilter">
        <aop:pointcut id="userBizExceptionAspect"
              expression="execution(* com.xxx.user.facade.service..*.*(..))"/>
        <aop:after-throwing pointcut-ref="userBizExceptionAspect" 
              method="afterThrowing" throwing="e"/>
    </aop:aspect>
</aop:config>

      6、攔截器,發生其他異常時封裝成模塊自定義異常拋出

package com.xxx.user.facade.exceptions

Class UserBizExceptionFilter implements ThrowAdvice{
   public void afterThrowing(Exception e) throws Throwable{
      //業務異常時直接拋出
      if(e instanceof UserBizException){
           throw (UserBizException) e;
       }
       //dubbo服務異常時封裝成子類異常拋出
       else if (e instanceof RpcException){
           throw UserBizException.XXXX;
       }
       //其它系統異常時封裝成子類異常拋出
       else{
           throw UserBizException.XXXX;
       }
   }

}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM