1. 概述

Netflix的基于JVM的路由器和服务器端负载均衡器Zuul,在Spring Cloud微服务架构中提供了灵活的规则引擎,用于增强路由功能。

在这篇文章中,我们将探讨如何通过编写自定义错误过滤器来定制Zuul中的异常和错误响应,这些过滤器会在代码执行期间发生错误时运行。

2. Zuul异常

Zuul中处理的所有异常都是ZuulException。需要明确的是,ZuulException不能被捕获由@ControllerAdvice(/spring-rest-with-zuul-proxy#controlleradvice)处理,也不能通过@ExceptionHandler(/exception-handling-for-rest-with-spring#exceptionhandler)注解的方法捕获。这是因为ZuulException是由错误过滤器抛出的,因此它会跳过后续过滤链,永远不会到达错误控制器。以下图展示了Zuul中的错误处理层次结构:

当发生ZuulException时,Zuul将显示以下错误响应:

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

在某些情况下,我们可能需要自定义ZuulException响应的消息或状态码。这时,Zuul过滤器就能派上用场。接下来,我们将讨论如何扩展Zuul的错误过滤器并定制ZuulException

3. 自定义Zuul异常

spring-cloud-starter-netflix-zuul启动包包含了三种类型的过滤器:预处理后处理和错误过滤器。我们将深入研究错误过滤器,并探索名为SendErrorFilter的Zuul错误过滤器的自定义。

首先,我们禁用默认的SendErrorFilter,该过滤器是自动配置的,这样我们就无需担心执行顺序,因为这是Zuul的唯一默认错误过滤器。让我们在application.yml中添加属性来禁用它:

zuul:
  SendErrorFilter:
    post:
      disable: true

现在,我们编写一个名为CustomZuulErrorFilter的自定义Zuul错误过滤器,如果底层服务不可用,它将抛出一个自定义异常:

public class CustomZuulErrorFilter extends ZuulFilter {
}

这个自定义过滤器需要继承com.netflix.zuul.ZuulFilter并重写其一些方法。

首先,我们需要重写filterType()方法并返回类型为“error”,因为我们想将Zuul过滤器配置为错误过滤器类型:

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

然后,我们重写filterOrder()方法并返回-1,以便该过滤器在链中的第一个

@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;
}

让我们分解run()方法以理解它的作用。首先,我们获取RequestContext的实例。接下来,我们检查从RequestContext获取的throwable是否为ZuulException的实例。然后,我们检查嵌套异常的根源是否为ConnectException的实例。最后,我们设置了响应的自定义属性。

请注意,在设置自定义响应之前,我们清除了throwable,以防在后续过滤器中进行进一步的错误处理

此外,我们还可以在run()方法中设置一个自定义异常,该异常可以被后续过滤器处理:

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

上述代码片段将记录堆栈跟踪并继续到下一个过滤器。

此外,我们还可以在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**。

现在,让我们通过一些测试用例验证它:

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

在上述测试场景中,故意让/foos/1路由失效,导致java.lang.ConnectException。因此,我们的自定义过滤器将捕获并返回503状态。

现在,让我们不注册自定义错误过滤器来测试这一点:

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

在未注册自定义错误过滤器的情况下执行上述测试案例,Zuul将返回500状态。

5. 总结

在这篇教程中,我们了解了错误处理的层次结构,并在Spring Zuul应用中配置了一个自定义的Zuul错误过滤器。这个错误过滤器为我们提供了定制响应体和响应码的机会。如往常一样,示例代码可在GitHub上找到。