自定义 Zuul Exception

1、概览

Zuul 是 Netflix 推出的基于 JVM 的网关和服务器端负载均衡器。Zuul 的规则引擎提供了灵活性,可以编写规则和过滤器(Filter)来增强在 Spring Cloud 微服务架构中的路由功能。。

本文将会带你了解如何通过自定义 Error Filter 来自定义 Zuul 中的异常和 Error 响应。当代码执行过程中发生错误时,这些自定义 Error Filter 将被执行。

2、Zuul 异常

Zuul 中处理的所有异常都是 ZuulException

ZuulException 不能通过 @ControllerAdvice 捕获,也不能通过 @ExceptionHandling 对方法进行注解。这是因为 ZuulException 是由 Error Filter 抛出的。因此,它会跳过后续的过滤链,永远不会到达 Error Controller。下图描述了 Zuul 中错误处理的层次结构:

Zuul 中错误处理的层次结构

出现 ZuulException 时,Zuul 会显示以下错误响应:

{
    "timestamp": "2022-01-23T22:43:43.126+00:00",
    "status": 500,
    "error": "Internal Server Error"
}

在某些情况下,可能需要自定义 ZuulException 响应中的错误信息或状态码。这时,Zuul Filter 就能派上用场了。

3、自定义 Zuul 异常

spring-cloud-starter-netflix-zuul Starter 包括三种 Filter:Pre(前置)、Post(后置)和 Error(错误)。

本文重点深入了解 Error Filter。

首先,禁用自动配置的默认 SendErrorFilter。这样就不必担心执行顺序,因为这是唯一的 Zuul 默认 Error Filter。

application.yml 中添加属性来禁用它:

zuul:
  SendErrorFilter:
    post:
      disable: true

现在,编写一个名为 CustomZuulErrorFilter 的自定义 Zuul Error Filter,在底层服务不可用时抛出一个自定义异常:

public class CustomZuulErrorFilter extends ZuulFilter {
}

此自定义 Filter 需要继承 com.netflix.zuul.ZuulFilter,并覆写其中的一些方法。

首先,必须覆写 filterType() 方法,并将类型返回为 “error”。这是因为我们配置的是一个 Error 类型的 Filter:

@Override
public String filterType() {
    return "error";
}

之后,覆写 filterOrder() 并返回 -1,这样 Filter 就是链中的第一个:

@Override
public int filterOrder() {
    return -1;
}

接着,覆写 shouldFilter() 方法,并无条件返回 true,因为我们希望在所有情况下都进入该过滤器:

@Override
public boolean shouldFilter() {
    return true;
}

最后,覆写 run() 方法:

@Override
public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    Throwable throwable = context.getThrowable();

    if (throwable instanceof ZuulException) {
        ZuulException zuulException = (ZuulException) throwable;
        if (throwable.getCause().getCause().getCause() instanceof ConnectException) {
            context.remove("throwable");
            context.setResponseBody(RESPONSE_BODY);
            context.getResponse()
                .setContentType("application/json");
            context.setResponseStatusCode(503);
        }
    }
    return null;
}

如上。首先,获取 RequestContext 的实例。接着,验证从 RequestContext 中获取的 throwable 是否是 ZuulException 的实例。然后,检查 throwable 中嵌套异常的原因是否是 ConnectException 的实例。最后,用响应的自定义属性设置 context。

注意,在设置自定义响应之前,会清除上下文中的 throwable,以防止后续 filter 进一步处理错误。

此外,还可以在 run() 方法中设置一个自定义异常,由后续 filter 处理:

if (throwable.getCause().getCause().getCause() instanceof ConnectException) {
    ZuulException customException = new ZuulException("", 503, "Service Unavailable");
    context.setThrowable(customException);
}

上述代码段将记录堆栈信息,并进入下一个 filter。

也以修改此示例,在 ZuulFilter 内处理多个异常。

4、测试自定义 Zuul 异常

测试 CustomZuulErrorFilter 中的自定义 Zuul 异常。

假设出现了 ConnectException,在 Zuul API 的响应中,上述示例的输出将是:

{
    "timestamp": "2022-01-23T23:10:25.584791Z",
    "status": 503,
    "error": "Service Unavailable"
}

此外,还可以通过配置 application.yml 文件中的 error.path 属性来更改默认的 Zuul error 转发路径 /error

测试:

@Test
public void whenSendRequestWithCustomErrorFilter_thenCustomError() {
    Response response = RestAssured.get("http://localhost:8080/foos/1");
    assertEquals(503, response.getStatusCode());
}

在上述测试场景中,/foos/1 路由被故意关闭,导致 java.lang.ConnectException 异常。因此,自定义 Filter 将拦截并响应 503 状态码。

现在,在不注册自定义 Error Filter 的情况下进行测试:

@Test
public void whenSendRequestWithoutCustomErrorFilter_thenError() {
    Response response = RestAssured.get("http://localhost:8080/foos/1");
    assertEquals(500, response.getStatusCode());
}

在未注册自定义 Error Filter 的情况下执行上述测试用例,结果 Zuul 响应状态码为 500。

5、总结

本文介绍了 Zuul 异常处理的层级结构,并深入了解了如何在 Spring Zuul 应用中配置自定义的 Zuul Error Filter,以提供自定义响应体和状态码。


参考:https://www.baeldung.com/zuul-customize-exception