gRPC伺服器中新增全域性異常攔截器

banq發表於2024-05-19

在本教程中,我們將研究攔截器在gRPC伺服器應用程式中處理全域性異常的作用。

攔截器可以在請求到達 RPC 方法之前驗證或操作請求。因此,它們在處理常見問題時非常有用,例如日誌記錄、安全性、快取、審計、身份驗證和授權以及應用程式的更多問題。

應用程式還可以使用攔截器作為全域性異常處理程。

攔截器作為全域性異常處理程式
攔截器主要可以幫助處理兩種型別的異常:

  1. 處理從無法處理它們的方法中轉義的未知執行時異常
  2. 處理從任何其他下游攔截器逃逸的異常

攔截器可以幫助建立一個框架來集中處理異常。這樣,應用程式就可以擁有一致的標準和強大的方法來處理異常。

他們可以透過多種方式處理異常:

  • 出於審計或報告目的記錄或保留異常
  • 建立支援票證
  • 在將錯誤響應傳送回客戶端之前修改或豐富錯誤響應

全域性異常處理程式的高階設計
攔截器可以將傳入請求轉發到目標 RPC 服務。但是,當目標 RPC 方法丟擲異常時,它可以捕獲該異常,然後進行適當的處​​理。

我們假設有一個訂單處理微服務。我們將在攔截器的幫助下開發一個全域性異常處理程式,以捕獲從微服務中的 RPC 方法中逃逸的異常。此外,攔截器捕獲從任何下游攔截器逃逸的異常。然後,它呼叫票務服務以在票務系統中提出票證。最後,響應被髮送回客戶端。

首先,我們將開始在protobuf檔案order_processing.proto中定義訂單處理服務的基類:

syntax = <font>"proto3";
package orderprocessing;
option java_multiple_files = true;
option java_package =
"com.baeldung.grpc.orderprocessing";
message OrderRequest {
  string product = 1;
  int32 quantity = 2;
  float price = 3;
}
message OrderResponse {
  string response = 1;
  string orderID = 2;
  string error = 3;
}
service OrderProcessor {
  rpc createOrder(OrderRequest) returns (OrderResponse){}
}

order_processing.proto檔案使用遠端方法createOrder()和兩個DTO OrderRequest和OrderResponse定義OrderProcessor。

稍後,我們可以使用order_processing.proto檔案生成用於實現OrderProcessorImpl和GlobalExeptionInterceptor的支援 Java 原始碼。

Maven外掛生成類OrderRequest、OrderResponse和OrderProcessorGrpc。

我們將在實現部分討論每個類。

我們將實現一個可以處理各種異常的攔截器。異常可能是由於某些失敗的邏輯而顯式引發的,也可能是由於某些不可預見的錯誤而導致的異常。

1.實施全域性異常處理程式
gRPC 應用程式中的攔截器必須實現ServerInterceptor介面的InterceptCall()方法:

public class GlobalExceptionInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata headers,
        ServerCallHandler<ReqT, RespT> next) {
        ServerCall.Listener<ReqT> delegate = null;
        try {
            delegate = next.startCall(serverCall, headers);
        } catch(Exception ex) {
            return handleInterceptorException(ex, serverCall);
        }
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) {
            @Override
            public void onHalfClose() {
                try {
                    super.onHalfClose();
                } catch (Exception ex) {
                    handleEndpointException(ex, serverCall);
                }
            }
        };
    }
    private static <ReqT, RespT> void handleEndpointException(Exception ex, ServerCall<ReqT, RespT> serverCall) {
        String ticket = new TicketService().createTicket(ex.getMessage());
        serverCall.close(Status.INTERNAL
            .withCause(ex)
            .withDescription(ex.getMessage() + <font>", Ticket raised:" + ticket), new Metadata());
    }
    private <ReqT, RespT> ServerCall.Listener<ReqT> handleInterceptorException(Throwable t, ServerCall<ReqT, RespT> serverCall) {
        String ticket = new TicketService().createTicket(t.getMessage());
        serverCall.close(Status.INTERNAL
            .withCause(t)
            .withDescription(
"An exception occurred in a **subsequent** interceptor:" + ", Ticket raised:" + ticket), new Metadata());
        return new ServerCall.Listener<ReqT>() {
           
// no-op<i>
        };
    }
}

InterceptCall()方法接受三個輸入引數:
  • ServerCall:幫助接收響應訊息
  • Metadata後設資料:儲存傳入請求的後設資料
  • ServerCallHandler:幫助將傳入的伺服器呼叫分派到攔截器鏈中的下一個處理器

該方法有兩個try - catch塊。第一個處理從任何後續下游攔截器丟擲的未捕獲的異常。在 catch 塊中,我們呼叫方法handleInterceptorException(),該方法為異常建立一個票證。最後返回一個ServerCall.Listener物件,這是一個回撥方法。

類似地,第二個try – catch塊處理從 RPC 端點丟擲的未捕獲的異常。 InterceptCall ()方法返回ServerCall.Listener,充當傳入 RPC 訊息的回撥。具體來說,它返回ForwardingServerCallListener的例項。SimpleForwardingServerCallListener是ServerCall.Listener的子類。

