Triple 3.3的RPC协议提供了开箱即用的REST支持,无需网关转换即可对外暴露REST服务,同时还支持SpringMvc等多种注解,极大的拓展了Dubbo RPC的适用范围、降低了升级适配和移植难度,特别是对于Service代码一大坨、Controller代码就一行的应用场景(当然这种代码风格是否合适暂且不论),能够节省一个Controller文件,只要在Dubbo API中使用官方支持的Rest注解暴露服务即可。
不过,原有基于SpringMvc的全局异常处理逻辑就需要调整,官方文档说对于使用SpringMvc注解暴露的服务,支持使用ExceptionHandler注解处理异常,但异常捕获的类型受限,且无法处理RPC调用的异常,因此经过实践发现了对于tri-rest方式的全局异常处理方式。既能支持Rest调用的自定义统一异常返回(如用GET方式调用POST接口、服务没有注册等),也支持通过RPC调用的自定义统一异常处理返回。
有关Dubbo的异常抛出逻辑这里就不再详细介绍了,网上关于org.apache.dubbo.rpc.filter.ExceptionFilter的介绍很多,在最新的3.3.2中的处理逻辑也没有变化,官方文档对此也说的很清楚。
有关服务间的参数传递(如网关集中认证的权限信息后向传递等典型应用场景),由于tri-rest方式既支持rest访问,又支持tri-rpc调用,因此需要统一对参数的传入方式做适配处理。
1. tri-rest中rest部分的全局异常处理
需要扩展点:
org.apache.dubbo.remoting.http12.ExceptionHandler
实践代码如下:
@Activate(order = -2000)
@Slf4j
public class GlobalExceptionHandler implements ExceptionHandler<Exception, Response<?>> {
@Override
public Level resolveLogLevel(Exception exception) {
return Level.ERROR;
}
@Override
public Response<?> handle(Exception exception, RequestMetadata metadata, MethodDescriptor descriptor) {
log.error("===> GlobalExceptionHandler: call {} at {} from {}: {}", metadata.method(), metadata.path(), descriptor, exception);
return switch (exception) {
case BizException bizException -> Response.fail(bizException); //处理业务异常,需自定义异常类
case RestException _ -> Response.fail(CommonResponseEnum.PARAM_NOT_VALID);
case IllegalArgumentException _ ->
Response.fail(CommonResponseEnum.PARAM_NOT_VALID.getErrorCode(), exception.getMessage()); //处理未传入任何参数的异常,注意不是参数校验异常
case HttpStatusException httpStatusException ->
Response.fail(String.valueOf(httpStatusException.getStatusCode()), httpStatusException.getMessage()); //处理未找到invoker、用GET请求POST接口等REST方法异常
default -> Response.fail(CommonResponseEnum.SYSTEM_ERROR.getErrorCode(), exception.getMessage()); //其他统一异常
};
}
}
SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.remoting.http12.ExceptionHandler文件,然后写入一行文字:
customGlobalExceptionHandler=XX.XX.XX.GlobalExceptionHandler
其中customGlobalExceptionHandler是自定义名称,只要不和Dubbo官方包内使用的一样即可。(官方的SPI定义统一放在dubbo的jar包中 jar:/META-INF/dubbo.internal中,打开对应扩展点文件即可看到,下同)。
2.tri-rest中triple部分的全局异常处理
需要扩展点:
org.apache.dubbo.rpc.Filter
实践代码如下:
@Activate(group = CommonConstants.PROVIDER)
@Slf4j
public class GlobalExceptionFilter extends ExceptionFilter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
log.info("===> GlobalExceptionFilter: call {} at {} with {}", invoker.getInterface().getName(), invoker.getUrl(), invocation.getArguments());
Result appResponse = invoker.invoke(invocation);
if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = appResponse.getException();
String classname = exception.getClass().getName();
log.error("===> GlobalExceptionFilter: catch exception {}", classname, exception);
if (exception instanceof BizException) { //处理自定义异常,需定义异常类
return AsyncRpcResult.newDefaultAsyncResult(Response.fail((BizException) exception), invocation);
}
} catch (Throwable e) {
return AsyncRpcResult.newDefaultAsyncResult(Response.fail(CommonResponseEnum.SYSTEM_ERROR), invocation); //包装其他默认异常
}
}
return appResponse;
}
}
SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:
globalExceptionFilter=XX.XX.XX.GlobalExceptionFilter
文字说明同上。
3.tri-rest中参数校验的全局异常处理
需要扩展点:
org.apache.dubbo.rpc.Filter
tri-rest本身是支持JSR303校验的,和SpringMvc等使用方法一样,只需要使用支持的注解即可,详细文档参见官方,这里只说明如何全局处理校验异常。
实践代码如下:
@Setter
@Activate(
group = {CONSUMER, PROVIDER},
value = VALIDATION_KEY,
order = 10000)
@Slf4j
public class CustomValidationFilter implements Filter {
private Validation validation;
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (needValidate(invoker.getUrl(), invocation.getMethodName())) {
log.info("====> ValidationFilter {} invoked {}", invoker.getUrl(), invocation.getMethodName());
try {
Validator validator = validation.getValidator(invoker.getUrl());
if (validator != null) {
validator.validate(
invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
}
} catch (RpcException e) {
throw e;
} catch (ConstraintViolationException e) {
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
StringBuilder sb = new StringBuilder();
// 自定义参数校验不通过的异常提示,可自定义提示信息如何返回
constraintViolations.forEach(violation ->
sb.append(violation.getPropertyPath().toString())
.append(" ")
.append(violation.getMessage())
.append(", 当前值: '")
.append(violation.getInvalidValue())
.append("'; ")
);
return AsyncRpcResult.newDefaultAsyncResult(Response.fail("10000", sb.toString()), invocation);
} catch (Throwable t) {
return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
}
}
return invoker.invoke(invocation);
}
private boolean needValidate(URL url, String methodName) {
return validation != null
&& !methodName.startsWith("$")
&& ConfigUtils.isNotEmpty(url.getMethodParameter(methodName, VALIDATION_KEY))
&& !"false".equalsIgnoreCase(url.getParameter(VALIDATION_KEY));
}
}
SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:
globalValidationFilter=XX.XX.XX.CustomValidationFilter
文字说明同上。
4.tri-rest中服务间参数的统一处理
需要扩展点:
org.apache.dubbo.rpc.Filter
实践代码如下:
@Activate(group = {CONSUMER, PROVIDER})
@Slf4j
public class TokenContextFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
log.info("====> TokenContextFilter ");
// 获取 RpcServiceContext
RpcServiceContext context = RpcServiceContext.getServiceContext();
// 从 RpcContext 中获取附加信息(适用于 Dubbo 客户端)
if (context.isConsumerSide()) {
Long token = LoginUserContextHolder.getToken();
if (token != null)
RpcContext.getClientAttachment().setAttachment(GlobalConstants.TOKEN_ID, userId);
return invoker.invoke(invocation);
} else {
String userTokenFromRpcContext = RpcContext.getServerContext().getAttachment(GlobalConstants.TOKEN_ID);
// 从 HttpServletRequest 中获取 HTTP 头(适用于 HTTP 客户端)
String userTokenFromHttpHeader = null;
Object request = RpcContext.getServerContext().getRequest();
if (request instanceof DefaultHttpRequest httpRequest) {
userTokenFromHttpHeader = httpRequest.header(GlobalConstants.TOKEN_ID);
}
// 优先使用 HTTP 头中的 Token
String token = userTokenFromHttpHeader != null ? userTokenFromHttpHeader : userTokenFromRpcContext;
// 将 Token 保存到 ThreadLocal,如自定义类LoginUserContextHolder
if (userId != null) {
LoginUserContextHolder.setUserId(userId);
}
try {
return invoker.invoke(invocation);
} finally {
// 清理 ThreadLocal
LoginUserContextHolder.remove();
}
}
}
}
SPI的扩展方式:通过新建resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter文件,然后写入一行文字:
globalTokenFilter=XX.XX.XX.TokenContextFilter
文字说明同上。
在最佳实践中,可以将上面的几种方式编写成一个或几个module,需要时直接导入依赖即可方便实现开箱使用。
2737

被折叠的 条评论
为什么被折叠?



