背景

我们的项目使用了dubbo进行不同系统之间的调用。
每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException)。
全局的异常处理会根据不同的异常类型进行不同的处理。
最近我们发现,某个系统调用dubbo请求,provider端(服务提供方)抛出了自定义的业务异常,但consumer端(服务消费方)拿到的并不是自定义的业务异常。
这是为什么呢?还需要从dubbo的ExceptionFilter说起。

ExceptionFilter

如果Dubbo的 provider端 抛出异常(Throwable),则会被 provider端 的ExceptionFilter拦截到,执行以下invoke方法:
  1. /*
  2. * Copyright 1999-2011 Alibaba Group.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. *      http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.alibaba.dubbo.rpc.filter;
  17. import java.lang.reflect.Method;
  18. import com.alibaba.dubbo.common.Constants;
  19. import com.alibaba.dubbo.common.extension.Activate;
  20. import com.alibaba.dubbo.common.logger.Logger;
  21. import com.alibaba.dubbo.common.logger.LoggerFactory;
  22. import com.alibaba.dubbo.common.utils.ReflectUtils;
  23. import com.alibaba.dubbo.common.utils.StringUtils;
  24. import com.alibaba.dubbo.rpc.Filter;
  25. import com.alibaba.dubbo.rpc.Invocation;
  26. import com.alibaba.dubbo.rpc.Invoker;
  27. import com.alibaba.dubbo.rpc.Result;
  28. import com.alibaba.dubbo.rpc.RpcContext;
  29. import com.alibaba.dubbo.rpc.RpcException;
  30. import com.alibaba.dubbo.rpc.RpcResult;
  31. import com.alibaba.dubbo.rpc.service.GenericService;
  32. /**
  33. * ExceptionInvokerFilter
  34. * <p>
  35. * 功能:
  36. * <ol>
  37. * <li>不期望的异常打ERROR日志(Provider端)<br>
  38. *     不期望的日志即是,没有的接口上声明的Unchecked异常。
  39. * <li>异常不在API包中,则Wrap一层RuntimeException。<br>
  40. *     RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。
  41. * </ol>
  42. *
  43. * @author william.liangf
  44. * @author ding.lid
  45. */
  46. @Activate(group = Constants.PROVIDER)
  47. public class ExceptionFilter implements Filter {
  48. private final Logger logger;
  49. public ExceptionFilter() {
  50. this(LoggerFactory.getLogger(ExceptionFilter.class));
  51. }
  52. public ExceptionFilter(Logger logger) {
  53. this.logger = logger;
  54. }
  55. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  56. try {
  57. Result result = invoker.invoke(invocation);
  58. if (result.hasException() && GenericService.class != invoker.getInterface()) {
  59. try {
  60. Throwable exception = result.getException();
  61. // 如果是checked异常,直接抛出
  62. if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {
  63. return result;
  64. }
  65. // 在方法签名上有声明,直接抛出
  66. try {
  67. Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
  68. Class<?>[] exceptionClassses = method.getExceptionTypes();
  69. for (Class<?> exceptionClass : exceptionClassses) {
  70. if (exception.getClass().equals(exceptionClass)) {
  71. return result;
  72. }
  73. }
  74. } catch (NoSuchMethodException e) {
  75. return result;
  76. }
  77. // 未在方法签名上定义的异常,在服务器端打印ERROR日志
  78. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
  79. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  80. + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
  81. // 异常类和接口类在同一jar包里,直接抛出
  82. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
  83. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
  84. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
  85. return result;
  86. }
  87. // 是JDK自带的异常,直接抛出
  88. String className = exception.getClass().getName();
  89. if (className.startsWith("java.") || className.startsWith("javax.")) {
  90. return result;
  91. }
  92. // 是Dubbo本身的异常,直接抛出
  93. if (exception instanceof RpcException) {
  94. return result;
  95. }
  96. // 否则,包装成RuntimeException抛给客户端
  97. return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
  98. } catch (Throwable e) {
  99. logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
  100. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  101. + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  102. return result;
  103. }
  104. }
  105. return result;
  106. } catch (RuntimeException e) {
  107. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
  108. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  109. + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  110. throw e;
  111. }
  112. }
  113. }

代码分析

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

