gRPC 異常處理

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章