最近产线上出现了一个下游服务抛出的Error,传递到上游的问题。引发了大家对于Dubbo异常处理的讨论。
笔者翻看了一下Dubbo的源码,针对这个问题,梳理了一下 Dubbo是如何处理业务异常。
先说一下结论:Dubbo实际上不处理异常,只是做异常的传递。下游业务系统如果没有catch住自己内部系统的异常,经由Dubbo调用后,上游系统会收到同样的异常。
那么这一切是如何发生的呢?
下文基于Dubbo 2.6.5版本,从源码角度说明一下Dubbo的异常处理流程。
服务提供端捕获异常
服务提供端接收到消费端的调用请求后,一般情况下会经由以下调用链路
- ChannelEventRunnable.run
- DecodeHandler.received
- HeaderExchangeHandler.received
- HeaderExchangeHandler.handleRequest
这几步主要是解析上游请求报文。随后会进行Dubbo协议的处理
- DubboProtocol.reply
@Override
public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
Invocation inv = (Invocation) message;
Invoker<?> invoker = getInvoker(channel, inv);
// need to consider backward-compatibility if it's a callback
if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) {
String methodsStr = invoker.getUrl().getParameters().get("methods");
boolean hasMethod = false;
if (methodsStr == null || methodsStr.indexOf(",") == -1) {
hasMethod = inv.getMethodName().equals(methodsStr);
} else {
String[] methods = methodsStr.split(",");
for (String method : methods) {
if (inv.getMethodName().equals(method)) {
hasMethod = true;
break;
}
}
}
if (!hasMethod) {
logger.warn(new IllegalStateException("The methodName " + inv.getMethodName()
+ " not found in callback service interface ,invoke will be ignored."
+ " please update the api interface. url is:"
+ invoker.getUrl()) + " ,invocation is :" + inv);
return null;
}
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
return invoker.invoke(inv);
}
throw new RemotingException(channel, "Unsupported request: "
+ (message == null ? null : (message.getClass().getName() + ": " + message))
+ ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
}
这里的reply是匿名内部类的方法。其关键是invoker.invoke(inv);
这一句调用,此调用会触发一系列的filter链和listener链的调用,如 EchoFilter、ClassLoaderFilter、GenericFilter…ExceptionFilter。
其中最关键的,就是ExceptionFilter了。
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
Result result = invoker.invoke(invocation);
// $-- 不会处理GenericService类型invoker的异常
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = result.getException();
// directly throw if it's checked exception
// $-- check Exception直接抛出
if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
return result;
}
// directly throw if the exception appears in the signature
// $-- 如果异常已经在被调用方法中声明了,那么也直接抛出
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;
}
// for the exception not found in method's signature, print ERROR message in server's log.
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);
// directly throw if exception class and interface class are in the same jar file.
// $-- 如果异常类和接口类在同一个jar包中,那么也直接抛出异常
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return result;
}
// directly throw if it's JDK exception
// $-- JDK异常也直接抛出
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return result;
}
// directly throw if it's dubbo exception
// $-- Dubbo异常也直接抛出
if (exception instanceof RpcException) {
return result;
}
// otherwise, wrap with RuntimeException and throw back to the client
// $-- 其他类型的异常(一般是消费者端不存在的自定义异常),将异常转换为字符串,并包装成一个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会处理一些特殊的异常,其主要目的是为了避免下游系统抛出的自定义异常,上游系统没有,从而导致上游出现反序列化问题。
你可能也注意到了,ExceptionFilter中,调用 invoker.invoke 方法的返回结果是一个Result类型的变量,而这个Result类型的变量实际上已经捕获了相应的异常,并封装到了其对象内部。
那么此处的Result对象是如何构造的呢?让我们继续往下看调用链路。
- InvokerWrapper.invoke
- DelegateProviderMetaDataInvoker.invoke
- AbstractProxyInvoker.invoke
@Override
public Result invoke(Invocation invocation) throws RpcException {
try {
return new RpcResult(doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()));
} catch (InvocationTargetException e) {
return new RpcResult(e.getTargetException());
} catch (Throwable e) {
throw new RpcException("Failed to invoke remote proxy method " + invocation.getMethodName() + " to " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
在AbstractProxyInvoker.invoke方法中,我们看到,Result对象实际上是在这里产生的。
doInvoke方法应该会调用相应的实现方法,如果出现异常了,那么应该就会被这里的 Throwable 捕获到。因此Dubbo不会抛异常。
但问题真的是这样么?
我们注意到,其实doInvoke方法还会依次调用如下方法:
- JavassistProxyFactory.doInvoke
- Wrapper1.invokeMethod(Wrapper1.java)
- UserServiceImpl.getNameById(真正的目标方法)
可以看到,中间通过了JavassistProxyFactory调用了一层。我们知道Javassist会帮助Dubbo生成一些代码。那么会不会在这生成的代码中,又做了一些骚操作呢?
在JavassistProxyFactory的getInvoker方法中,我们可以看到:
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
doInvoke方法,实际上是由wrapper来执行的。而这个wrapper呢,是通过Wrapper.getWrapper来动态生成的代码。因此在实际调用运行时进行debug的话,你是看不到这代码执行。
public static Wrapper getWrapper(Class<?> c) {
while (ClassGenerator.isDynamicClass(c)) // can not wrapper on dynamic class.
c = c.getSuperclass();
if (c == Object.class)
return OBJECT_WRAPPER;
Wrapper ret = WRAPPER_MAP.get(c);
if (ret == null) {
ret = makeWrapper(c);
WRAPPER_MAP.put(c, ret);
}
return ret;
}
上述代码中的 makeWrapper方法,就是动态生成代码的代码。(太长了,这里就不展示了)
如果在下游系统启动的时候,在此处相应位置加上debug,就可以看到相应的动态生成的代码了。这里我截取了一下的 invokeMethod 动态生成的实际方法。
public Object invokeMethod(Object o, String n, Class[] p, Object[] v) throws java.lang.reflect.InvocationTargetException {
cn.hewie.hservice.facade.impl.UserServiceImpl w;
try {
w = ((cn.hewie.hservice.facade.impl.UserServiceImpl) );
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
try {
if ("getNameById".equals() && .length == 1) {
return ($w) w.getNameById((java.lang.String) [0]);
}
} catch (Throwable e) {
throw new java.lang.reflect.InvocationTargetException(e);
}
throw new com.alibaba.dubbo.common.bytecode.NoSuchMethodException("Not found method \"" + + "\" in class cn.hewie.hservice.facade.impl.UserServiceImpl.");
}
哈,在这里我们发现,通过try和catch方法,将我们实际调用的getNameById方法包了一层。如果getNameById方法出现异常的话,会被包装成InvocationTargetException。
如果此处抛出了异常,那么在AbstractProxyInvoker的invoke方法处,会捕获异常,然后封装成相应的RpcResult。
@Override
public Result invoke(Invocation invocation) throws RpcException {
try {
return new RpcResult(doInvoke(proxy, invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments()));
} catch (InvocationTargetException e) {
return new RpcResult(e.getTargetException());
} catch (Throwable e) {
throw new RpcException("Failed to invoke remote proxy method " + invocation.getMethodName() + " to " + getUrl() + ", cause: " + e.getMessage(), e);
}
}
再往上,ExceptionFilter拿到结果了之后,会判断是哪一种类型的异常。如果是Jdk的Error,如:NoClassDefError,最终会被以下代码块捕获,从而打印出日志中的那段"Got unchecked and undeclared…"
// for the exception not found in method's signature, print ERROR message in server's log.
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);
// directly throw if exception class and interface class are in the same jar file.
// $-- 如果异常类和接口类在同一个jar包中,那么也直接抛出异常
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return result;
}
// directly throw if it's JDK exception
// $-- JDK异常也直接抛出
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return result;
}
上述介绍了服务提供端的异常捕获流程。总结下来就是,Dubbo将业务异常包装在RpcResult中,然后返回给了上游。
那么上游消费端是如何解析这个异常的呢?
消费端重放异常
当服务提供端抛出了一个业务异常时,Dubbo并不会报错,而是将这个异常包裹到一个 RpcResult 对象中了。
消费端拿到这个 RpcResult 之后,按照调用的链路顺序,一层一层向上剥开处理。(这个过程可以参考笔者的另一篇博文:Dubbo服务调用的过程)
最终处理异常,是在 InvokerInvocationHandler
这个类中。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
return invoker.invoke(new RpcInvocation(method, args)).recreate();
}
InvokerInvocationHandler
这个类的 invoke 方法,调用得到结果之后,会调用 recreate() 这个方法。
就是这个不起眼的方法,进行了异常的处理。它的逻辑如下:
@Override
public Object recreate() throws Throwable {
if (exception != null) {
throw exception;
}
return result;
}
如果 RpcResult 中存在异常,就直接将异常抛出。这样消费端就得到了和业务端一样的业务异常了。
附
关于应用域的异常处理
前文笔者提到,产线出现了下游异常抛出到上游的问题。其实在业务开发中,我们的代码中,在对外提供的facade服务层,一般都进行了 try … catch的异常处理。
之所以会出现这个问题,是因为下游服务抛出的是一个 Error,而我们的上下游应用代码中捕获的都是 Exception。因此没有兜住,导致一个Error报错,将上游也干趴了。从而导致产线数据和业务问题。
真正的业务应用中,上游对下游的调用结果,可能并没有“那么关心”。也许下游的某个服务挂了/执行出现了异常,对上游也没有很大的影响。此时为了保证业务的正常,应当采取相应的降级措施。
针对这一次的问题,笔者建议对于这种“不影响主流程”的下游服务调用,可以catch一个Throwable异常。这样才能做到真正的降级!
异常日志
[DubboServerHandler-192.168.10.213:20886-thread-3] ERROR - com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:85) - [DUBBO] Got unchecked and undeclared exception which called by 192.168.10.213. service: cn.hewie.hservice.facade.UserService, method: getNameById, exception: java.lang.NoClassDefFoundError: Could not initialize class cn.hewie.hservice.facade.MockNoClassDefFoundError, dubbo version: 2.6.5, current host: 192.168.10.213
java.lang.NoClassDefFoundError: Could not initialize class cn.hewie.hservice.facade.MockNoClassDefFoundError
at cn.hewie.hservice.facade.impl.UserServiceImpl.getNameById(UserServiceImpl.java:20) ~[classes/:?]
at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) ~[dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory.doInvoke(JavassistProxyFactory.java:47) ~[dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:76) ~[dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:52) ~[dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56) ~[dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:62) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.TimeoutFilter.invoke(TimeoutFilter.java:42) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter.invoke(TraceFilter.java:78) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.ContextFilter.invoke(ContextFilter.java:73) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:138) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.ClassLoaderFilter.invoke(ClassLoaderFilter.java:38) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.filter.EchoFilter.invoke(EchoFilter.java:38) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper.invoke(ProtocolFilterWrapper.java:72) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol.reply(DubboProtocol.java:104) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.handleRequest(HeaderExchangeHandler.java:96) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:173) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:51) [dubbo-2.6.5.jar:2.6.5]
at com.alibaba.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57) [dubbo-2.6.5.jar:2.6.5]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_171]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_171]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_171]