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上找到。