在Dubbo使用SpringMVC的自定义异常通知

问题重现

下面一个SpringMVC的异常通知类

代码

@ControllerAdvice

public class CommonExceptionHandler {

    @ExceptionHandler(HealthException.class)

    public ResponseEntity<ExceptionResulthandlerLyException(HealthException e) {

        return ResponseEntity.status(e.getExceptionEnum().getCode())

                .body(new ExceptionResult(e.getExceptionEnum()));

    }

    @ExceptionHandler(RuntimeException.class)

    public ResponseEntity<ExceptionResulthandlerRuntimeException(RuntimeException e) {

        e.printStackTrace();

        return ResponseEntity.status(500)

                .body(new ExceptionResult(500getHeadMessage(e.getMessage())));

    }

    @ExceptionHandler(Exception.class)

    public ResponseEntity<ExceptionResulthandlerException(Exception e){

        return ResponseEntity.status(500).body(new ExceptionResult(500getHeadMessage(e.getMessage())));

    }

}

如果在消费方通过RPC调用提供方时,此时提供方抛出了一个自定义异常HealthException

代码

throw new HealthException(ExceptionEnum.INVALID_REQUEST);

由于提供方和消费方是基于RPC通信,所以消费方是不可能直接捕获到这个自定义异常的,这个过程是由RPC框架实现的。

此时消费方输出的日志是

java.lang.RuntimeException: com.l1yp.common.exception.HealthException

可以看出异常类型是java.lang.RuntimeException而不是com.liangyp.common.exception.HealthException,网上一顿搜索最后发现是一个ExceptionFilterRuntimeExceptionHealthException包裹起来的。

下面是ExceptionFilter的代码

代码

// 异常过滤器的逻辑

public Result invoke(Invoker<?> invokerInvocation invocationthrows RpcException {

    try {

        // 调用service

        Result result invoker.invoke(invocation);

        // 内部抛出异常并且不是dubboservice

        if (result.hasException() && GenericService.class != invoker.getInterface()) {

            try {

                // 获取异常对象 此时是 HealthException 的实例

                Throwable exception result.getException();

                // 如果是checked exception(编译期异常)就直接抛出,比如IOException

                // directly throw if it's checked 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);

                // 如果自定义异常类和service接口在同一个jar包,直接抛出

                // 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 result;

                }

                // 如果是JDK的异常,直接抛出

                // directly throw if it's JDK exception

                String className exception.getClass().getName();

                if (className.startsWith("java."|| className.startsWith("javax.")) {

                    return result;

                }

                // 如果是dubbo的异常,直接抛出

                // directly throw if it's dubbo exception

                if (exception instanceof RpcException) {

                    return result;

                }

                // 否则用RuntimeException包裹并抛出。:直接调用toString

                // otherwise, wrap with RuntimeException and throw back to the client

                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;

    }

}

下面分析为什么会有上面的条件:

提前剧透,dubbo为了避免服务消费者不能顺滑的反序列化服务提供者的自定义异常类,很多库的严重bug都是因为序列化问题引起的,比如Jackson前些天...

  • 编译期异常可以直接抛出

    • 编译期异常要么方法签名上有,要么是JDK的异常
      • 方法签名上有,说明服务消费者拥有该异常的声明
      • JDK的异常任何项目都可以反序列化
  • 方法签名上有声明可能抛出异常

    • 因为方法签名上有说明service接口这个jar包肯定有这个异常的类
  • 自定义异常类和service接口在同一jar包

    • 同一Jar包内必定有该自定义异常类的声明
  • JDK的异常(前缀是java./javax.)

    • 任何项目都拥有JDK的异常声明
  • dubbo的异常

    • dubbo消费方也依赖dubbo,所以也拥有dubbo异常声明

针对以上几点给出以下几种解决方案:

自定义异常一般都是继承RuntimeException

  • service接口上每一个函数都添加throws HealthException(不科学的方案)

  • HealthException放到和service接口的jar包

  • HealthException声明在javax的包下,因为ClassLoader只是不允许java.开头

代码

private ProtectionDomain preDefineClass(String name,

                                        ProtectionDomain pd)

{

    if (!checkName(name))

        throw new NoClassDefFoundError("IllegalName: " name);

    // Note:  Checking logic in java.lang.invoke.MemberName.checkForTypeAlias

    // relies on the fact that spoofing is impossible if a class has a name

    // of the form "java.*"

    if ((name != null&& name.startsWith("java.")

            && this != getBuiltinPlatformClassLoader()) {

        throw new SecurityException

            ("Prohibited package name: " +

             name.substring(0name.lastIndexOf('.')));

    }

    if (pd == null) {

        pd defaultDomain;

    }

    if (name != null) {

        checkCerts(namepd.getCodeSource());

    }

    return pd;

}

  • 添加自己的一个CustomExceptionFilter并把dubbo自带的ExceptionFilter干掉

    • 在异常类声明的jar工程添加自定义过滤器
    • CV大法把ExceptionFilter的代码复制到自己的CustomExceptionFilter,稍微修改
    代码

    // 如果是checked exception(编译期异常)就直接抛出,比如IOException

    // directly throw if it's checked exception

    if (!(exception instanceof RuntimeException)

            && (exception instanceof Exception)) {

        return result;

    }

    // 自定义异常直接返回

    if (exception instanceof HealthException){

        return result;

    }

    • resource下新增文件META-INF\dubbo\com.alibaba.dubbo.rpc.Filter

      • 文件内容填写
      代码

      customExceptionFilter=com.liangyp.common.filter.CustomExceptionFilter

  • dubboProviderConfig

代码

@Bean // #1

public ProviderConfig providerConfig() {

    ProviderConfig providerConfig new ProviderConfig();

    providerConfig.setTimeout(1000);

    providerConfig.setFilter("customExceptionFilter,-exception");

    return providerConfig;

}

至于dubbo的ExceptionFilter的filterName从何得知?

​ 其实很简单,到dubbo的jar包一翻就看到了,

  • 文件位置:META-INF\dubbo\internal\com.alibaba.dubbo.rpc.Filter
  • 文件内容:
代码

monitor=com.alibaba.dubbo.monitor.support.MonitorFilter

validation=com.alibaba.dubbo.validation.filter.ValidationFilter

cache=com.alibaba.dubbo.cache.filter.CacheFilter

trace=com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter

future=com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter

echo=com.alibaba.dubbo.rpc.filter.EchoFilter

generic=com.alibaba.dubbo.rpc.filter.GenericFilter

genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter

token=com.alibaba.dubbo.rpc.filter.TokenFilter

accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter

activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter

classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter

context=com.alibaba.dubbo.rpc.filter.ContextFilter

consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter

exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter

executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter

deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter

compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter

timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter