stay My blog Read this article
1. Core demands
- Service provider exceptions can be perceived by service consumers
- Exception classification and handling:
- If the business is abnormal, you need to return the corresponding error code to facilitate the service consumer's prompt + log of international copywriting.
- Non business exceptions (such as NPE) need to return content to the service consumer for perception.
- Expansibility & the process should be as simple as possible
2. Scheme selection
2.1. Directly call the OnError method and return after passing the Status wrapper exception
example:
try { }catch (Throwable t) { // Throwable t | StreamObserver<xxx> responseObserver responseObserver.onError(Status.UNKNOWN .withDescription(t.getMessage()) .withCause(t) .asRuntimeException()); }
In this way, the client can perceive it, but the information that can be put in is limited. It can only be a string. It can only be in the parameter withDescription. If multiple parameters are required, it may be converted into a string with the help of some serialization frameworks.
2.2. With the help of protobuf's OneOf syntax
protobuf file:
message Request { int32 number = 1; } message SuccessResponse { int32 result = 1; } enum ErrorCode { ABOVE_20 = 0; BELOW_2 = 1; } message ErrorResponse { int32 input = 1; ErrorCode error_code = 2; } // The point here is that there are two kinds of callbacks, one success and the other failure message Response { oneof response { SuccessResponse success_response = 1; ErrorResponse error_response = 2; } } service CalculatorService { rpc findSquare(Request) returns (Response) {}; }
Java code:
@Override public void findSquare(Request request, StreamObserver<Response> responseObserver) { Response.Builder builder = Response.newBuilder(); try { // Abnormal business }catch (Throwable t) { // If there is an exception, the wrong Message type will be returned ErrorResponse errorResponse = ErrorResponse.newBuilder() // Business exception .setInput(xxx) // Business error code .setErrorCode(errorCode) .build(); builder.setErrorResponse(errorResponse); return; } // If successful, the correct Message type will be returned builder.setSuccessResponse(SuccessResponse.newBuilder()).build()); responseObserver.onNext(builder.build()); responseObserver.onCompleted(); }
This method can store multiple data (as long as more fields are defined in the success / failure Message), but it is troublesome to define two messages (success and failure) to complete the business.
2.3. Based on grpc metadata (actual scheme)
protobuf file:
This protobuf file suggests that the infrastructure department be fed back and integrated into the company's two-party package to avoid redefining each project.
syntax = "proto3"; package credit ; option java_package = "com.maycur.grpc.credit"; // General exception handling information message ErrorInfo { // Wrong business code string errorCode = 1; // Default prompt string defaultMsg = 2; }
java code:
try{ // Business code | streamobserver < XXX > responseobserver } catch(Throwable t) { if (t instanceof ServiceException) { // Business exception, return the error code and default copy to the client ServiceException serviceException = (ServiceException) t; Metadata trailers = new Metadata(); Error.ErrorInfo errorInfo = Error.ErrorInfo.newBuilder() .setErrorCode(serviceException.getErrorCode()) .setDefaultMsg(serviceException.getMessage()) .build(); Metadata.Key<Error.ErrorInfo> ERROR_INFO_TRAILER_KEY = ProtoUtils.keyForProto(errorInfo); trailers.put(ERROR_INFO_TRAILER_KEY, errorInfo); responseObserver.onError(Status.UNKNOWN .withCause(serviceException) .asRuntimeException(trailers)); } else { // For non business exceptions, return the exception details to the client. responseObserver.onError(Status.UNKNOWN // Here is our custom exception information .withDescription(t.getMessage()) .withCause(t) .asRuntimeException()); } // Throw exceptions to make the upper business aware (such as transaction rollback, which may be used) throw new RuntimeException(t); }
In general, this is the picture:
2.4. Optimized scheme
The above scheme is not elegant enough, because all exceptions need to be manually try catch, which is very redundant. There are several options to optimize him.
2.4.1. Implement the ServerInterceptor interface provided by gRPC
class ExceptionInterceptor implements ServerInterceptor { public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { ServerCall.Listener<ReqT> reqTListener = next.startCall(call, headers); return new ExceptionListener(reqTListener, call); } } class ExceptionListener extends ServerCall.Listener { ...... public void onHalfClose() { try { this.delegate.onHalfClose(); } catch (Throwable t) { // Unified exception handling ExtendedStatusRuntimeException exception = fromThrowable(t); // Call Close() sends Status and metadata // This method is essentially the same as onError() call.close(exception.getStatus(), exception.getTrailers()); } } }
In fact, the core of this scheme is to manually execute the contents of the onError method without calling the onError() method. However, I personally think that doing so violates the programming contract. If the subsequent gRPC code changes, there are potential risks.
2.4.2. Wrap the StreamObserver class to enhance its functionality. (scheme adopted)
/** * gRPC The callback delegation (decoration) class is responsible for enhancing the original {@ link StreamObserver}, adding and capturing gRPC exceptions and performing corresponding processing * <p> * {@link GrpcService} This class should be combined and implemented through delegation * <p> * Thread-unSafe implementation * * @author masaiqi * @date 2021/4/12 18:11 */ public class StreamObserverDelegate<ReqT extends Message, RespT extends Message> implements StreamObserver<RespT> { private static final Logger logger = LoggerFactory.getLogger(StreamObserverDelegate.class); private StreamObserver<RespT> originResponseObserver; public StreamObserverDelegate(StreamObserver<RespT> originResponseObserver) { Assert.notNull(originResponseObserver, "originResponseObserver must not null!"); this.originResponseObserver = originResponseObserver; } @Override public void onNext(RespT value) { this.originResponseObserver.onNext(value); } @Override public void onError(Throwable t) { if (t instanceof ServiceException) { // Business exception, return the error code and default copy to the client ServiceException serviceException = (ServiceException) t; Metadata trailers = new Metadata(); Error.ErrorInfo errorInfo = Error.ErrorInfo.newBuilder() .setErrorCode(serviceException.getErrorCode()) .setDefaultMsg(serviceException.getMessage()) .build(); Metadata.Key<Error.ErrorInfo> ERROR_INFO_TRAILER_KEY = ProtoUtils.keyForProto(errorInfo); trailers.put(ERROR_INFO_TRAILER_KEY, errorInfo); this.originResponseObserver.onError(Status.UNKNOWN .withCause(serviceException) .asRuntimeException(trailers)); } else { // For non business exceptions, return the exception details to the client. this.originResponseObserver.onError(Status.UNKNOWN // Here is our custom exception information .withDescription(t.getMessage()) .withCause(t) .asRuntimeException()); } // Throw exceptions to make the upper business aware (such as transaction rollback, which may be used) throw new RuntimeException(t); } @Override public void onCompleted() { if (originResponseObserver != null) { originResponseObserver.onCompleted(); } } /** * Execute business (automatically handle exceptions) * * @author masaiqi * @date 2021/4/12 18:11 */ public RespT executeWithException(Function<ReqT, RespT> function, ReqT request) { RespT response = null; try { response = function.apply(request); } catch (Throwable e) { this.onError(e); } return response; } /** * Execute business (automatically handle exceptions) * * @author masaiqi * @date 2021/4/12 18:11 */ public RespT executeWithException(Supplier<RespT> supplier) { RespT response = null; try { response = supplier.get(); } catch (Throwable e) { this.onError(e); } return response; } }
With the above delegation class (wrapper class), you can easily implement the gRPC method. Take the call method of an Unary RPC as an example:
public class xxxGrpc extends xxxImplBase { @Override public void saveXXX(XXX request, StreamObserver<XXX> responseObserver) { StreamObserverDelegate streamObserverDelegate = new StreamObserverDelegate(responseObserver); streamObserverDelegate.executeWithException(() -> { // Business code return xxx; }); } }
The service provider obtains the exception information in the following ways:
3. Reference documents
- gRPC Error Handling (there are 3 schemes, which can be referred to below)
- https://github.com/grpc/grpc-java/issues/2189
- https://skyao.gitbooks.io/learning-grpc/content/server/status/exception_process.html
- https://www.vinsguru.com/grpc-error-handling/