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责任链:
而dubbo自身提供了全局异常处理机制,也是通过Filter扩展点实现,即ExceptionFilter:
在onResponse方法的逻辑中通过注解不难看出:
- 如果时受检异常直接抛出;
- 如果时定义在方法签名上的异常直接抛出;
- 如果异常类和接口在同一包下,直接抛出;
- 如果是Java内部异常或者是dubbo内部异常,直接抛出;
- 如果以上都不是,那将异常信息用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/