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

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