Dubbo自定义扩展点:filter过滤器——实现全局异常捕获与invoke信息打印以及traceId传递

一、前言

Dubbo是一款高性能且轻量级的开源Java RPC 框架,由阿里巴巴公司开发并在2011年开源,Dubbo不仅支持标准的RPC通信模型,还支持多种传输协议和序列化方式,可以根据具体需求进行配置。此外,它可以与Spring框架无缝集成,并且在阿里巴巴集团内部得到了广泛的应用。它的主要特点包括:

  • 面向接口的远程方法调用:允许应用程序通过RPC远程调用服务。
  • 智能容错和负载均衡:提供故障恢复和高可用性的机制,以及根据请求动态分配服务器资源的功能。
  • 服务自动注册和发现:简化了服务之间的互相发现过程,无需手动配置。

此外Dubbo框架提供了非常丰富的扩展点,其扩展性依赖于dubbo的扩展点加载机制,即dubbo SPI。不同于Java SPi的是dubbo在Java SPI原有基础之上做了扩展,包括:提供IOC和AOP的实现以及按需加载扩展类。本文将基于此讨论如何通过dubbo提供的扩展点,解决dubbo提供者将自定义异常转为RuntimeException的问题,以及监控调用方请求。

二、问题描述与解决

1.问题描述

简单讲就是上游服务通过dubbo调用下游服务失败时,得到的异常并不是期待的自定义异常(BizException),取而代之的是RuntimeException运行时异常。

2.解决过程

通过翻阅资料和查看源码,发现dubbo在执行远程调用请求时,会执行构建的filter链,在加载Protocol接口的扩展点ProtocolFilterWrapper时构建filter责任链:

image-20240203194730788

而dubbo自身提供了全局异常处理机制,也是通过Filter扩展点实现,即ExceptionFilter:

image-20240203195124157

在onResponse方法的逻辑中通过注解不难看出:

  1. 如果时受检异常直接抛出;
  2. 如果时定义在方法签名上的异常直接抛出;
  3. 如果异常类和接口在同一包下,直接抛出;
  4. 如果是Java内部异常或者是dubbo内部异常,直接抛出;
  5. 如果以上都不是,那将异常信息用RuntimeException包装返回。
    @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(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("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);

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

                // otherwise, wrap with RuntimeException and throw back to the client
                appResponse.setException(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);
            }
        }
    }

通过官方文档可以知道,只需要扩展接口org.apache.dubbo.rpc.Filter,并在META-INF/dubbo/org.apache.dubbo.rpc.Filter中添加:

xxx=com.xxx.XxxFilter

具体的Maven项目结构应为:

src
 |-main
    |-java
        |-com
            |-xxx
                |-XxxFilter.java
    |-resources
        |-META-INF
            |-dubbo
                |-org.apache.dubbo.rpc.Filter

服务提供方和服务消费方调用过程拦截,Dubbo 本身的大多功能均基于此扩展点实现,每次远程方法执行,该拦截都会被执行,需注意对性能的影响。

约定:

  • 用户自定义 filter 默认在内置 filter 之后。
  • 特殊值 default,表示缺省扩展点插入的位置。比如:filter="xxx,default,yyy",表示 xxx 在缺省 filter 之前,yyy 在缺省 filter 之后。
  • 特殊符号 -,表示剔除。比如:filter="-foo1",剔除添加缺省扩展点 foo1。比如:filter="-default",剔除添加所有缺省扩展点。
  • provider 和 service 同时配置的 filter 时,累加所有 filter,而不是覆盖。比如:<dubbo:provider filter="xxx,yyy"/><dubbo:service filter="aaa,bbb" />,则 xxx,yyy,aaa,bbb 均会生效。如果要覆盖,需配置:<dubbo:service filter="-xxx,-yyy,aaa,bbb" />

所以,可以将dubbo提供的ExceptionFilter二次封装,使其可以返回自定义异常:

    @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;
                }

                // 返回自定义异常
                if (exception instanceof BaseBizException){
                    appResponse.setException(((BaseBizException) exception));
                }
                ···
                // otherwise, wrap with RuntimeException and throw back to the client
                appResponse.setException(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);
            }
        }
    }

此外,还希望filter做一些其他的事情,比如在日志中打印dubbo调用情况,可以这么做:

首先在自定义的filter上加注解@Activate(group = {Constants.PROVIDER,Constants.CONSUMER},order = 1000),然后在invoke中打印日志输出dubbo调用信息:

@Slf4j
@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 10000)
public class CustomDubboFilter implements Filter {
    private String role = "provider";
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcServiceContext serviceContext = RpcContext.getServiceContext();
        if (serviceContext.isConsumerSide())
            role = "consumer";
        String remoteApplicationName = serviceContext.getRemoteApplicationName();
        String remoteAddressString = serviceContext.getRemoteAddressString();
        String method = invoker.getInterface().getName() +"#"+ invocation.getMethodName();

        String log = String.format("[DUBBO-INVOKE-LOG] ROLE:%s,invokeTime: %s,remoteApplicationName: %s,invokeMethod: %s,inputParameterTypes: %s, inputParams: %s, remoteAddress: %s",
                role,
                DateUtil.formatDateTime(new Date()),
                remoteApplicationName,
                method,
                JSON.toJSONString(invocation.getParameterTypes()),
                JSON.toJSONString(invocation.getArguments()),
                remoteAddressString);
        logger.info(log);
        return invoker.invoke(invocation);
    }
}

也可以通过attachment传递traceId:

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 1000)
public class TraceFilter implements Filter, Filter.Listener {
    private Logger logger = LoggerFactory.getLogger(ExceptionFilter.class);

    private static final String TRACE_ID = "TraceId";

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext rpcContext = RpcContext.getContext();
        String traceId;
        if (rpcContext.isConsumerSide()) {
            traceId = (String) MDC.get(TRACE_ID);
            if (traceId == null) {
                traceId = UUID.randomUUID().toString();
            }
            rpcContext.setAttachment(TRACE_ID, traceId);
        }else if (rpcContext.isProviderSide()) {
            traceId = rpcContext.getAttachment(TRACE_ID);
            if (traceId == null) {
                traceId = UUID.randomUUID().toString();
            }
            MDC.put(TRACE_ID, traceId);
        }
        return invoker.invoke(invocation);
    }
    ···
}

最后,需要修改配置文件(yml):

dubbo:
  consumer:
    filter: TraceFilter, LogFilter, -exception
  provider:
    filter: CustomExceptionFilter, LogFilter, TraceFilter, -exception

参考文献

[1] dubbo调用拦截扩展 https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/spi/description/filter/

posted @ 2024-02-03 22:05  爱吃麦辣鸡翅  阅读(1633)  评论(0编辑  收藏  举报