一、前提
我們平時在用SpringMVC
的時候,只要是經過DispatcherServlet
處理的請求,可以通過@ControllerAdvice
和@ExceptionHandler
自定義不同類型異常的處理邏輯,具體可以參考ResponseEntityExceptionHandler
和DefaultHandlerExceptionResolver
,底層原理很簡單,就是發生異常的時候搜索容器中已經存在的異常處理器並且匹配對應的異常類型,匹配成功之后使用該指定的異常處理器返回結果進行Response
的渲染,如果找不到默認的異常處理器則用默認的進行兜底(個人認為,Spring在很多功能設計的時候都有這種“有則使用自定義,無則使用默認提供”這種思想十分優雅)。
SpringMVC
中提供的自定義異常體系在Spring-WebFlux
中並不適用,其實原因很簡單,兩者底層的運行容器並不相同。WebExceptionHandler
是Spring-WebFlux
的異常處理器頂層接口,因此追溯到子類可以追蹤到DefaultErrorWebExceptionHandler
是Spring Cloud Gateway
的全局異常處理器,配置類是ErrorWebFluxAutoConfiguration
。
二、為什么要自定義異常處理
先畫一個假想但是貼近實際架構圖,定位一下網關的作用:
網關在整個架構中的作用是:
- 路由服務端應用的請求到后端應用。
- (聚合)后端應用的響應轉發到服務端應用。
假設網關服務總是正常的前提下:
對於第1點來說,假設后端應用不能平滑無損上線,會有一定的幾率出現網關路由請求到一些后端的“僵屍節點(請求路由過去的時候,應用更好在重啟或者剛好停止)”,這個時候會路由會失敗拋出異常,一般情況是Connection Refuse。
對於第2點來說,假設后端應用沒有正確處理異常,那么應該會把異常信息經過網關轉發回到服務端應用,這種情況理論上不會出現異常。
其實還有第3點隱藏的問題,網關如果不單單承擔路由的功能,還包含了鑒權、限流等功能,如果這些功能開發的時候對異常捕獲沒有做完善的處理甚至是邏輯本身存在BUG,有可能導致異常沒有被正常捕獲處理,走了默認的異常處理器DefaultErrorWebExceptionHandler
,默認的異常處理器的處理邏輯可能並不符合我們預期的結果。
三、如何自定義異常處理
3.1、SpringCloudGateway異常處理類間關系
在org.springframework.boot.autoconfigure.web.reactive.error
包下有三個類用於處理異常。
DefaultErrorWebExceptionHandler
然后將我們處理異常的邏輯替換原有的邏輯。然后通過配置類,將自己寫的類替換原有的類即可。
3.2、自定義異常處理實現
Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace)
已經被標記為過時,使用
Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options)
取代,所以我們需要重寫這個方法就可以了。
3.2.1、(springboot2.3之前版本)
我們可以先看默認的異常處理器的配置類ErrorWebFluxAutoConfiguration
:
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class }) public class ErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.orderedStream() .collect(Collectors.toList()); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler( ErrorAttributes errorAttributes) { DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler( errorAttributes, this.resourceProperties, this.serverProperties.getError(), this.applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; } @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes( this.serverProperties.getError().isIncludeException()); } }
注意到兩個Bean實例ErrorWebExceptionHandler
和DefaultErrorAttributes
都使用了@ConditionalOnMissingBean
注解,也就是我們可以通過自定義實現去覆蓋它們。先自定義一個CustomErrorWebFluxAutoConfiguration
(除了ErrorWebExceptionHandler
的自定義實現,其他直接拷貝ErrorWebFluxAutoConfiguration
):
@Configuration @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnClass(WebFluxConfigurer.class) @AutoConfigureBefore(WebFluxAutoConfiguration.class) @EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class}) public class CustomErrorWebFluxAutoConfiguration { private final ServerProperties serverProperties; private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public CustomErrorWebFluxAutoConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties, ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { this.serverProperties = serverProperties; this.applicationContext = applicationContext; this.resourceProperties = resourceProperties; this.viewResolvers = viewResolversProvider.orderedStream() .collect(Collectors.toList()); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { // TODO 這里完成自定義ErrorWebExceptionHandler實現邏輯 return null; } @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } }
ErrorWebExceptionHandler
的實現,可以直接參考DefaultErrorWebExceptionHandler
,甚至直接繼承DefaultErrorWebExceptionHandler
,覆蓋對應的方法即可。這里直接把異常信息封裝成下面格式的Response
返回,最后需要渲染成JSON格式:
{ "code": 200, "message": "描述信息", "path" : "請求路徑", "method": "請求方法" }
我們需要分析一下DefaultErrorWebExceptionHandler
中的一些源碼:
// 封裝異常屬性 protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { return this.errorAttributes.getErrorAttributes(request, includeStackTrace); } // 渲染異常Response protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) { boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL); Map<String, Object> error = getErrorAttributes(request, includeStackTrace); return ServerResponse.status(getHttpStatus(error)) .contentType(MediaType.APPLICATION_JSON_UTF8) .body(BodyInserters.fromObject(error)); } // 返回路由方法基於ServerResponse的對象 @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse); } // HTTP響應狀態碼的封裝,原來是基於異常屬性的status屬性進行解析的 protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) { int statusCode = (int) errorAttributes.get("status"); return HttpStatus.valueOf(statusCode); }
確定三點:
- 最后封裝到響應體的對象來源於
DefaultErrorWebExceptionHandler#getErrorAttributes()
,並且結果是一個Map<String, Object>
實例轉換成的字節序列。 - 原來的
RouterFunction
實現只支持HTML格式返回,我們需要修改為JSON格式返回(或者說支持所有格式返回)。 DefaultErrorWebExceptionHandler#getHttpStatus()
是響應狀態碼的封裝,原來的邏輯是基於異常屬性getErrorAttributes()
的status屬性進行解析的。
自定義的JsonErrorWebExceptionHandler
如下:
public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler { public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties, ErrorProperties errorProperties, ApplicationContext applicationContext) { super(errorAttributes, resourceProperties, errorProperties, applicationContext); } @Override protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { // 這里其實可以根據異常類型進行定制化邏輯 Throwable error = super.getError(request); Map<String, Object> errorAttributes = new HashMap<>(8); errorAttributes.put("message", error.getMessage()); errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); errorAttributes.put("method", request.methodName()); errorAttributes.put("path", request.path()); return errorAttributes; } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse); } @Override protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) { // 這里其實可以根據errorAttributes里面的屬性定制HTTP響應碼 return HttpStatus.INTERNAL_SERVER_ERROR; } }
配置類CustomErrorWebFluxAutoConfiguration
添加JsonErrorWebExceptionHandler
:
@Bean @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT) @Order(-1) public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) { JsonErrorWebExceptionHandler exceptionHandler = new JsonErrorWebExceptionHandler( errorAttributes, resourceProperties, this.serverProperties.getError(), applicationContext); exceptionHandler.setViewResolvers(this.viewResolvers); exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters()); exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders()); return exceptionHandler; }
很簡單,這里把異常的HTTP響應狀態碼統一為HttpStatus.INTERNAL_SERVER_ERROR(500)
,改造的東西並不多,只要了解原來異常處理的上下文邏輯即可。
3.2.2、(springboot2.3之后版本)
重寫這個方法:Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options)
參考(https://www.jianshu.com/p/b64a6154d742)
或者:重寫springboot的org.springframework.boot.web.reactive.error.DefaultErrorAttributes類的Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options)方法:
public class GatewayErrorAttributes extends DefaultErrorAttributes { private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class); @Override public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { // 這里其實可以根據異常類型進行定制化邏輯 Throwable error = super.getError(request); Map<String, Object> errorAttributes = new HashMap<>(8); errorAttributes.put("message", error.getMessage()); errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value()); errorAttributes.put("method", request.methodName()); errorAttributes.put("path", request.path()); return errorAttributes; } }
四、測試
測試場景一:只啟動網關,下游服務不啟動的情況下直接調用下游服務:
curl http://localhost:9090/order/host // 響應結果 {"path":"/order/host","code":500,"message":"Connection refused: no further information: localhost/127.0.0.1:9091","method":"GET"}
測試場景二:下游服務正常啟動和調用,網關自身拋出異常。
在網關應用自定義一個全局過濾器並且故意拋出異常:
@Component public class ErrorGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { int i = 1/0; return chain.filter(exchange); } }
curl http://localhost:9090/order/host // 響應結果 {"path":"/order/host","code":500,"message":"/ by zero","method":"GET"}
響應結果和定制的邏輯一致,並且后台的日志也打印了對應的異常堆棧。
五、小結
筆者一直認為,做異常分類和按照分類處理是工程里面十分重要的一環。筆者在所在公司負責的系統中,堅持實現異常分類捕獲,主要是需要區分可以重試補償以及無法重試需要及時預警的異常,這樣子才能針對可恢復異常定制自愈邏輯,對不能恢復的異常及時預警和人為介入。所以,Spring Cloud Gateway
這個技術棧也必須調研其自定義異常的處理邏輯。
轉自:
https://www.cnblogs.com/throwable/p/10848879.html
https://www.jianshu.com/p/b64a6154d742