zoukankan      html  css  js  c++  java
  • dubbo的ExceptionFilter异常处理

    转载自:https://blog.csdn.net/mj158518/article/details/51228649

    背景

    我们的项目使用了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的使用


    世界上所有的不公平都是由于当事人能力不足造成的.
  • 相关阅读:
    替换所有的cell的右侧箭头
    (转载)iOS UILabel自定义行间距时获取高度
    UITableViewCell的separator分隔线设置失效
    tableview中在tableheaderView上放一个视图,第一次进入视图显示不正常,往下拉视图仍然不正常,往上拉视图正常
    Xcode打印frame id
    使用System Sound Services 播放音效(最简单,比较底层),调用AudioServicesPlaySystemSound()
    tcpdump
    /pentest/sniffers/hamster
    dsniff
    /usr/local/sbin/dsniff
  • 原文地址:https://www.cnblogs.com/javayida/p/13347070.html
Copyright © 2011-2022 走看看