Dubbo捕获自定义异常

一.问题描述

Dubbo远程服务提供者抛出的自定义异常无法被消费方正常捕获,消费方捕获的自定义异常全部变成RuntimeException,使用起来很不方便。

二.原因分析

相关源码

java 复制代码
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.dubbo.rpc.filter;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;
import org.apache.dubbo.rpc.service.GenericService;
import org.apache.dubbo.rpc.support.RpcUtils;

import java.lang.reflect.Method;

import static org.apache.dubbo.common.constants.LoggerCodeConstants.CONFIG_FILTER_VALIDATION_EXCEPTION;


/**
 * ExceptionInvokerFilter
 * <p>
 * Functions:
 * <ol>
 * <li>unexpected exception will be logged in ERROR level on provider side. Unexpected exception are unchecked
 * exception not declared on the interface</li>
 * <li>Wrap the exception not introduced in API package into RuntimeException. Framework will serialize the outer exception but stringnize its cause in order to avoid of possible serialization problem on client side</li>
 * </ol>
 */
@Activate(group = CommonConstants.PROVIDER)
public class ExceptionFilter implements Filter, Filter.Listener {
    private ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(ExceptionFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
            try {
                Throwable exception = appResponse.getException();

                // directly throw if it's checked exception
                if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                    return;
                }
                // directly throw if the exception appears in the signature
                try {
                    Method method = invoker.getInterface().getMethod(RpcUtils.getMethodName(invocation), invocation.getParameterTypes());
                    Class<?>[] exceptionClasses = method.getExceptionTypes();
                    for (Class<?> exceptionClass : exceptionClasses) {
                        if (exception.getClass().equals(exceptionClass)) {
                            return;
                        }
                    }
                } catch (NoSuchMethodException e) {
                    return;
                }

                // for the exception not found in method's signature, print ERROR message in server's log.
                logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "",
                    "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().getRemoteHost() +
                        ". service: " + invoker.getInterface().getName() + ", method: " + RpcUtils.getMethodName(invocation) +
                        ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);

                // directly throw if exception class and interface class are in the same jar file.
                String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                    return;
                }
                // directly throw if it's JDK exception
                String className = exception.getClass().getName();
                if (className.startsWith("java.") || className.startsWith("javax.") || className.startsWith("jakarta.")) {
                    return;
                }
                // directly throw if it's dubbo exception
                if (exception instanceof RpcException) {
                    return;
                }

                //   这里把其它情况的异常都改为RuntimeException抛出
                // otherwise, wrap with RuntimeException and throw back to the client
                appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
            } catch (Throwable e) {
                logger.warn(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "",
                    "Fail to ExceptionFilter when called by " + RpcContext.getServiceContext().getRemoteHost() +
                        ". service: " + invoker.getInterface().getName() + ", method: " + RpcUtils.getMethodName(invocation) +
                        ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
            }
        }
    }

    @Override
    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
        logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "",
            "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().getRemoteHost() +
                ". service: " + invoker.getInterface().getName() + ", method: " + RpcUtils.getMethodName(invocation) +
                ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
    }

    // For test purpose
    public void setLogger(ErrorTypeAwareLogger logger) {
        this.logger = logger;
    }
}

源码中只有以下几种情况会直接抛出异常

  1. 非RuntimeException,直接抛出异常

  2. 如果是checked异常,直接抛出

  3. 在方法签名上有声明,直接抛出

  4. 异常类和接口类在同一个jar包,直接抛出

  5. 是JDK自带的包名以java、javax、jakarta开头的异常,直接抛出

  6. 是dubbo本身的RpcException异常,直接抛出

这样的话那最简单的方案就是重写ExceptionFilter,在识别到我们程序自定义异常时也return就可以在消费端正常捕获并处理异常

三.解决方法

1.重写ExceptionFilter的onResponse方法

java 复制代码
public class DubboExceptionFilter extends ExceptionFilter {
    private ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(ExceptionFilter.class);
	/**redis校验异常 字符串*/
	private static final String REDIS_VALID_EXCEPTION_TEXT = "RedisValidException";
	
	@Override
	public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
		if(!appResponse.hasException() || GenericService.class == invoker.getInterface()) {
			return;
		}
		
        try {
            Throwable exception = appResponse.getException();

            // directly throw if it's checked exception
            if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                return;
            }

            /*--------------------------------------自定义的异常处理--------------------------------------------*/
            if(exception instanceof BusinessFailException) {
            	return;
            }
            //如果还有其它需要直接抛出的异常,也在这里处理
            /*--------------------------------------自定义的异常处理--------------------------------------------*/
            
            // directly throw if the exception appears in the signature
            try {
                Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                Class<?>[] exceptionClasses = method.getExceptionTypes();
                for (Class<?> exceptionClass : exceptionClasses) {
                    if (exception.getClass().equals(exceptionClass)) {
                        return;
                    }
                }
            } catch (NoSuchMethodException e) {
                return;
            }

            // for the exception not found in method's signature, print ERROR message in server's log.
            logger.error(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Got unchecked and undeclared exception which called by " + RpcContext.getServiceContext().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.
            String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
            String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
            if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                return;
            }
            // directly throw if it's JDK exception
            String className = exception.getClass().getName();
            if (className.startsWith("java.") || className.startsWith("javax.")) {
                return;
            }
            // directly throw if it's dubbo exception
            if (exception instanceof RpcException) {
                return;
            }

            // 除了以上所有的异常,其它异常都改为RuntimeException抛出
            appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
        } catch (Throwable e) {
            logger.warn(CONFIG_FILTER_VALIDATION_EXCEPTION, "", "", "Fail to ExceptionFilter when called by " + RpcContext.getServiceContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
        }
	}
}

2.配置让自定义Filter生效

在服务提供方resources目录下创建META-INF/dubbo文件夹,在新建的文件夹下创建org.apache.dubbo.rpc.Filter文件,文件内容为

bash 复制代码
dubboExceptionFilter=你的包名.DubboExceptionFilter

3.修改配置

以properties配置为例

bash 复制代码
#自定义的异常捕获filter
dubbo.provider.filter=dubboExceptionFilter,-exception

四.Dubbo版本>= 3.1.6的问题

dubbo 3.1.6以上版本提高了序列化安全检查强度,自定义的异常抛出时会因为安全问题无法序列化

配置文件中加入以下配置可解决

bash 复制代码
#检查模式级别改为WARN
dubbo.application.serialize-check-status=WARN

问题参考

Dubbo新版本(>3.2)序列化问题

相关推荐
欢迎来到祖安!4 小时前
企业微信 API 接口能力介绍:支持消息收发、图片发送、表情互动、联系人管理等多场景应用
java·前端·数据仓库·spring cloud·企业微信
dadaobusi8 小时前
学习:RV lkvm SBI
java·学习·spring
HEJOO910 小时前
深入理解 Java volatile 关键字:原理、用法与常见误区
java·开发语言·spring
曹牧10 小时前
Java:no content to map due to end of input
java·开发语言
阿维的博客日记10 小时前
Example 类全场景实战指南
java·mybatis
两点王爷10 小时前
SpringBoot 项目集成 GeoServer 源码,实现地图服务发布
java·spring boot·后端
Javatutouhouduan11 小时前
Java如何速通性能优化难题?
java·java面试·jvm调优·后端开发·java程序员·java八股文·java性能优化
lemon_sjdk11 小时前
DOM 节点
java·前端·javascript
leoZ23111 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
驭渊的小故事12 小时前
多线程02
java·开发语言·jvm