本文原地址:http://blog.csdn.net/mj158518/article/details/51228649
背景
我們的項目使用了dubbo進行不同系統之間的調用。
每個項目都有一個全局的異常處理,對於業務異常,我們會拋出自定義的業務異常(繼承RuntimeException)。
全局的異常處理會根據不同的異常類型進行不同的處理。
最近我們發現,某個系統調用dubbo請求,provider端(服務提供方)拋出了自定義的業務異常,但consumer端(服務消費方)拿到的並不是自定義的業務異常。
這是爲什麼呢?還需要從dubbo的ExceptionFilter說起。
ExceptionFilter
如果Dubbo的 provider端 拋出異常(Throwable),則會被 provider端 的ExceptionFilter攔截到,執行以下invoke方法:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@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();
-
-
-
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;
-
}
-
-
-
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);
-
-
-
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
-
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
-
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
-
return result;
-
}
-
-
String className = exception.getClass().getName();
-
if (className.startsWith("java.") || className.startsWith("javax.")) {
-
return result;
-
}
-
-
if (exception instanceof RpcException) {
-
return result;
-
}
-
-
-
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;
-
}
-
}
-
-
}
代碼分析
按邏輯順序進行分析,滿足其中一個即返回,不再繼續執行判斷。
-
if (result.hasException() && GenericService.class != invoker.getInterface()) {
-
-
}
-
return result;
調用結果有異常且未實現GenericService接口,進入後續判斷邏輯,否則直接返回結果。
-
-
-
-
-
-
-
public interface GenericService {
-
-
-
-
-
-
-
-
-
-
-
Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
-
-
}
泛接口實現方式主要用於服務器端沒有API接口及模型類元的情況,參數及返回值中的所有POJO均用Map表示,通常用於框架集成,比如:實現一個通用的遠程服務Mock框架,可通過實現GenericService接口處理所有服務請求。
不適用於此場景,不在此處探討。
邏輯1
-
-
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:
-
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,或引入/升級異常所在的包)。
-
-
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
-
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
-
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
-
return result;
-
}
如果異常類和接口類在同一個jar包中,直接拋出。
邏輯4
-
-
String className = exception.getClass().getName();
-
if (className.startsWith("java.") || className.startsWith("javax.")) {
-
return result;
-
}
以java.或javax.開頭的異常直接拋出。
邏輯5
-
-
if (exception instanceof RpcException) {
-
return result;
-
}
dubbo自身的異常,直接拋出。
邏輯6
-
-
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是自定義的業務異常)