最近第一次使用 gRPC 创建一个服务,在服务端我想将一个自定义异常直接抛出去,让客户端能看到。一开始,我这样尝试:
// responseObserver.onError(new CustomException("custom exception"));
throw new CustomException("one error occurs");
可是得到了很尴尬的结果:
io.grpc.StatusRuntimeException: UNKNOWN
客户端看不到我自定义抛出的异常 error message。经过一番研究,找到两种客户端可以获取到服务端抛出来的自定义异常信息。
方式 1: 设置异常 message 到 Status 的 description
服务端实现是这样的:
// 自定义异常处理
@Override
public void customException(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
try {
if (request.getMessage().equals("error")) {
throw new CustomException("custom exception message");
}
EchoResponse echoResponse = EchoResponse.newBuilder().build();
responseObserver.onNext(echoResponse);
responseObserver.onCompleted();
} catch (CustomException e) {
responseObserver.onError(Status.INVALID_ARGUMENT
// 这里就是我们的自定义异常信息
.withDescription(e.getMessage())
.withCause(e)
.asRuntimeException());
}
}
使用 Status.INVALID_ARGUMENT
指定异常 code,这个 code 也是表示参数有问题,正常服务端需要明确抛出的异常大多也是参数问题,如果是服务问题的话,就不用做特殊处理了,让它直接抛出吧。
客户端调用:
try {
EchoResponse echoResponse = stub.customException(
EchoRequest.newBuilder().setMessage("error").build());
System.out.println(echoResponse.getMessage());
} catch (StatusRuntimeException e) {
e.printStackTrace();
// INVALID_ARGUMENT: occurs exception
// 这个message 会包含 INVALID_ARGUMENT, 不是我们想需要的
System.out.println(e.getMessage());
if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
// 这就是我们想要的自定义异常的信息
System.out.println(e.getStatus().getDescription());
// 抛出 CustomException, 方便我们的 ExceptionHandler 处理
throw new CustomException(e.getStatus().getDescription());
} else {
throw e;
}
}
方式 2:通过 MetaData 传递更详细的错误信息
这种方式中,在 proto 文件里自定义了一个 ErrorInfo
:
message ErrorInfo {
// list 里可以放很多的错误信息
repeated string message = 1;
}
这里定义的 ErrorInfo 可以承载很多的信息,比如可以在里面定义一个 code 字段,然后可以表示更丰富的信息。
在服务端实现类中:
private static final Metadata.Key<ErrorInfo> ERROR_INFO_TRAILER_KEY =
ProtoUtils.keyForProto(ErrorInfo.getDefaultInstance());
@Override
public void detailErrorMessage(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
try {
if (request.getMessage().equals("error")) {
throw new CustomException("custom exception message");
}
EchoResponse echoResponse = EchoResponse.newBuilder().build();
responseObserver.onNext(echoResponse);
responseObserver.onCompleted();
} catch (CustomException e) {
Metadata trailers = new Metadata();
ErrorInfo.Builder builder = ErrorInfo.newBuilder()
.addMessage(e.getMessage());
trailers.put(ERROR_INFO_TRAILER_KEY, builder.build());
responseObserver.onError(Status.INVALID_ARGUMENT
.withCause(e)
.asRuntimeException(trailers));
}
}
然后看客户端调用:
try {
EchoResponse echoResponse = stub.detailErrorMessage(
EchoRequest.newBuilder().setMessage("error").build());
System.out.println(echoResponse.getMessage());
} catch (StatusRuntimeException e) {
if (e.getStatus().getCode() == Status.Code.INVALID_ARGUMENT) {
Metadata trailers = Status.trailersFromThrowable(e);
if (trailers.containsKey(ERROR_INFO_TRAILER_KEY)) {
ErrorInfo errorInfo = trailers.get(ERROR_INFO_TRAILER_KEY);
if (errorInfo.getMessageList() != null && errorInfo.getMessageList().size() != 0) {
// 这就是我们想要的自定义异常的信息
System.out.println(errorInfo.getMessageList());
}
}
} else {
throw e;
}
}
上面都是客户端同步调用异常处理,异步调用的异常处理会有一些小区别,完整代码可参考:https://github.com/jiaobuchong/grpc-learning/tree/master/grpc-error-handling
参考:
https://github.com/grpc/grpc-java/tree/master/examples
Introduction to gRPC
https://grpc.github.io/grpc/core/md_doc_statuscodes.html
https://stackoverflow.com/questions/48748745/pattern-for-rich-error-handling-in-grpc