Перейти к основному содержимому

Фабрики предикатов маршрутизации Spring Cloud Gateway

· 8 мин. чтения

1. Введение

В предыдущей статье мы рассмотрели, что такое Spring Cloud Gateway и как использовать встроенные предикаты для реализации основных правил маршрутизации. Однако иногда этих встроенных предикатов может быть недостаточно. Например, наша логика маршрутизации может по какой-то причине потребовать поиска в базе данных.

В этих случаях Spring Cloud Gateway позволяет нам определять пользовательские предикаты. После определения мы можем использовать их как любой другой предикат, что означает, что мы можем определять маршруты с помощью Fluent API и/или DSL.

2. Анатомия предиката

Вкратце, предикат в Spring Cloud Gateway — это объект, который проверяет, соответствует ли данный запрос заданному условию. Для каждого маршрута мы можем определить один или несколько предикатов, которые, если они будут удовлетворены, будут принимать запросы для настроенного бэкенда после применения любых фильтров.

Прежде чем писать наш предикат, давайте взглянем на исходный код существующего предиката или, точнее, на код существующей PredicateFactory. Как следует из названия, Spring Cloud Gateway использует популярный шаблон фабричных методов в качестве механизма для поддержки создания экземпляров Predicate расширяемым способом.

Мы можем выбрать любую из встроенных фабрик предикатов, которые доступны в пакете org.springframework.cloud.gateway.handler.predicate модуля spring-cloud-gateway-core . Мы можем легко определить существующие, так как все их имена заканчиваются на RoutePredicateFactory . HeaderRouterPredicateFactory — хороший пример:

public class HeaderRoutePredicateFactory extends 
AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {

// ... setup code omitted
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
// ... predicate logic omitted
}
};
}

@Validated
public static class Config {
public Config(boolean isGolden, String customerIdCookie ) {
// ... constructor details omitted
}
// ...getters/setters omitted
}
}

Есть несколько ключевых моментов, которые мы можем наблюдать в реализации:

  • Он расширяет AbstractRoutePredicateFactory<T> , который, в свою очередь, реализует интерфейс RoutePredicateFactory , используемый шлюзом.
  • Метод применения возвращает экземпляр фактического предиката — в данном случае GatewayPredicate.
  • Предикат определяет внутренний класс Config , который используется для хранения статических параметров конфигурации, используемых логикой тестирования.

Если мы посмотрим на другие доступные PredicateFactory, то увидим, что базовый шаблон в основном такой же:

  1. Определите класс Config для хранения параметров конфигурации
  2. Расширьте AbstractRoutePredicateFactory , используя класс конфигурации в качестве параметра шаблона.
  3. Переопределить метод применения , возвращая предикат , который реализует желаемую логику теста.

3. Реализация собственной фабрики предикатов

Для нашей реализации предположим следующий сценарий: для данного вызова API нам нужно выбрать между двумя возможными бэкендами. «Золотые» клиенты, которые являются нашими самыми ценными клиентами, должны быть перенаправлены на мощный сервер с доступом к большему объему памяти, большему количеству ЦП и быстрым дискам. Незолотые клиенты переходят на менее мощный сервер, что приводит к более медленному времени отклика.

Чтобы определить, исходит ли запрос от золотого клиента, нам нужно вызвать службу, которая принимает идентификатор клиента , связанный с запросом, и возвращает его статус. Что касается customerId , в нашем простом сценарии мы предполагаем, что он доступен в файле cookie.

Теперь со всей этой информацией мы можем написать наш собственный предикат. Мы сохраним существующее соглашение об именах и назовем наш класс GoldenCustomerRoutePredicateFactory :

public class GoldenCustomerRoutePredicateFactory extends 
AbstractRoutePredicateFactory<GoldenCustomerRoutePredicateFactory.Config> {

private final GoldenCustomerService goldenCustomerService;

// ... constructor omitted

@Override
public Predicate<ServerWebExchange> apply(Config config) {
return (ServerWebExchange t) -> {
List<HttpCookie> cookies = t.getRequest()
.getCookies()
.get(config.getCustomerIdCookie());

boolean isGolden;
if ( cookies == null || cookies.isEmpty()) {
isGolden = false;
} else {
String customerId = cookies.get(0).getValue();
isGolden = goldenCustomerService.isGoldenCustomer(customerId);
}
return config.isGolden() ? isGolden : !isGolden;
};
}

@Validated
public static class Config {
boolean isGolden = true;
@NotEmpty
String customerIdCookie = "customerId";
// ...constructors and mutators omitted
}
}

Как видим, реализация достаточно проста. Наш метод apply возвращает лямбду, которая реализует требуемую логику, используя переданный ему ServerWebExchange . Во-первых, он проверяет наличие куки -файла customerId . Если он не может его найти, то это обычный клиент. В противном случае мы используем значение cookie для вызова метода службы isGoldenCustomer .

Затем мы объединяем тип клиента с настроенным параметром isGolden , чтобы определить возвращаемое значение. Это позволяет нам использовать один и тот же предикат для создания обоих описанных ранее маршрутов, просто изменив значение параметра isGolden .

4. Регистрация фабрики пользовательских предикатов

После того, как мы закодировали нашу собственную фабрику предикатов, нам нужен способ, чтобы Spring Cloud Gateway знал о if. Поскольку мы используем Spring, это делается обычным способом: мы объявляем bean-компонент типа GoldenCustomerRoutePredicateFactory .

Поскольку наш тип реализует RoutePredicateFactory через базовый класс, он будет выбран Spring во время инициализации контекста и сделан доступным для Spring Cloud Gateway.

Здесь мы создадим наш bean-компонент, используя класс @Configuration :