為了處理下游方法丟擲的異常,我們重寫了ForwardingServerCallListener類中的onHalfClose()方法。SimpleForwardingServerCallListener。一旦客戶端完成傳送訊息,它就會被呼叫。

在此方法中,super.onHalfClose()將請求轉發到OrderProcessorImpl類中的RPC 端點createOrder()。如果端點中有未捕獲的異常,我們會捕獲該異常,然後呼叫handleEndpointException()來建立票證。最後,我們呼叫serverCall物件上的close()方法來關閉伺服器呼叫並將響應傳送回客戶端。

2.註冊全域性異常處理程式
我們在啟動期間建立io.grpc.Server物件時註冊攔截器:

public class OrderProcessingServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(8080)
          .addService(new OrderProcessorImpl())
          .intercept(new LogInterceptor())
          .intercept(new GlobalExceptionInterceptor())
          .build();
        server.start();
        server.awaitTermination();
    }
}

我們將GlobalExceptionInterceptor物件傳遞給io.grpc.ServerBuilder類的intercept()方法。這可確保對OrderProcessorImpl服務的任何 RPC 呼叫都經過GlobalExceptionInterceptor。同樣,我們呼叫addService()方法來註冊OrderProcessorImpl服務。最後,我們呼叫Server物件上的start()方法來啟動伺服器應用程式。

3.處理來自端點的未捕獲異常
為了演示異常處理程式,我們首先看一下OrderProcessorImpl類:

public class OrderProcessorImpl extends OrderProcessorGrpc.OrderProcessorImplBase {
    @Override
    public void createOrder(OrderRequest request, StreamObserver<OrderResponse> responseObserver) {
        if (!validateOrder(request)) {
             throw new StatusRuntimeException(Status.FAILED_PRECONDITION.withDescription(<font>"Order Validation failed"));
        } else {
            OrderResponse orderResponse = processOrder(request);
            responseObserver.onNext(orderResponse);
            responseObserver.onCompleted();
        }
    }
    private Boolean validateOrder(OrderRequest request) {
        int tax = 100/0;
        return false;
    }
    private OrderResponse processOrder(OrderRequest request) {
        return OrderResponse.newBuilder()
          .setOrderID(
"ORD-5566")
          .setResponse(
"Order placed successfully")
          .build();
    }
}

RPC 方法createOrder()首先驗證訂單,然後透過呼叫processOrder()方法對其進行處理。在validateOrder()方法中,我們故意透過將數字除以零來強制執行時異常。

現在,讓我們執行該服務並看看它如何處理異常:

@Test
void whenRuntimeExceptionInRPCEndpoint_thenHandleException() {
    OrderRequest orderRequest = OrderRequest.newBuilder()
      .setProduct(<font>"PRD-7788")
      .setQuantity(1)
      .setPrice(5000)
      .build();
    try {
        OrderResponse response = orderProcessorBlockingStub.createOrder(orderRequest);
    } catch (StatusRuntimeException ex) {
        assertTrue(ex.getStatus()
          .getDescription()
          .contains(
"Ticket raised:TKT"));
    }
}

我們建立OrderRequest物件,然後將其傳遞給客戶端存根中的createOrder()方法。正如預期的那樣,服務丟擲異常。當我們檢查異常中的描述時,我們發現其中嵌入了票證資訊。因此,它表明GlobalExceptionInterceptor完成了它的工作。

這對於流媒體案例也同樣有效。

4.處理攔截器中未捕獲的異常
假設在GlobalExceptionInterceptor 之後有第二個攔截器被呼叫。 LogInterceptor記錄所有傳入請求以用於稽核目的。我們來看一下:

public class LogInterceptor implements ServerInterceptor {
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata,
        ServerCallHandler<ReqT, RespT> next) {
        logMessage(serverCall);
        ServerCall.Listener<ReqT> delegate = next.startCall(serverCall, metadata);
        return delegate;
    }
    private <ReqT, RespT> void logMessage(ServerCall<ReqT, RespT> call) {
        int result = 100/0;
    }
}

在LogInterceptor中,interceptCall()方法在將請求轉發到 RPC 端點之前呼叫logMessage()來記錄訊息。 logMessage ()方法故意執行除以零以引發執行時異常,以演示GlobalExceptionInterceptor的功能。

讓我們執行該服務並看看它如何處理LogInterceptor引發的異常:

@Test
void whenRuntimeExceptionInLogInterceptor_thenHandleException() {
    OrderRequest orderRequest = OrderRequest.newBuilder()
        .setProduct(<font>"PRD-7788")
        .setQuantity(1)
        .setPrice(5000)
        .build();
    try {
        OrderResponse response = orderProcessorBlockingStub.createOrder(orderRequest);
    } catch (StatusRuntimeException ex) {
        assertTrue(ex.getStatus()
            .getDescription()
            .contains(
"An exception occurred in a **subsequent** interceptor:, Ticket raised:TKT"));
    }
    logger.info(
"order processing over");
}

首先,我們在客戶端存根上呼叫createOrder()方法。這次,GlobalExceptionInterceptor在第一個try - catch塊中捕獲從LogInterceptor逃逸的異常。隨後,客戶端收到異常,並在描述中嵌入票證資訊。

相關文章