Spring Cloud Gateway-自定義異常處理

Throwable發表於2019-05-11

前提

我們平時在用SpringMVC的時候,只要是經過DispatcherServlet處理的請求,可以通過@ControllerAdvice@ExceptionHandler自定義不同型別異常的處理邏輯,具體可以參考ResponseEntityExceptionHandlerDefaultHandlerExceptionResolver,底層原理很簡單,就是發生異常的時候搜尋容器中已經存在的異常處理器並且匹配對應的異常型別,匹配成功之後使用該指定的異常處理器返回結果進行Response的渲染,如果找不到預設的異常處理器則用預設的進行兜底(個人認為,Spring在很多功能設計的時候都有這種“有則使用自定義,無則使用預設提供”這種思想十分優雅)。

SpringMVC中提供的自定義異常體系在Spring-WebFlux中並不適用,其實原因很簡單,兩者底層的執行容器並不相同。WebExceptionHandlerSpring-WebFlux的異常處理器頂層介面,因此追溯到子類可以追蹤到DefaultErrorWebExceptionHandlerSpring Cloud Gateway的全域性異常處理器,配置類是ErrorWebFluxAutoConfiguration

為什麼要自定義異常處理

先畫一個假想但是貼近實際架構圖,定位一下閘道器的作用:

s-c-c-e-1.png

閘道器在整個架構中的作用是:

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

假設閘道器服務總是正常的前提下:

對於第1點來說,假設後端應用不能平滑無損上線,會有一定的機率出現閘道器路由請求到一些後端的“殭屍節點(請求路由過去的時候,應用更好在重啟或者剛好停止)”,這個時候會路由會失敗丟擲異常,一般情況是Connection Refuse。

對於第2點來說,假設後端應用沒有正確處理異常,那麼應該會把異常資訊經過閘道器轉發回到服務端應用,這種情況理論上不會出現異常。

其實還有第3點隱藏的問題,閘道器如果不單單承擔路由的功能,還包含了鑑權、限流等功能,如果這些功能開發的時候對異常捕獲沒有做完善的處理甚至是邏輯本身存在BUG,有可能導致異常沒有被正常捕獲處理,走了預設的異常處理器DefaultErrorWebExceptionHandler,預設的異常處理器的處理邏輯可能並不符合我們預期的結果。

如何自定義異常處理

我們可以先看預設的異常處理器的配置類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例項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)

相關文章