逻辑0

  1. if (result.hasException() && GenericService.class != invoker.getInterface()) {
  2. //...
  3. }
  4. return result;

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

  1. /**
  2. * 通用服务接口
  3. *
  4. * @author william.liangf
  5. * @export
  6. */
  7. public interface GenericService {
  8. /**
  9. * 泛化调用
  10. *
  11. * @param method 方法名,如:findPerson,如果有重载方法,需带上参数列表,如:findPerson(java.lang.String)
  12. * @param parameterTypes 参数类型
  13. * @param args 参数列表
  14. * @return 返回值
  15. * @throws Throwable 方法抛出的异常
  16. */
  17. Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
  18. }

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

 

逻辑1

  1. // 如果是checked异常,直接抛出
  2. if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {
  3. return result;
  4. }

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

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

此处会抛出IOException,上层catch后会做toString处理,放到mErrorMsg属性中:
  1. try {
  2. decode(channel, inputStream);
  3. } catch (Throwable e) {
  4. if (log.isWarnEnabled()) {
  5. log.warn("Decode rpc result failed: " + e.getMessage(), e);
  6. }
  7. response.setStatus(Response.CLIENT_ERROR);
  8. response.setErrorMessage(StringUtils.toString(e));
  9. } finally {
  10. hasDecoded = true;
  11. }

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

  1. private Object returnFromResponse() throws RemotingException {
  2. Response res = response;
  3. if (res == null) {
  4. throw new IllegalStateException("response cannot be null");
  5. }
  6. if (res.getStatus() == Response.OK) {
  7. return res.getResult();
  8. }
  9. if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
  10. throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
  11. }
  12. throw new RemotingException(channel, res.getErrorMessage());
  13. }

DubboInvoker捕获RemotingException,抛出RpcException:

  1. try {
  2. boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
  3. boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
  4. int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY,Constants.DEFAULT_TIMEOUT);
  5. if (isOneway) {
  6. boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
  7. currentClient.send(inv, isSent);
  8. RpcContext.getContext().setFuture(null);
  9. return new RpcResult();
  10. } else if (isAsync) {
  11. ResponseFuture future = currentClient.request(inv, timeout) ;
  12. RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
  13. return new RpcResult();
  14. } else {
  15. RpcContext.getContext().setFuture(null);
  16. return (Result) currentClient.request(inv, timeout).get();
  17. }
  18. } catch (TimeoutException e) {
  19. throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
  20. } catch (RemotingException e) {
  21. throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
  22. }

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

 
异常示例:
  1. 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}
  2. java.io.IOException: Response data error, expect Throwable, but get {cause=(this Map), detailMessage=null, suppressedExceptions=[], stackTrace=[Ljava.lang.StackTraceElement;@23b84919}
  3. at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:94)

逻辑2

  1. // 在方法签名上有声明,直接抛出
  2. try {
  3. Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
  4. Class<?>[] exceptionClassses = method.getExceptionTypes();
  5. for (Class<?> exceptionClass : exceptionClassses) {
  6. if (exception.getClass().equals(exceptionClass)) {
  7. return result;
  8. }
  9. }
  10. } catch (NoSuchMethodException e) {
  11. return result;
  12. }

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

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

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

逻辑3

  1. // 异常类和接口类在同一jar包里,直接抛出
  2. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
  3. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
  4. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
  5. return result;
  6. }

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

 

逻辑4

  1. // 是JDK自带的异常,直接抛出
  2. String className = exception.getClass().getName();
  3. if (className.startsWith("java.") || className.startsWith("javax.")) {
  4. return result;
  5. }

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

 

逻辑5

  1. // 是Dubbo本身的异常,直接抛出
  2. if (exception instanceof RpcException) {
  3. return result;
  4. }

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

 

逻辑6

  1. // 否则,包装成RuntimeException抛给客户端
  2. 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的使用