@Configuration
public class CustomPredicatesConfig {
@Bean
public GoldenCustomerRoutePredicateFactory goldenCustomer(
GoldenCustomerService goldenCustomerService) {
return new GoldenCustomerRoutePredicateFactory(goldenCustomerService);
}
}

Здесь мы предполагаем, что у нас есть подходящая реализация GoldenCustomerService, доступная в контексте Spring. В нашем случае у нас есть просто фиктивная реализация, которая сравнивает значение customerId с фиксированным — не реалистично, но полезно для демонстрационных целей.

5. Использование пользовательского предиката

Теперь, когда наш предикат «Золотой клиент» реализован и доступен для Spring Cloud Gateway, мы можем начать использовать его для определения маршрутов. Сначала мы будем использовать fluent API для определения маршрута, а затем сделаем это декларативно, используя YAML.

5.1. Определение маршрута с помощью Fluent API

Fluent API — популярный выбор дизайна, когда нам нужно программно создавать сложные объекты. В нашем случае мы определяем маршруты в @Bean , который создает объект RouteLocator , используя RouteLocatorBuilder и нашу собственную фабрику предикатов:

@Bean
public RouteLocator routes(RouteLocatorBuilder builder, GoldenCustomerRoutePredicateFactory gf ) {
return builder.routes()
.route("golden_route", r -> r.path("/api/**")
.uri("https://fastserver")
.predicate(gf.apply(new Config(true, "customerId"))))
.route("common_route", r -> r.path("/api/**")
.uri("https://slowserver")
.predicate(gf.apply(new Config(false, "customerId"))))
.build();
}

Обратите внимание, как мы использовали две разные конфигурации Config в каждом маршруте. В первом случае первый аргумент равен true , поэтому предикат также принимает значение true , когда у нас есть запрос от золотого клиента. Что касается второго маршрута, мы передаем false в конструктор, чтобы наш предикат возвращал true для незолотых клиентов.

5.2. Определение маршрута в YAML

Мы можем добиться того же результата, что и раньше, декларативным способом, используя свойства или файлы YAML. Здесь мы будем использовать YAML, так как его немного легче читать:

spring:
cloud:
gateway:
routes:
- id: golden_route
uri: https://fastserver
predicates:
- Path=/api/**
- GoldenCustomer=true
- id: common_route
uri: https://slowserver
predicates:
- Path=/api/**
- name: GoldenCustomer
args:
golden: false
customerIdCookie: customerId

Здесь мы определили те же маршруты, что и раньше, используя две доступные опции для определения предикатов. Первый, Golden_route , использует компактное представление, которое принимает форму Predicate=[param[,param]+] . Здесь Predicate — это имя предиката, которое автоматически получается из имени класса фабрики путем удаления суффикса RoutePredicateFactory . После знака «=» у нас есть параметры, используемые для заполнения связанного экземпляра конфигурации .

Этот компактный синтаксис удобен, когда нашему предикату требуются только простые значения, но это не всегда так. Для этих сценариев мы можем использовать длинный формат, показанный во втором маршруте. В этом случае мы снабжаем объект двумя свойствами: name и args . name содержит имя предиката, а args используется для заполнения экземпляра Config . Поскольку на этот раз args является объектом, наша конфигурация может быть настолько сложной, насколько это необходимо.

6. Тестирование

Теперь давайте проверим, все ли работает должным образом, используя curl для тестирования нашего шлюза. Для этих тестов мы настроили наши маршруты так же, как показано ранее, но мы будем использовать общедоступную службу httpbin.org в качестве нашего фиктивного бэкенда. Это довольно полезный сервис, который мы можем использовать, чтобы быстро проверить, работают ли наши правила должным образом, он доступен как в Интернете, так и в виде образа докера, который мы можем использовать локально.

Наша тестовая конфигурация также включает стандартный фильтр AddRequestHeader . Мы используем его для добавления к запросу пользовательского заголовка Goldencustomer со значением, соответствующим результату предиката. Мы также добавляем фильтр StripPrefix , так как мы хотим удалить / api из URI запроса перед вызовом бэкенда.

Сначала протестируем сценарий «общий клиент». Когда наш шлюз запущен и работает, мы используем curl для вызова API заголовков httpbin , который просто будет отображать все полученные заголовки: ``

$ curl http://localhost:8080/api/headers
{
"headers": {
"Accept": "*/*",
"Forwarded": "proto=http;host=\"localhost:8080\";for=\"127.0.0.1:51547\"",
"Goldencustomer": "false",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Forwarded-Host": "localhost:8080",
"X-Forwarded-Prefix": "/api"
}
}

Как и ожидалось, мы видим, что заголовок Goldencustomer был отправлен с ложным значением. Попробуем теперь с «Золотым» клиентом:

$ curl -b customerId=foreach http://localhost:8080/api/headers
{
"headers": {
"Accept": "*/*",
"Cookie": "customerId=foreach",
"Forwarded": "proto=http;host=\"localhost:8080\";for=\"127.0.0.1:51651\"",
"Goldencustomer": "true",
"Host": "httpbin.org",
"User-Agent": "curl/7.55.1",
"X-Forwarded-Host": "localhost:8080",
"X-Forwarded-Prefix": "/api"
}
}

На этот раз Goldencustomer имеет значение true , так как мы отправили файл cookie customerId со значением, которое наша фиктивная служба распознает как допустимое для золотого клиента.

7. Заключение

В этой статье мы рассмотрели, как добавить пользовательские фабрики предикатов в Spring Cloud Gateway и использовать их для определения маршрутов с использованием произвольной логики.

Как обычно, весь код доступен на GitHub .