背景
我们的项目使用了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;
}
}
}
代码分析
按逻辑顺序进行分析,满足其中一个即返回,不再继续执行判断。
逻辑0
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(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,或引入/升级异常所在的包)。
逻辑3
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是自定义的业务异常)
8. 实现dubbo的filter,自定义provider的异常处理逻辑(方法可参考之前的文章 给dubbo接口添加白名单——dubbo Filter的使用)