Spring Cloud Gateway-自定義異常處理

前提

咱們平時在用SpringMVC的時候,只要是通過DispatcherServlet處理的請求,能夠經過@ControllerAdvice@ExceptionHandler自定義不一樣類型異常的處理邏輯,具體能夠參考ResponseEntityExceptionHandlerDefaultHandlerExceptionResolver,底層原理很簡單,就是發生異常的時候搜索容器中已經存在的異常處理器而且匹配對應的異常類型,匹配成功以後使用該指定的異常處理器返回結果進行Response的渲染,若是找不到默認的異常處理器則用默認的進行兜底(我的認爲,Spring在不少功能設計的時候都有這種「有則使用自定義,無則使用默認提供」這種思想十分優雅)。java

SpringMVC中提供的自定義異常體系在Spring-WebFlux中並不適用,其實緣由很簡單,二者底層的運行容器並不相同。WebExceptionHandlerSpring-WebFlux的異常處理器頂層接口,所以追溯到子類能夠追蹤到DefaultErrorWebExceptionHandlerSpring Cloud Gateway的全局異常處理器,配置類是ErrorWebFluxAutoConfigurationspring

爲何要自定義異常處理

先畫一個假想可是貼近實際架構圖,定位一下網關的做用:json

s-c-c-e-1.png

網關在整個架構中的做用是:後端

  1. 路由服務端應用的請求到後端應用。
  2. (聚合)後端應用的響應轉發到服務端應用。

假設網關服務老是正常的前提下:bash

對於第1點來講,假設後端應用不能平滑無損上線,會有必定的概率出現網關路由請求到一些後端的「殭屍節點(請求路由過去的時候,應用更好在重啓或者恰好中止)」,這個時候會路由會失敗拋出異常,通常狀況是Connection Refuse。架構

對於第2點來講,假設後端應用沒有正確處理異常,那麼應該會把異常信息通過網關轉發回到服務端應用,這種狀況理論上不會出現異常。app

其實還有第3點隱藏的問題,網關若是不僅僅承擔路由的功能,還包含了鑑權、限流等功能,若是這些功能開發的時候對異常捕獲沒有作完善的處理甚至是邏輯自己存在BUG,有可能致使異常沒有被正常捕獲處理,走了默認的異常處理器DefaultErrorWebExceptionHandler,默認的異常處理器的處理邏輯可能並不符合咱們預期的結果。curl

如何自定義異常處理

咱們能夠先看默認的異常處理器的配置類ErrorWebFluxAutoConfigurationide

@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實例ErrorWebExceptionHandlerDefaultErrorAttributes都使用了@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);
}
複製代碼

肯定三點:

  1. 最後封裝到響應體的對象來源於DefaultErrorWebExceptionHandler#getErrorAttributes(),而且結果是一個Map<String, Object>實例轉換成的字節序列。
  2. 原來的RouterFunction實現只支持HTML格式返回,咱們須要修改成JSON格式返回(或者說支持全部格式返回)。
  3. 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),改造的東西並很少,只要瞭解原來異常處理的上下文邏輯便可。

測試

測試場景一:只啓動網關,下游服務不啓動的狀況下直接調用下游服務:

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這個技術棧也必須調研其自定義異常的處理邏輯。

原文連接

(本文完 c-1-d e-a-20190511)

相關文章
相關標籤/搜索