浅谈dubbo的ExceptionFilter异常处理的更多相关文章

  1. 浅谈dubbo服务

    Dubbo分布式服 推荐大家一个画图工具:https://www.processon.com/i/572d51efe4b0c3c74981ec14 1.Dubbo是一个分布式服务框架,致力于提供高性能 ...

  2. 浅谈微服务架构与服务治理的Eureka和Dubbo

    前言 本来计划周五+周末三天自驾游,谁知人算不如天算,周六恰逢台风来袭,湖州附近的景点全部关停,不得已只能周五玩完之后,于周六踩着台风的边缘逃回上海.周末过得如此艰难,这次就聊点务虚的话题,一是浅谈微 ...

  3. 浅谈Java的throw与throws

    转载:http://blog.csdn.net/luoweifu/article/details/10721543 我进行了一些加工,不是本人原创但比原博主要更完善~ 浅谈Java异常 以前虽然知道一 ...

  4. 浅谈WebService的版本兼容性设计

    在现在大型的项目或者软件开发中,一般都会有很多种终端, PC端比如Winform.WebForm,移动端,比如各种Native客户端(iOS, Android, WP),Html5等,我们要满足以上所 ...

  5. 浅谈Hybrid技术的设计与实现

    前言 浅谈Hybrid技术的设计与实现 浅谈Hybrid技术的设计与实现第二弹 浅谈Hybrid技术的设计与实现第三弹——落地篇 随着移动浪潮的兴起,各种APP层出不穷,极速的业务扩展提升了团队对开发 ...

  6. jsp内置对象浅谈

    jsp内置对象浅谈 | 浏览:1184 | 更新:2013-12-11 16:01 JSP内置对象:我们在使用JSP进行页面编程时可以直接使用而不需自己创建的一些Web容器已为用户创建好的JSP内置对 ...

  7. 【架构】浅谈web网站架构演变过程

    浅谈web网站架构演变过程   前言 我们以javaweb为例,来搭建一个简单的电商系统,看看这个系统可以如何一步步演变.   该系统具备的功能:   用户模块:用户注册和管理 商品模块:商品展示和管 ...

  8. [C#]6.0新特性浅谈

    原文:[C#]6.0新特性浅谈 C#6.0出来也有很长一段时间了,虽然新的特性和语法趋于稳定,但是对于大多数程序猿来说,想在工作中用上C#6.0估计还得等上不短的一段时间.所以现在再来聊一聊新版本带来 ...

  9. Android安全开发之启动私有组件漏洞浅谈

    0x00 私有组件浅谈 android应用中,如果某个组件对外导出,那么这个组件就是一个攻击面.很有可能就存在很多问题,因为攻击者可以以各种方式对该组件进行测试攻击.但是开发者不一定所有的安全问题都能 ...

随机推荐

  1. SpringBoot -- 计划任务

    从Spring 3.1 开始,计划任务在Spring中的实现变得异常的简单.首先通过在配置类注解@EnableScheduling 来开启对计划任务的支持,然后再执行集合任务的方法上注解@Schedu ...

  2. [亲测!超级简单] Centos 安装Python3.6环境

    配置好Python3.6和pip3安装EPEL和IUS软件源 yum install epel-release -y yum install https://centos7.iuscommunity. ...

  3. python学习之老男孩python全栈第九期_day001知识点总结

    1. Python2与Python3的区别: Python2:源码不标准,混乱,重复代码太多: Python3:统一标准,去除重复代码. 编码方式: python2的默认编码方式为ASCII码:pyt ...

  4. 微信小程序 折叠效果

    <view class='help'> <view class='help_item'> <view class='title' data-index='1' catch ...

  5. 一张图看懂Mysql的join连接

    INNER JOIN:当两个表中都匹配时返回行. LEFT JOIN:返回左表中的所有行,即使右表中没有匹配项也是如此. RIGHT JOIN:返回右表中的所有行,即使左表中没有匹配项也是如此. FU ...

  6. Jquery 清除空白字符

    $.grep(“jQuery数组”, function(n) { return $.trim(n).length > 0; }); /*仅过滤空数组,不过滤相同数组*/

  7. Retrofit+RxJava(1)-在Android Studio中配置

    在build.gradle中添加 //加入retrolambda需要的plugin声明 apply plugin: 'me.tatarka.retrolambda' //retrolambda的编译路 ...

  8. Asp.Net Core混合使用cookie和JwtBearer认证方案

    自己有时捣鼓一些小型演示项目,服务端主要是提供Web Api功能.为了便于管理,需要在服务端加一些简单的MVC网页,用管理员身份登录,做一些简单的操作. 因此需要实现一个功能,在一个Asp.Net C ...

  9. h5页面调用摄像头(简易版)

    <input type="button" value="OpenVideo" id="btnOpenVideo" /> < ...

  10. python基础学习14----正则表达式

    正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符.及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑. 在python中正则表达式被封 ...