1. Обзор
Zuul — это маршрутизатор на базе JVM и балансировщик нагрузки на стороне сервера от Netflix . Механизм правил Zuul обеспечивает гибкость при написании правил и фильтров для улучшения маршрутизации в архитектуре микросервисов Spring Cloud.
В этой статье мы рассмотрим, как настроить исключения и ответы на ошибки в Zuul, написав настраиваемые фильтры ошибок, которые запускаются при возникновении ошибки во время выполнения кода.
2. Исключения для зуулов
Все обрабатываемые исключения в Zuul — это ZuulExceptions
. Теперь давайте проясним, что ZuulException
не может быть перехвачено @ControllerAdvice
и аннотировано методом @ExceptionHandling
. Это связано с тем, что ZuulException
выбрасывается из фильтра ошибок . Таким образом, он пропускает последующие цепочки фильтров и никогда не достигает контроллера ошибок. На следующем рисунке показана иерархия обработки ошибок в Zuul:
Zuul отображает следующий ответ об ошибке при наличии ZuulException
:
{
"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
включает три типа фильтров: предварительные , пост -фильтры и фильтры ошибок. Здесь мы углубимся в фильтры ошибок и изучим настройку фильтра ошибок Zuul, получившего название SendErrorFilter
.
Во-первых, мы отключим фильтр SendErrorFilter
по умолчанию , который настраивается автоматически. Это позволяет нам не беспокоиться о порядке выполнения, так как это единственный фильтр ошибок Zuul по умолчанию. Давайте добавим свойство в application.yml
, чтобы отключить его:
zuul:
SendErrorFilter:
post:
disable: true
Теперь давайте напишем собственный фильтр ошибок Zuul под названием CustomZuulErrorFilter
, который выдает пользовательское исключение, если базовая служба недоступна:
public class CustomZuulErrorFilter extends ZuulFilter {
}
Этот пользовательский фильтр должен расширять com.netflix.zuul.ZuulFilter
и переопределять несколько его методов .
Во- первых, мы должны переопределить метод filterType()
и вернуть тип как «ошибку»
. Это потому, что мы хотим настроить фильтр 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
. Затем мы проверяем, является ли throwable
, полученный из RequestContext
, экземпляром ZuulException
. Затем мы проверяем, не является ли причиной вложенного исключения в throwable
экземпляр ConnectException
. Наконец, мы установили контекст с пользовательскими свойствами ответа.
Обратите внимание, что перед настройкой пользовательского ответа мы очищаем throwable
от контекста, чтобы предотвратить дальнейшую обработку ошибок в последующих фильтрах .
Кроме того, мы также можем установить пользовательское исключение внутри нашего метода run()
, которое может быть обработано последующими фильтрами:
if (throwable.getCause().getCause().getCause() instanceof ConnectException) {
ZuulException customException = new ZuulException("", 503, "Service Unavailable");
context.setThrowable(customException);
}
Приведенный выше фрагмент зарегистрирует трассировку стека и перейдет к следующим фильтрам.
Более того, мы можем изменить этот пример для обработки нескольких исключений внутри ZuulFilter.
4. Тестирование пользовательских исключений Zuul
В этом разделе мы протестируем пользовательские исключения Zuul в нашем CustomZuulErrorFilter
.
Предполагая, что существует ConnectException
, вывод приведенного выше примера в ответе Zuul API будет таким:
{
"timestamp": "2022-01-23T23:10:25.584791Z",
"status": 503,
"error": "Service Unavailable"
}
Кроме того, мы всегда можем изменить путь пересылки ошибок Zuul по умолчанию /error
, настроив свойство error.path
в файле application.yml .
Теперь давайте проверим это с помощью нескольких тестов:
@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. Вывод
В этом руководстве мы узнали об иерархии обработки ошибок и углубились в настройку пользовательского фильтра ошибок Zuul в приложении Spring Zuul. Этот фильтр ошибок предоставил возможность настроить тело ответа, а также код ответа. Как обычно, пример кода доступен на GitHub .