浅谈dubbo的ExceptionFilter异常处理

背景

我们的项目使用了dubbo进行不同系统之间的调用。

每个项目都有一个全局的异常处理,对于业务异常,我们会抛出自定义的业务异常(继承RuntimeException)。

全局的异常处理会根据不同的异常类型进行不同的处理。

最近我们发现,某个系统调用dubbo请求,provider端(服务提供方)抛出了自定义的业务异常,但consumer端(服务消费方)拿到的并不是自定义的业务异常。

这是为什么呢?还需要从dubbo的ExceptionFilter说起。

ExceptionFilter

如果Dubbo的 provider端 抛出异常(Throwable),则会被 provider端 的ExceptionFilter拦截到,执行以下invoke方法:

/*
 * Copyright 1999-2011 Alibaba Group.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
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;

/**
 * ExceptionInvokerFilter
 * <p>
 * 功能:
 * <ol>
 * <li>不期望的异常打ERROR日志(Provider端)<br>
 *     不期望的日志即是,没有的接口上声明的Unchecked异常。
 * <li>异常不在API包中,则Wrap一层RuntimeException。<br>
 *     RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。
 * </ol>
 *
 * @author william.liangf
 * @author ding.lid
 */
@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();

                    // 如果是checked异常,直接抛出
                    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;
                    }

                    // 未在方法签名上定义的异常,在服务器端打印ERROR日志
                    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);

                    // 异常类和接口类在同一jar包里,直接抛出
                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
                        return result;
                    }
                    // 是JDK自带的异常,直接抛出
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return result;
                    }
                    // 是Dubbo本身的异常,直接抛出
                    if (exception instanceof RpcException) {
                        return result;
                    }

                    // 否则,包装成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;
        }
    }

}

代码分析

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

逻辑0

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

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

/**
 * 通用服务接口
 *
 * @author william.liangf
 * @export
 */
public interface GenericService {

    /**
     * 泛化调用
     *
     * @param method 方法名,如:findPerson,如果有重载方法,需带上参数列表,如:findPerson(java.lang.String)
     * @param parameterTypes 参数类型
     * @param args 参数列表
     * @return 返回值
     * @throws Throwable 方法抛出的异常
     */
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;

}

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

不适用于此场景,不在此处探讨。

逻辑1

// 如果是checked异常,直接抛出
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://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}
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

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

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

逻辑4

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

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

逻辑5

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

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

逻辑6

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

时间: 2024-10-08 07:23:50

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

浅谈Java throw, throws, try catch异常处理

1.throws关键字通常被应用在声明方法时,用来指定可能抛出的异常.多个异常可以使用逗号隔开.当在主函数中调用该方法时,如果发生异常,就会将异常抛给指定异常对象.如下面例子所示: public class Shoot {   创建类 static void pop() throws NegativeArraySizeException { //定义方法并抛出NegativeArraySizeException异常 int [] arr = new int[-3];//创建数组 } public

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

前言 本来计划周五+周末三天自驾游,谁知人算不如天算,周六恰逢台风来袭,湖州附近的景点全部关停,不得已只能周五玩完之后,于周六踩着台风的边缘逃回上海.周末过得如此艰难,这次就聊点务虚的话题,一是浅谈微服务的架构设计,二是聊聊微服务中广泛用于服务治理的Eureka与RPC框架Dubbo异同点. 一.微服务的架构设计 之所以想聊一下这个话题,主要有感于最近接触的两个新的微服务项目--两个项目的架构设计出自两个人之手,却不约而同的使用了相同的设计理念,项目结构非常类似.又想到就职于上家公司时接触到的项

浅谈java异常[Exception]

本文转自:focusJ 一. 异常的定义 在<java编程思想>中这样定义 异常:阻止当前方法或作用域继续执行的问题.虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常.绝对一点说异常就是某种意义上的错误,就是问题,它可能会导致程序失败.之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常的情况,请注意. 记得当初学习java的时候,异常总是搞不太清楚,不知道这个异常是什么意思,为什么会有这个机制?但是随着知识的积累逐渐也

浅谈高并发架构

本篇文章主要是浅谈一些高并发的方案,指出一个大致方向,如果有需要优化提高系统性能,可以从以下方法中找出合适的使用. 随着淘宝.京东.唯品会等很多电商的出现,所谓互联网公司也就经常听到了,这些互联网公司给我们的第一印象,用户活跃交易量大.为了给用户一个好的交互体验,我们需要根据具体的业务场景来设计适合自己的高并发处理方案.服务器的架构我们在网上也看到过很多文档描述,像美团的火热.饿了么的崛起都有提到服务器的架构演变,基本都是从相对单一到集群,再到分布式服务.从一开始交易量小知名度低软件开发工期紧张

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

浅谈web网站架构演变过程 前言 我们以javaweb为例,来搭建一个简单的电商系统,看看这个系统可以如何一步步演变. 该系统具备的功能: 用户模块:用户注册和管理 商品模块:商品展示和管理 交易模块:创建交易和管理 阶段一.单机构建网站 网站的初期,我们经常会在单机上跑我们所有的程序和软件.此时我们使用一个容器,如tomcat.jetty.jboos,然后直接使用JSP/servlet技术,或者使用一些开源的框架如maven+spring+struct+hibernate.maven+spri

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

0x00 私有组件浅谈 android应用中,如果某个组件对外导出,那么这个组件就是一个攻击面.很有可能就存在很多问题,因为攻击者可以以各种方式对该组件进行测试攻击.但是开发者不一定所有的安全问题都能考虑全面. ? 对于这样的问题,最方便的修复方式就是在确定不影响业务的情况下,将这个存在问题的组件不对外导出变成私有组件.这样做的确很有效,私有组件也很安全.但是,如果存在某个私有组件能被导出组件启动的话,那么这个私有组件其实就不再是私有了.如果攻击者可以通过控制导出的组件对私有组件进行控制,那么攻

浅谈 Python 的 with 语句

浅谈 Python 的 with 语句 王 生辉 和 李 骅宸2011 年 12 月 02 日发布 WeiboGoogle+用电子邮件发送本页面 3 引言 with 语句是从 Python 2.5 开始引入的一种与异常处理相关的功能(2.5 版本中要通过 from __future__ import with_statement 导入后才可以使用),从 2.6 版本开始缺省可用(参考 What's new in Python 2.6? 中 with 语句相关部分介绍).with 语句适用于对资源

浅谈Hybrid技术的设计与实现

浅谈Hybrid技术的设计与实现 前言 随着移动浪潮的兴起,各种APP层出不穷,极速的业务扩展提升了团队对开发效率的要求,这个时候使用IOS&Andriod开发一个APP似乎成本有点过高了,而H5的低成本.高效率.跨平台等特性马上被利用起来形成了一种新的开发模式:Hybrid APP. 作为一种混合开发的模式,Hybrid APP底层依赖于Native提供的容器(UIWebview),上层使用Html&Css&JS做业务开发,底层透明化.上层多多样化,这种场景非常有利于前端介入,非

浅谈 Python 程序和 C 程序的整合

源地址:http://www.ibm.com/developerworks/cn/linux/l-cn-pythonandc/ 概览 Python 是一种用于快速开发软件的编程语言,它的语法比较简单,易于掌握,但存在执行速度慢的问题,并且在处理某些问题时存在不足,如对计算机硬件系统的访问,对媒体文件的访问等.而作为软件开发的传统编程语言—— C 语言,却能在这些问题上很好地弥补 Python 语言的不足.因此,本文通过实例研究如何在 Python 程序中整合既有的 C 语言模块,包括用 C 语言