写在前面
dubbo给我们提供了很多的扩展点,异常过滤
就是其中的一个,比如参数验证的ContraintViolationException
异常需要我们在service方法上手动的throw,才能抛出去,不然会自动的封装为RuntimeException,我们来扩展异常过滤,抛出自定义的异常信息,从而以更加友好的方式来暴漏出现的问题。
扩展的原理是利用SPI
,SPI符合开闭原则,即,对修改关闭,对扩展开放,dubbo在jdk spi 的基础上进行了升级改造,定义了dubbo SPI。
1:rpc-service-api
1.1:自定义异常类
当发生异常时我们抛出自定义的异常来暴漏错误信息给用户。
public final class ServiceException extends RuntimeException {
/**
* 错误码
*/
private Integer code;
public ServiceException() { // 创建默认构造方法,用于反序列化的场景。
}
public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
// 使用父类的 message 字段
super(serviceExceptionEnum.getMessage());
// 设置错误码
this.code = serviceExceptionEnum.getCode();
}
public ServiceException(ServiceExceptionEnum serviceExceptionEnum, String message) {
// 使用父类的 message 字段
super(message);
// 设置错误码
this.code = serviceExceptionEnum.getCode();
}
public Integer getCode() {
return code;
}
}
1.2:自定义异常错误码
定义用于返回给用户的错误编码和错误信息。
public enum ServiceExceptionEnum {
// ========== 系统级别 ==========
SUCCESS(0, "成功"),
SYS_ERROR(2001001000, "服务端发生异常"),
MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
INVALID_REQUEST_PARAM_ERROR(2001001002, "参数信息不合法"),
// ========== 用户模块 ==========
USER_NOT_FOUND(1001002000, "用户不存在"),
USER_EXISTS(1001002001, "用户已经存在"),
// ========== 订单模块 ==========
// ========== 商品模块 ==========
// ========== 其他模块 ==========
;
/**
* 错误码
*/
private int code;
/**
* 错误提示
*/
private String message;
ServiceExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "ServiceExceptionEnum{" +
"code=" + code +
", message='" + message + '\'' +
'}';
}
}
2:rpc-service-provider
2.1:自定义异常过滤器类
继承org.apache.dubbo.rpc.ListenableFilter
:
@Activate(group = CommonConstants.PROVIDER)
public class DubboExceptionFilter extends ListenableFilter {
public DubboExceptionFilter() {
super.listener = new ExceptionListenerX();
}
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
return invoker.invoke(invocation);
}
static class ExceptionListenerX extends ExceptionListener {
@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
// 发生异常,并且非泛化调用
if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
Throwable exception = appResponse.getException();
// <1> 如果是 ServiceException 异常,直接返回
if (exception instanceof ServiceException) {
return;
}
// <2> 如果是参数校验的 ConstraintViolationException 异常,则封装返回
if (exception instanceof ConstraintViolationException) {
appResponse.setException(this.handleConstraintViolationException((ConstraintViolationException) exception));
return;
}
}
// <3> 其它情况,继续使用父类处理
super.onResponse(appResponse, invoker, invocation);
}
private ServiceException handleConstraintViolationException(ConstraintViolationException ex) {
// 拼接错误
StringBuilder detailMessage = new StringBuilder();
for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
// 使用 ; 分隔多个错误
if (detailMessage.length() > 0) {
detailMessage.append(";");
}
// 拼接内容到其中
detailMessage.append(constraintViolation.getMessage());
}
// 返回异常
return new ServiceException(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR,
detailMessage.toString());
}
}
static class ExceptionListener implements Listener {
private Logger logger = LoggerFactory.getLogger(ExceptionListener.class);
@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<?>[] exceptionClassses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClassses) {
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)));
return;
} 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;
}
}
}
@Override
public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
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);
}
// For test purpose
public void setLogger(Logger logger) {
this.logger = logger;
}
}
}
@Activate(group = CommonConstants.PROVIDER)
是设置在服务提供者端显示。
2.2:dubbo SPI
在resources目录下创建META-INF/dubbo
文件夹,并创建org.apache.dubbo.rpc.Filter
文件,并添加如下内容:
dubboExceptionFilter=cn.iocoder.springboot.lab30.rpc.filter.DubboExceptionFilter
2.3:UserRpcServiceImpl
修改代码模拟添加一个已经存在的用户:
@Override
public Integer add(UserAddDTO addDTO) {
// 【额外添加】这里,模拟用户已经存在的情况
if ("dongshidaddy".equals(addDTO.getName())) {
throw new ServiceException(ServiceExceptionEnum.USER_EXISTS);
}
return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}
2.4:修改配置文件
修改application.yml
,增加filter="-exception"
去除本身提供的ExceptionFiler过滤器。
<dubbo:service ref="userRpcServiceImpl"
interface="dongshi.daddy.api.UserRpcService"
version="${dubbo.provider.UserRpcService.version}"
validation="true"
filter="-exception"/>
3:rpc-service-consumer
增加会触发名称重复错误的spring bean:
@Component
public class UserRpcServiceTest03 implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private UserRpcService userRpcService;
@Override
public void run(String... args) {
// 添加用户
try {
// 创建 UserAddDTO
UserAddDTO addDTO = new UserAddDTO();
addDTO.setName("dongshidaddy"); // 设置为 dongshidaddy ,触发 ServiceException 异常
addDTO.setGender(1);
// 发起调用
userRpcService.add(addDTO);
logger.info("[run][发起一次 Dubbo RPC 请求,添加用户为({})]", addDTO);
} catch (Exception e) {
logger.error("[run][添加用户发生异常({}),信息为:[{}]", e.getClass().getSimpleName(), e.getMessage());
}
}
}
此时启动在消费者端会看到如下异常,则说明成功了:
2021-11-15 13:23:17.162 ERROR 18604 --- [ main] d.daddy.consumer.UserRpcServiceTest03 : [run][添加用户发生异常(ServiceException),信息为:[用户已经存在]