Spring Cloud Gateway 全局异常处理

1、概览

本文将带你了解如何在 Spring Cloud Gateway 中实现全局异常处理。

在现代软件开发中,尤其是在微服务中,API 的高效管理至关重要。这正是 Spring Cloud Gateway 作为 Spring 生态系统的关键组件发挥重要作用的地方。它就像一个门卫,将流量和请求引导到适当的微服务,并提供跨切面的关注点,如安全性、监控/指标和弹性。

然而,在如此复杂的环境中,由于网络故障、服务宕机或应用错误而产生的异常是必然的,这就需要一个强大的异常处理机制。Spring Cloud Gateway 的全局异常处理可确保在所有服务中采用一致的错误处理方法,并增强整个系统的弹性和可靠性。

2、全局异常处理的必要性

Spring Cloud Gateway 是 Spring 生态系统中的一个项目,旨在作为微服务架构中的 API 网关,其主要作用是根据预先制定的规则将请求路由到相应的微服务。网关提供安全(认证和授权)、监控和熔断(Circuit Breakers)等功能。通过处理请求并将其导向适当的后端服务,它有效地管理了诸如安全性和流量管理等横切关注点。

在微服务等分布式系统中,异常可能来自多个方面,如网络问题、服务不可用、下游服务错误和应用级错误,这些都是常见的罪魁祸首。在这种环境下,以本地化方式(即在每个服务内)处理异常可能会导致零散和不一致的错误处理。这种不一致性会使调试工作变得繁琐,并降低用户体验:

API 网关的异常处理设计

全局异常处理通过提供集中的异常管理机制,确保所有异常(无论其来源如何)都能得到一致的处理,并提供标准化的错误响应,从而应对这一挑战。

这种一致性对于系统恢复能力、简化错误跟踪和分析至关重要。它还能提供精确一致的错误格式,帮助用户了解出错的原因,从而提升用户体验。

3、在 Spring Cloud Gateway 中实现全局异常处理

在 Spring Cloud Gateway 中实现全局异常处理涉及几个关键步骤,每个步骤都能确保建立一个强大而高效的异常管理系统。

3.1、创建自定义全局异常处理器

全局异常处理器对于捕获和处理网关中发生的异常至关重要。为此,需要继承 AbstractErrorWebExceptionHandler 并将其添加到 Spring Context 中。这样,就创建了一个能拦截所有异常的集中式处理器。

@Component
public class CustomGlobalExceptionHandler extends AbstractErrorWebExceptionHandler {
    // 构造函数和其他方法
}

该类应能处理各种类型的异常,从 NullPointerException 等一般异常到 HttpClientErrorException 等更具体的异常。目标是涵盖各种可能的错误。该类的核心方法如下所示。

@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
    return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}

// 其他方法

在该方法中,可以根据使用当前请求的 Predicate 将 Handler Function 应用于错误,并对其进行正确处理。需要注意的是,全局异常处理器只处理在网关上下文中抛出的异常。这意味着全局异常处理器的上下文中不包括 5xx4xx 等响应码,这些状态码应使用路由或全局过滤器来处理。

AbstractErrorWebExceptionHandler 提供了许多方法,可以帮助我们处理请求过程中抛出的异常。

private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
    ErrorAttributeOptions options = ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE);
    Map<String, Object> errorPropertiesMap = getErrorAttributes(request, options);
    Throwable throwable = getError(request);
    HttpStatusCode httpStatus = determineHttpStatus(throwable);

    errorPropertiesMap.put("status", httpStatus.value());
    errorPropertiesMap.remove("error");

    return ServerResponse.status(httpStatus)
      .contentType(MediaType.APPLICATION_JSON_UTF8)
      .body(BodyInserters.fromObject(errorPropertiesMap));
}

