Spring Cloud Gateway-自定義異常處理


前提

我們平時在用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)

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM