Spring Cloud Gateway-自定义异常处理

简介: 我们平时在用SpringMVC的时候,只要是经过DispatcherServlet处理的请求,可以通过@ControllerAdvice和@ExceptionHandler自定义不同类型异常的处理逻辑,具体可以参考ResponseEntityExceptionHandler和DefaultHandlerExceptionResolver,底层原理很简单,就是发生异常的时候搜索容器中已经存在的异常处理器并且匹配对应的异常类型,匹配成功之后使用该指定的异常处理器返回结果进行Response的渲染,如果找不到默认的异常处理器则用默认的进行兜底。

前提



我们平时在用SpringMVC的时候,只要是经过DispatcherServlet处理的请求,可以通过@ControllerAdvice@ExceptionHandler自定义不同类型异常的处理逻辑,具体可以参考ResponseEntityExceptionHandlerDefaultHandlerExceptionResolver,底层原理很简单,就是发生异常的时候搜索容器中已经存在的异常处理器并且匹配对应的异常类型,匹配成功之后使用该指定的异常处理器返回结果进行Response的渲染,如果找不到默认的异常处理器则用默认的进行兜底(个人认为,Spring在很多功能设计的时候都有这种“有则使用自定义,无则使用默认提供”这种思想十分优雅)。


SpringMVC中提供的自定义异常体系在Spring-WebFlux中并不适用,其实原因很简单,两者底层的运行容器并不相同。WebExceptionHandlerSpring-WebFlux的异常处理器顶层接口,因此追溯到子类可以追踪到DefaultErrorWebExceptionHandlerSpring Cloud Gateway的全局异常处理器,配置类是ErrorWebFluxAutoConfiguration


为什么要自定义异常处理



先画一个假想但是贴近实际架构图,定位一下网关的作用:


微信截图_20220512173555.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这个技术栈也必须调研其自定义异常的处理逻辑。


相关文章
|
4天前
|
前端开发 Java 程序员
Spring Boot统一功能处理(拦截器, 统一数据返回格式, 统一异常处理)
Spring Boot统一功能处理(拦截器, 统一数据返回格式, 统一异常处理)
14 1
|
15天前
|
消息中间件 Java 数据安全/隐私保护
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
Spring Cloud 项目中实现推送消息到 RabbitMQ 消息中间件
|
15天前
|
负载均衡 监控 Java
我把Spring Cloud的超详细资料介绍给你,面试官不会生气吧?geigei
我把Spring Cloud的超详细资料介绍给你,面试官不会生气吧?geigei
|
16天前
|
负载均衡 Java 应用服务中间件
Spring Cloud 负载平衡的意义什么?
负载平衡是指将网络流量在多个服务器之间分布,以达到提高系统性能、增强可靠性和提供更好用户体验的目的。在负载平衡的架构中,多个服务器被组织成一个集群,共同处理用户的请求。
49 4
|
17天前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
28 0
|
17天前
|
消息中间件 负载均衡 Java
【Spring Cloud 初探幽】
【Spring Cloud 初探幽】
21 1
|
17天前
|
安全 Java Docker
|
17天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
42 6
|
17天前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
|
17天前
|
Java 测试技术 开发者
【亮剑】通过自定义注解实现Spring AOP,可以更灵活地控制方法拦截和增强
【4月更文挑战第30天】通过自定义注解实现Spring AOP,可以更灵活地控制方法拦截和增强。首先定义自定义注解,如`@MyCustomAnnotation`,然后创建切面类`MyCustomAspect`,使用`@Pointcut`和`@Before/@After`定义切点及通知。配置AOP代理,添加`@EnableAspectJAutoProxy`到配置类。最后,在需拦截的方法上应用自定义注解。遵循保持注解职责单一、选择合适保留策略等最佳实践,提高代码可重用性和可维护性。记得测试AOP逻辑。