在本教程中,我們將研究攔截器在gRPC伺服器應用程式中處理全域性異常的作用。
攔截器可以在請求到達 RPC 方法之前驗證或操作請求。因此,它們在處理常見問題時非常有用,例如日誌記錄、安全性、快取、審計、身份驗證和授權以及應用程式的更多問題。
應用程式還可以使用攔截器作為全域性異常處理程。
攔截器作為全域性異常處理程式
攔截器主要可以幫助處理兩種型別的異常:
- 處理從無法處理它們的方法中轉義的未知執行時異常
- 處理從任何其他下游攔截器逃逸的異常
攔截器可以幫助建立一個框架來集中處理異常。這樣,應用程式就可以擁有一致的標準和強大的方法來處理異常。他們可以透過多種方式處理異常:
- 出於審計或報告目的記錄或保留異常
- 建立支援票證
- 在將錯誤響應傳送回客戶端之前修改或豐富錯誤響應
全域性異常處理程式的高階設計
攔截器可以將傳入請求轉發到目標 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逃逸的異常。隨後,客戶端收到異常,並在描述中嵌入票證資訊。