Spring Cloud Gateway中異常處理
最近我們的專案在考慮使用Gateway,考慮使用Spring Cloud Gateway,發現閘道器的異常處理和spring boot 單體應用異常處理還是有很大區別的。讓我們來回顧一下異常。
關於異常是拿來幹什麼的,很多人老程式設計師認為就是拿來我們Debug的時候排錯的,當然這一點確實是異常機制非常大的一個好處,但異常機制包含著更多的意義。
- 關注業務實現 。異常機制使得業務程式碼與異常處理程式碼可以分開,你可以將一些你呼叫資料庫操作的程式碼寫在一個方法裡而只需要在方法上加上throw DB相關的異常。至於如何處理它,你可以在呼叫該方法的時候處理或者甚至選擇不處理,而不是直接在該方法內部新增上if判斷如果資料庫操作錯誤該如何辦,這樣業務程式碼會非常混亂。
- 統一異常處理 。與上一點有所聯絡。我當前所在專案的實踐是,自定義業務類異常,在Controller或Service中丟擲,讓後使用Spring提供的異常介面統一處理我們自己在內部丟擲的異常。這樣一個異常處理架構就非常明瞭。
- 程式的健壯性 。如果沒有異常機制,那麼來了個對空物件的某方法呼叫怎麼辦呢?直接讓程式掛掉?這令人無法接受,當然,我們自己平時寫的一些小的東西確實是這樣,沒有處理它,讓後程序掛了。但在web框架中,可以利用異常處理機制捕獲該異常並將錯誤資訊傳遞給我們然後繼續處理下個請求。所以異常對於健壯性是非常有幫助的。
異常處理(又稱為錯誤處理)功能提供了處理程式執行時出現的任何意外或異常情況的方法。異常處理使用 try、catch 和 finally 關鍵字來嘗試可能未成功的操作,處理失敗,以及在事後清理資源。異常根據意義成三種:業務、系統、程式碼異常,不同的異常採用不同的處理方式。具體的什麼樣的異常怎麼處理就不說了。
紅線和綠線代表兩條異常路徑
1,紅線代表:請求到Gateway發生異常,可能由於後端app在啟動或者是沒啟動
2,綠線代表:請求到Gateway轉發到後端app,後端app發生異常,然後Gateway轉發後端異常到前端
紅線肯定是走Gateway自定義異常:
兩個類的程式碼如下(參考:http://cxytiandi.com/blog/detail/20548):
1 @Configuration 2 @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) 3 public class ExceptionHandlerConfiguration { 4 5private final ServerProperties serverProperties; 6 7private final ApplicationContext applicationContext; 8 9private final ResourceProperties resourceProperties; 10 11private final List<ViewResolver> viewResolvers; 12 13private final ServerCodecConfigurer serverCodecConfigurer; 14 15public ExceptionHandlerConfiguration(ServerProperties serverProperties, 16ResourceProperties resourceProperties, 17ObjectProvider<List<ViewResolver>> viewResolversProvider, 18ServerCodecConfigurer serverCodecConfigurer, 19ApplicationContext applicationContext) { 20this.serverProperties = serverProperties; 21this.applicationContext = applicationContext; 22this.resourceProperties = resourceProperties; 23this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); 24this.serverCodecConfigurer = serverCodecConfigurer; 25} 26 27@Bean 28@Order(Ordered.HIGHEST_PRECEDENCE) 29public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { 30JsonExceptionHandler exceptionHandler = new JsonExceptionHandler( 31errorAttributes, 32this.resourceProperties, 33this.serverProperties.getError(), 34this.applicationContext); 35exceptionHandler.setViewResolvers(this.viewResolvers); 36exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); 37exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); 38return exceptionHandler; 39}
1 public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler { 2 3private static Logger logger = LoggerFactory.getLogger(JsonExceptionHandler.class); 4 5public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, 6ErrorProperties errorProperties, ApplicationContext applicationContext) { 7super(errorAttributes, resourceProperties, errorProperties, applicationContext); 8} 9 10/** 11* 獲取異常屬性 12*/ 13@Override 14protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { 15int code = HttpStatus.INTERNAL_SERVER_ERROR.value(); 16Throwable error = super.getError(request); 17if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) { 18code = HttpStatus.NOT_FOUND.value(); 19} 20return response(code, this.buildMessage(request, error)); 21} 22 23/** 24* 指定響應處理方法為JSON處理的方法 25* @param errorAttributes 26*/ 27@Override 28protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { 29return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); 30} 31 32 33/** 34* 根據code獲取對應的HttpStatus 35* @param errorAttributes 36*/ 37@Override 38protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) { 39int statusCode = (int) errorAttributes.get("code"); 40return HttpStatus.valueOf(statusCode); 41} 42 43/** 44* 構建異常資訊 45* @param request 46* @param ex 47* @return 48*/ 49private String buildMessage(ServerRequest request, Throwable ex) { 50StringBuilder message = new StringBuilder("Failed to handle request ["); 51message.append(request.methodName()); 52message.append(" "); 53message.append(request.uri()); 54message.append("]"); 55if (ex != null) { 56message.append(": "); 57message.append(ex.getMessage()); 58} 59return message.toString(); 60} 61 62/** 63* 構建返回的JSON資料格式 64* @param status狀態碼 65* @param errorMessage異常資訊 66* @return 67*/ 68public static Map<String, Object> response(int status, String errorMessage) { 69Map<String, Object> map = new HashMap<>(); 70map.put("code", status); 71map.put("message", errorMessage); 72map.put("data", null); 73logger.error(map.toString()); 74return map; 75} 76}
綠線代表Gateway轉發異常
轉發的異常,肯定是springboot單體中處理的,至於spring單體中的異常是怎麼處理的呢?肯定是用@ControllerAdvice去做。
1@ExceptionHandler(value = Exception.class) 2@ResponseBody 3public AppResponse exceptionHandler(HttpServletRequest request, Exception e) { 4String ip = RequestUtil.getIpAddress(request); 5logger.info("呼叫者IP:" + ip); 6String errorMessage = String.format("Url:[%s]%n{%s}", request.getRequestURL().toString(), e.getMessage()); 7logger.error(errorMessage, e); 8return AppResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); 9}
到這裡基本上可以了,大家不要試著去用Gateway去捕獲後端異常,回到最初的起點,API 閘道器(API Gateway)主要負責服務請求路由、組合及協議轉換,異常同樣也是一樣,Gateway只負責轉發單體應用的異常,不要試圖Gateway捕獲後端服務異常,然後再輸出給前端。感謝猿天地的一句驚醒夢中人!