最近第一次使用 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