private HttpStatusCode determineHttpStatus(Throwable throwable) {
    if (throwable instanceof ResponseStatusException) {
        return ((ResponseStatusException) throwable).getStatusCode();
    } else if (throwable instanceof CustomRequestAuthException) {
        return HttpStatus.UNAUTHORIZED;
    } else if (throwable instanceof RateLimitRequestException) {
        return HttpStatus.TOO_MANY_REQUESTS;
    } else {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

上面的代码中,getErrorAttributes()getError() 这两个方法是最相关的,它们提供了上下文和错误信息,对于正确处理异常非常重要。

最后,这些方法会收集 Spring 上下文提供的数据,隐藏一些细节,并根据异常类型调整状态码和响应。CustomRequestAuthExceptionRateLimitRequestException 是自定义异常。

3.2、配置 GatewayFilter

Gateway Filter 是拦截所有传入请求和传出响应的组件:

Spring cloud gateway 设计

通过实现 GatewayFilterGlobalFilter 并将其添加到 Spring 上下文中,可以确保请求得到统一、正确的处理

public class MyCustomFilter implements GatewayFilter {
    // 实现的细节 ...
}

该 Filter 可用于记录传入的请求,这对调试很有帮助。如果出现异常,该 Filter 应将流量重定向到 GlobalExceptionHandler。两者的区别在于,GlobalFilter 针对的是所有即将到来的请求,而 GatewayFilter 针对的是 RouteLocator 中定义的特定路由。

接下来,看看两个 Filter 实现的示例:

public class MyCustomFilter implements GatewayFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (isAuthRoute(exchange) && !isAuthorization(exchange)) {
            throw new CustomRequestAuthException("Not authorized");
        }

        return chain.filter(exchange);
    }

    private static boolean isAuthorization(ServerWebExchange exchange) {
        return exchange.getRequest().getHeaders().containsKey("Authorization");
    }

    private static boolean isAuthRoute(ServerWebExchange exchange) {
        return exchange.getRequest().getURI().getPath().equals("/test/custom_auth");
    }
}

示例中的 MyCustomFilter 模拟了网关验证,如果请求中没有 Authorization 头,就会失败并阻止该请求。如果出现这种情况,就会抛出异常,将错误提交给全局异常处理器。

@Component
class MyGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (hasReachedRateLimit(exchange)) {
            throw new RateLimitRequestException("Too many requests");
        }

        return chain.filter(exchange);
    }

    private boolean hasReachedRateLimit(ServerWebExchange exchange) {
        // 模拟达到速率上限
        return exchange.getRequest().getURI().getPath().equals("/test/custom_rate_limit") && 
          (!exchange.getRequest().getHeaders().containsKey("X-RateLimit-Remaining") || 
            Integer.parseInt(exchange.getRequest().getHeaders().getFirst("X-RateLimit-Remaining")) <= 0);
    }
}

最后,在 MyGlobalFilter 中,Filter 会检查所有请求,但只对特定路由失败。它使用 Header 模拟了对速率限制的验证。由于这是一个 GlobalFilter,只需要将其添加到 Spring 上下文中即可。

同样,一旦发生异常,全局异常处理器就会负责响应管理。

3.3、统一异常处理

异常处理的一致性至关重要。这包括设置标准的错误响应格式,包括 HTTP 状态码、错误信息(响应体)以及任何有助于调试或用户理解的附加信息。

private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
    // 在此定义错误响应结构
}

使用这种方法,可以根据异常类型调整响应。例如,500 Internal Server 问题表示服务器端异常,400 Bad Request 表示客户端问题,等等。正如我们在示例中看到的,Spring 上下文已经提供了一些数据,但响应还可以自定义。

4、高级考虑因素

高级考虑因素包括对所有异常情况实现增强型日志记录。这可能需要集成外部监控和日志记录工具,如 SplunkELK Stack 等。此外,对异常情况进行分类,并根据这些类别定制错误信息,可大大有助于故障排除和改善用户沟通。

测试对于确保全局异常处理程序的有效性至关重要。这包括编写单元测试和集成测试来模拟各种异常情况。JUnit 和 Mockito 等工具在这一过程中发挥了重要作用,让你可以模拟服务并测试异常处理器如何响应不同的异常。

5、总结

实现全局异常处理的最佳做法包括保持错误处理逻辑简单而全面。重要的是记录每一个异常,以便将来进行分析,并在发现新异常时定期更新处理逻辑。定期审查异常处理机制还有助于跟上不断发展的微服务架构。

在 Spring Cloud Gateway 中实现全局异常处理对于开发稳健的微服务架构至关重要。它能确保所有服务采用一致的错误处理策略,并显著提高系统的弹性和可靠性。开发人员可以通过遵循本文的实施策略和最佳实践,构建一个更易于使用和维护的系统。


Ref:https://www.baeldung.com/spring-cloud-global-exception-handling