Написание собственного Predicate — RoutePredicateFactory
Когда встроенных предикатов недостаточно по логике или производительности, пишут собственные RoutePredicateFactory. Ниже — полный пример: реализуем предикат, который проверяет наличие параметра X-Feature и сопоставляет его по списку допустимых значений, при этом конфигурируемый через YAML.
1) Структура — класс предиката
Ключевые моменты:
Наследуемся от AbstractRoutePredicateFactory<Config>. Это даёт поддержку автоконфигурируемой десериализации конфигурации из YAML в Config.
apply(Config) возвращает Predicate<ServerWebExchange>, который будет выполняться для каждого запроса.
Config — POJO с геттерами/сеттерами: Spring автоматически маппит значения из YAML (через Binder).
2) Регистрация (component или bean)
Если не использовать @Component, можно зарегистрировать как bean через конфигурацию.
3) Использование в YAML
В AbstractRoutePredicateFactory стандартная разбивка аргументов (если конфиг простой) позволит подставить список значений. Для более сложной настройки (ключ:значение) необходимо переопределить shortcutFieldOrder() или использовать вложенный Config с именованными полями.
4) Использование в Java DSL
Если хотите inline-предикат в коде (без фабрики), Java DSL позволяет:
Однако такой inline-предикат теряет преимущества конфигурируемости через YAML и переиспользуемости.
#Java #middle #Spring_Cloud_Gateway
Когда встроенных предикатов недостаточно по логике или производительности, пишут собственные RoutePredicateFactory. Ниже — полный пример: реализуем предикат, который проверяет наличие параметра X-Feature и сопоставляет его по списку допустимых значений, при этом конфигурируемый через YAML.
1) Структура — класс предиката
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.web.server.ServerWebExchange;
import java.util.function.Predicate;
import java.util.List;
public class XFeatureRoutePredicateFactory extends AbstractRoutePredicateFactory<XFeatureRoutePredicateFactory.Config> {
public XFeatureRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
List<String> allowed = config.getAllowedValues();
boolean ignoreCase = config.isIgnoreCase();
return exchange -> {
List<String> headerValues = exchange.getRequest().getHeaders().get("X-Feature");
if (headerValues == null || headerValues.isEmpty()) {
return false;
}
for (String hv : headerValues) {
for (String allowedVal : allowed) {
if (ignoreCase) {
if (hv.equalsIgnoreCase(allowedVal)) return true;
} else {
if (hv.equals(allowedVal)) return true;
}
}
}
return false;
};
}
public static class Config {
private List<String> allowedValues;
private boolean ignoreCase = true;
public List<String> getAllowedValues() { return allowedValues; }
public void setAllowedValues(List<String> allowedValues) { this.allowedValues = allowedValues; }
public boolean isIgnoreCase() { return ignoreCase; }
public void setIgnoreCase(boolean ignoreCase) { this.ignoreCase = ignoreCase; }
}
}
Ключевые моменты:
Наследуемся от AbstractRoutePredicateFactory<Config>. Это даёт поддержку автоконфигурируемой десериализации конфигурации из YAML в Config.
apply(Config) возвращает Predicate<ServerWebExchange>, который будет выполняться для каждого запроса.
Config — POJO с геттерами/сеттерами: Spring автоматически маппит значения из YAML (через Binder).
2) Регистрация (component или bean)
import org.springframework.stereotype.Component;
@Component
public class XFeatureRoutePredicateFactory extends AbstractRoutePredicateFactory<XFeatureRoutePredicateFactory.Config> {
// ... код как выше
}
Если не использовать @Component, можно зарегистрировать как bean через конфигурацию.
3) Использование в YAML
spring:
cloud:
gateway:
routes:
- id: feature-route
uri: http://feature-service
predicates:
- XFeature=allowed1,allowed2
В AbstractRoutePredicateFactory стандартная разбивка аргументов (если конфиг простой) позволит подставить список значений. Для более сложной настройки (ключ:значение) необходимо переопределить shortcutFieldOrder() или использовать вложенный Config с именованными полями.
4) Использование в Java DSL
Если хотите inline-предикат в коде (без фабрики), Java DSL позволяет:
.route("xfeature-inline", r -> r.p(exchange -> {
List<String> values = exchange.getRequest().getHeaders().get("X-Feature");
return values != null && values.stream().anyMatch(v -> v.equalsIgnoreCase("allowed1"));
}).uri("http://feature-service"))Однако такой inline-предикат теряет преимущества конфигурируемости через YAML и переиспользуемости.
#Java #middle #Spring_Cloud_Gateway
👍2
Работа с конфигурационными объектами и AbstractRoutePredicateFactory — тонкости
Shortcut configuration vs. full Config binding
Если RoutePredicateFactory использует короткий синтаксис (например: MyPred=val1,val2), то AbstractRoutePredicateFactory поддерживает маппинг по shortcutFieldOrder() — список полей Config, которые будут заполнены по порядку.
Для явной структуры лучше использовать именованные свойства в YAML и обычный биндинг в Config.
Валидация конфигурации
Валидируйте конфиг в конструкторе предиката или в apply() — например, проверить, что список не пуст. Ошибки конфигурации лучше бросать на старте приложения, а не при первом запросе.
Сериализация/десериализация сложных типов
Для сложных типов (например, Duration, Pattern, InetAddress[]) используйте соответствующие конвертеры или храните строковые представления и парсите в Config.
Потокобезопасность
Predicate возвращаемый apply() должен быть потокобезопасным и не содержать mutable state, зависящего от запроса; храните precomputed структуры (например Pattern), а не парсьте каждый раз.
Логирование и мониторинг
Для сложных предикатов логируйте причины отказа (на низком уровне) либо метрики отказов, чтобы упростить отладку некорректного маршрутизации.
Примеры: несколько реальных сценариев и лучшие практики
Пример 1 — комбинация Host + Path + Method (YAML)
Пример 2 — OR-логика через два маршрута (YAML)
Пример 3 — кастомный предикат с конфигом в YAML
#Java #middle #Spring_Cloud_Gateway
Shortcut configuration vs. full Config binding
Если RoutePredicateFactory использует короткий синтаксис (например: MyPred=val1,val2), то AbstractRoutePredicateFactory поддерживает маппинг по shortcutFieldOrder() — список полей Config, которые будут заполнены по порядку.
Для явной структуры лучше использовать именованные свойства в YAML и обычный биндинг в Config.
Валидация конфигурации
Валидируйте конфиг в конструкторе предиката или в apply() — например, проверить, что список не пуст. Ошибки конфигурации лучше бросать на старте приложения, а не при первом запросе.
Сериализация/десериализация сложных типов
Для сложных типов (например, Duration, Pattern, InetAddress[]) используйте соответствующие конвертеры или храните строковые представления и парсите в Config.
Потокобезопасность
Predicate возвращаемый apply() должен быть потокобезопасным и не содержать mutable state, зависящего от запроса; храните precomputed структуры (например Pattern), а не парсьте каждый раз.
Логирование и мониторинг
Для сложных предикатов логируйте причины отказа (на низком уровне) либо метрики отказов, чтобы упростить отладку некорректного маршрутизации.
Примеры: несколько реальных сценариев и лучшие практики
Пример 1 — комбинация Host + Path + Method (YAML)
routes:
- id: users-route
uri: lb://user-service
predicates:
- Host=api.example.com
- Path=/users/**
- Method=GET
Пояснение: типичный маршрут, дешёвые проверки (Host/Method/Path) — быстрый short-circuit.
Пример 2 — OR-логика через два маршрута (YAML)
routes:
- id: mobile-route
uri: lb://mobile-backend
predicates:
- Header=X-Client, ^mobile-.*
- Path=/api/**
- id: fallback-route
uri: lb://web-backend
predicates:
- Path=/api/**
Пояснение: первый маршрут отведёт мобильный трафик на mobile-backend; второй поймает остальные запросы по тому же Path — это простой способ построить OR-поведение без кастомных предикатов.
Пример 3 — кастомный предикат с конфигом в YAML
predicates:
- XFeature=allowedA,allowedB
(см. реализацию XFeatureRoutePredicateFactory выше).
#Java #middle #Spring_Cloud_Gateway
👍2
Predicates в Spring Cloud Gateway: Механика и расширяемость условий маршрутизации
Механизм работы предикатов: от конфигурации к исполнению
Предикаты в Spring Cloud Gateway — это условия, определяющие, должен ли конкретный маршрут быть применён к входящему запросу. На архитектурном уровне предикаты реализуют паттерн Factory Method через абстракцию RoutePredicateFactory. Каждый предикат компилируется в объект Predicate<ServerWebExchange>, который затем оценивается в процессе сопоставления маршрута.
Процесс инициализации предиката
Когда приложение Spring Cloud Gateway стартует, происходит следующая последовательность:
Парсинг конфигурации: Конфигурационный файл (YAML или properties) читается, и определения предикатов преобразуются в объекты PredicateDefinition.
Поиск фабрик: Для каждого PredicateDefinition Spring ищет соответствующий bean типа RoutePredicateFactory. Имя фабрики определяется по свойству name в определении предиката.
Создание конфигурационного объекта: Фабрика создаёт конфигурационный объект (обычно static inner class), который заполняется значениями из аргументов предиката.
Генерация предиката: Вызывается метод apply() фабрики, который возвращает функциональный интерфейс Predicate<ServerWebExchange>.
Порядок оценки предикатов
Предикаты внутри маршрута оцениваются в порядке их объявления, но с важной оптимизацией: оценка происходит лениво и останавливается при первом false.
Механизм оценки реализован через цепочку вызовов and():
Детальный разбор встроенных предикатов
Path Predicate: эффективное сопоставление путей
Предикат Path — наиболее часто используемый. Внутри он использует PathPatternParser из Spring WebFlux, который компилирует строковые шаблоны в оптимизированные структуры данных.
Механика работы:
Использование переменных пути:
Host Predicate: виртуальные хосты и шаблоны
Предикат Host позволяет маршрутизировать на основе заголовка Host или имени сервера.
Расширенные шаблоны:
Шаблон {subdomain}.api.example.com извлекает поддомен как переменную.
Внутренне это преобразуется в регулярное выражение:
Query Predicate: параметры запроса
Предикат Query проверяет наличие и значение параметров URL.
Конфигурация с регулярными выражениями:
Внутренняя реализация использует ServerWebExchange.getRequest().getQueryParams() для доступа к параметрам. Важно отметить, что параметры кэшируются после первого чтения для эффективности.
#Java #middle #Spring_Cloud_Gateway
Механизм работы предикатов: от конфигурации к исполнению
Предикаты в Spring Cloud Gateway — это условия, определяющие, должен ли конкретный маршрут быть применён к входящему запросу. На архитектурном уровне предикаты реализуют паттерн Factory Method через абстракцию RoutePredicateFactory. Каждый предикат компилируется в объект Predicate<ServerWebExchange>, который затем оценивается в процессе сопоставления маршрута.
Процесс инициализации предиката
Когда приложение Spring Cloud Gateway стартует, происходит следующая последовательность:
Парсинг конфигурации: Конфигурационный файл (YAML или properties) читается, и определения предикатов преобразуются в объекты PredicateDefinition.
Поиск фабрик: Для каждого PredicateDefinition Spring ищет соответствующий bean типа RoutePredicateFactory. Имя фабрики определяется по свойству name в определении предиката.
Создание конфигурационного объекта: Фабрика создаёт конфигурационный объект (обычно static inner class), который заполняется значениями из аргументов предиката.
Генерация предиката: Вызывается метод apply() фабрики, который возвращает функциональный интерфейс Predicate<ServerWebExchange>.
Порядок оценки предикатов
Предикаты внутри маршрута оцениваются в порядке их объявления, но с важной оптимизацией: оценка происходит лениво и останавливается при первом false.
Механизм оценки реализован через цепочку вызовов and():
// Внутренняя реализация оценки предикатов маршрута
public boolean test(ServerWebExchange exchange) {
for (Predicate<ServerWebExchange> predicate : predicates) {
if (!predicate.test(exchange)) {
return false; // Прерывание при первом false
}
}
return true;
}
Детальный разбор встроенных предикатов
Path Predicate: эффективное сопоставление путей
Предикат Path — наиболее часто используемый. Внутри он использует PathPatternParser из Spring WebFlux, который компилирует строковые шаблоны в оптимизированные структуры данных.
Механика работы:
// Пример внутренней реализации
public class PathRoutePredicateFactory extends
AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
@Override
public Predicate<ServerWebExchange> apply(Config config) {
final PathPattern pattern = pathPatternParser.parse(config.getPattern());
return exchange -> {
PathContainer path = exchange.getRequest().getPath();
PathPattern.PathMatchInfo info = pattern.matchAndExtract(path);
if (info != null) {
// Сохранение извлечённых переменных в атрибуты
exchange.getAttributes().put(
PathPatternRoutePredicateHandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,
info.getUriVariables()
);
return true;
}
return false;
};
}
}
Использование переменных пути:
predicates:
- Path=/api/users/{id}/orders/{orderId}
Извлечённые переменные id и orderId доступны в фильтрах через шаблоны ${id} и ${orderId}.
Host Predicate: виртуальные хосты и шаблоны
Предикат Host позволяет маршрутизировать на основе заголовка Host или имени сервера.
Расширенные шаблоны:
predicates:
- Host=**.example.com,{subdomain}.api.example.com
Шаблон {subdomain}.api.example.com извлекает поддомен как переменную.
Внутренне это преобразуется в регулярное выражение:
// Пример генерации паттерна
String pattern = "^(?<subdomain>[^.]+)\\.api\\.example\\.com$";
Query Predicate: параметры запроса
Предикат Query проверяет наличие и значение параметров URL.
Конфигурация с регулярными выражениями:
predicates:
- Query=version,v[0-9]+\.[0-9]+ # Проверка формата версии
- Query=token # Просто наличие параметра
Внутренняя реализация использует ServerWebExchange.getRequest().getQueryParams() для доступа к параметрам. Важно отметить, что параметры кэшируются после первого чтения для эффективности.
#Java #middle #Spring_Cloud_Gateway
👍2
RemoteAddr Predicate: контроль доступа по IP
Этот предикат проверяет IP-адрес клиента через CIDR-нотацию.
Механика работы:
Важные нюансы:
При использовании за прокси (nginx, load balancer) необходимо корректно настраивать X-Forwarded-For
IP-адрес извлекается из ServerHttpRequest.getRemoteAddress(), который может быть обёрткой для реального соединения
Weight Predicate: взвешенная маршрутизация
Предикат Weight используется для канареечных развёртываний и A/B тестирования. Он работает в паре: один маршрут определяет группу, а другие маршруты распределяют вес внутри группы.
Конфигурация:
Внутренняя механика:
При инициализации создаётся общий AtomicInteger для группы
Для каждого запроса вычисляется хэш (обычно на основе пути и/или заголовков)
Хэш мапируется на диапазон весов для выбора маршрута
Выбор детерминирован для одинаковых хэшей, что обеспечивает согласованную маршрутизацию
Method Predicate: фильтрация по HTTP-методам
Простейший, но эффективный предикат для ограничения доступа:
Сложные комбинации через AND/OR
Spring Cloud Gateway поддерживает логические комбинации предикатов через DSL и конфигурацию.
Комбинации через YAML
В YAML предикаты по умолчанию объединяются через логическое И (AND):
Все четыре условия должны быть истинны для применения маршрута.
Логическое ИЛИ через кастомный предикат
Для реализации ИЛИ необходимо создать составной предикат:
Использование в YAML:
Негация (NOT) через After с отрицанием
Прямой поддержки NOT нет, но можно использовать временные предикаты с отрицанием:
#Java #middle #Spring_Cloud_Gateway
Этот предикат проверяет IP-адрес клиента через CIDR-нотацию.
Механика работы:
public Predicate<ServerWebExchange> apply(Config config) {
List<IpSubnetFilterRule> sources = config.getSources().stream()
.map(IpSubnetFilterRule::new)
.collect(Collectors.toList());
return exchange -> {
InetSocketAddress remoteAddress = exchange.getRequest()
.getRemoteAddress();
if (remoteAddress != null) {
for (IpSubnetFilterRule source : sources) {
if (source.matches(remoteAddress)) {
return true;
}
}
}
return false;
};
}Важные нюансы:
При использовании за прокси (nginx, load balancer) необходимо корректно настраивать X-Forwarded-For
IP-адрес извлекается из ServerHttpRequest.getRemoteAddress(), который может быть обёрткой для реального соединения
Weight Predicate: взвешенная маршрутизация
Предикат Weight используется для канареечных развёртываний и A/B тестирования. Он работает в паре: один маршрут определяет группу, а другие маршруты распределяют вес внутри группы.
Конфигурация:
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.example.org
predicates:
- Path=/api/**
- Weight=group1, 80
metadata:
response-timeout: 3000
- id: weight_low
uri: https://weightlow.example.org
predicates:
- Path=/api/**
- Weight=group1, 20
metadata:
response-timeout: 5000
Внутренняя механика:
При инициализации создаётся общий AtomicInteger для группы
Для каждого запроса вычисляется хэш (обычно на основе пути и/или заголовков)
Хэш мапируется на диапазон весов для выбора маршрута
Выбор детерминирован для одинаковых хэшей, что обеспечивает согласованную маршрутизацию
Method Predicate: фильтрация по HTTP-методам
Простейший, но эффективный предикат для ограничения доступа:
predicates:
- Method=GET,POST,OPTIONS
Внутренне преобразуется в проверку exchange.getRequest().getMethod().
Сложные комбинации через AND/OR
Spring Cloud Gateway поддерживает логические комбинации предикатов через DSL и конфигурацию.
Комбинации через YAML
В YAML предикаты по умолчанию объединяются через логическое И (AND):
predicates:
- Path=/api/**
- Method=GET
- Header=X-API-Key,.*
- Query=version,v2
Все четыре условия должны быть истинны для применения маршрута.
Логическое ИЛИ через кастомный предикат
Для реализации ИЛИ необходимо создать составной предикат:
@Component
public class OrRoutePredicateFactory extends
AbstractRoutePredicateFactory<OrRoutePredicateFactory.Config> {
public OrRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
for (PredicateDefinition def : config.getPredicates()) {
RoutePredicateFactory factory = findFactory(def);
if (factory != null && factory.apply(convert(def)).test(exchange)) {
return true;
}
}
return false;
};
}
public static class Config {
private List<PredicateDefinition> predicates = new ArrayList<>();
// геттеры и сеттеры
}
}
Использование в YAML:
predicates:
- name: Or
args:
predicates:
- name: Path
args:
pattern: /api/v1/**
- name: Path
args:
pattern: /legacy/api/**
Негация (NOT) через After с отрицанием
Прямой поддержки NOT нет, но можно использовать временные предикаты с отрицанием:
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// Инвертирование любого предиката
return !delegatePredicate.test(exchange);
};
}#Java #middle #Spring_Cloud_Gateway
👍2
Создание кастомного предиката
Реализация RoutePredicateFactory
Создание кастомного предиката требует реализации интерфейса RoutePredicateFactory:
#Java #middle #Spring_Cloud_Gateway
Реализация RoutePredicateFactory
Создание кастомного предиката требует реализации интерфейса RoutePredicateFactory:
@Component
public class JwtClaimRoutePredicateFactory extends
AbstractRoutePredicateFactory<JwtClaimRoutePredicateFactory.Config> {
private final JwtDecoder jwtDecoder;
public JwtClaimRoutePredicateFactory(JwtDecoder jwtDecoder) {
super(Config.class);
this.jwtDecoder = jwtDecoder;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// Извлечение JWT из заголовка
String authHeader = exchange.getRequest()
.getHeaders()
.getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return false;
}
String token = authHeader.substring(7);
try {
// Декодирование и проверка claims
Jwt jwt = jwtDecoder.decode(token);
// Проверка конкретного claim
String claimValue = jwt.getClaimAsString(config.getClaimName());
if (claimValue == null) {
return false;
}
// Сопоставление с ожидаемым значением
if (config.getPattern() != null) {
return claimValue.matches(config.getPattern());
}
return config.getExpectedValues()
.stream()
.anyMatch(expected -> expected.equals(claimValue));
} catch (JwtException e) {
return false;
}
};
}
// Конфигурационный класс
public static class Config {
private String claimName;
private String pattern;
private List<String> expectedValues = new ArrayList<>();
// Геттеры и сеттеры
public String getClaimName() {
return claimName;
}
public void setClaimName(String claimName) {
this.claimName = claimName;
}
public String getPattern() {
return pattern;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public List<String> getExpectedValues() {
return expectedValues;
}
public void setExpectedValues(List<String> expectedValues) {
this.expectedValues = expectedValues;
}
}
// Короткая форма конфигурации
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("claimName", "pattern");
}
}
#Java #middle #Spring_Cloud_Gateway
👍2
Регистрация и использование
Spring Boot автоматически обнаружит компонент через аннотацию @Component.
Для использования в YAML:
Или в краткой форме, если реализован shortcutFieldOrder():
Предикат с динамической конфигурацией
Для предикатов, требующих внешней конфигурации или состояния:
Асинхронные предикаты
Для предикатов, требующих асинхронных операций (обращение к БД, внешним API):
Асинхронные предикаты возвращают AsyncPredicate<ServerWebExchange>, который оценивается в реактивном контексте и не блокирует event loop threads.
#Java #middle #Spring_Cloud_Gateway
Spring Boot автоматически обнаружит компонент через аннотацию @Component.
Для использования в YAML:
predicates:
- name: JwtClaim
args:
claimName: roles
expectedValues: admin,superuser
Или в краткой форме, если реализован shortcutFieldOrder():
predicates:
- JwtClaim=roles,admin|superuser
Предикат с динамической конфигурацией
Для предикатов, требующих внешней конфигурации или состояния:
@Component
public class RateLimitPredicateFactory extends
AbstractRoutePredicateFactory<RateLimitPredicateFactory.Config>
implements ApplicationListener<EnvironmentChangeEvent> {
private final RateLimiter rateLimiter;
private final Map<String, Config> configCache = new ConcurrentHashMap<>();
public RateLimitPredicateFactory(RateLimiter rateLimiter) {
super(Config.class);
this.rateLimiter = rateLimiter;
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// Кэширование конфигурации для производительности
String cacheKey = config.getClientId() + ":" + config.getLimit();
configCache.put(cacheKey, config);
return exchange -> {
Config cachedConfig = configCache.get(cacheKey);
// Извлечение идентификатора клиента из запроса
String clientId = extractClientId(exchange);
if (clientId == null || !clientId.equals(cachedConfig.getClientId())) {
return false;
}
// Проверка лимита
return rateLimiter.tryAcquire(clientId, cachedConfig.getLimit());
};
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
// Очистка кэша при изменении конфигурации
configCache.clear();
}
private String extractClientId(ServerWebExchange exchange) {
// Логика извлечения clientId из запроса
return exchange.getRequest().getHeaders()
.getFirst("X-Client-Id");
}
public static class Config {
private String clientId;
private int limit;
// Геттеры и сеттеры
}
}
Асинхронные предикаты
Для предикатов, требующих асинхронных операций (обращение к БД, внешним API):
@Component
public class ExternalServicePredicateFactory extends
AbstractRoutePredicateFactory<ExternalServicePredicateFactory.Config> {
private final WebClient webClient;
@Override
public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
return exchange -> {
// Асинхронная проверка через внешний сервис
return webClient.post()
.uri(config.getValidationUrl())
.bodyValue(buildValidationRequest(exchange))
.retrieve()
.bodyToMono(ValidationResponse.class)
.map(response -> response.isValid())
.onErrorReturn(false); // При ошибке считаем предикат false
};
}
public static class Config {
private String validationUrl;
private int timeoutMs = 1000;
// Геттеры и сеттеры
}
}
Асинхронные предикаты возвращают AsyncPredicate<ServerWebExchange>, который оценивается в реактивном контексте и не блокирует event loop threads.
#Java #middle #Spring_Cloud_Gateway
👍2
Производительность и оптимизации
Кэширование вычислений предикатов
Для дорогих предикатов важно кэшировать результаты:
Порядок предикатов для оптимизации
Располагайте предикаты в порядке возрастания вычислительной сложности:
Мониторинг и метрики
Интеграция с Micrometer для мониторинга эффективности предикатов:
#Java #middle #Spring_Cloud_Gateway
Кэширование вычислений предикатов
Для дорогих предикатов важно кэшировать результаты:
public Predicate<ServerWebExchange> apply(Config config) {
LoadingCache<ServerWebExchange, Boolean> cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(exchange -> computeExpensivePredicate(exchange, config));
return exchange -> {
try {
return cache.get(exchange);
} catch (Exception e) {
return false;
}
};
}Порядок предикатов для оптимизации
Располагайте предикаты в порядке возрастания вычислительной сложности:
predicates:
- Method=GET # Быстрая проверка
- Path=/api/** # Умеренная сложность
- Query=token,.* # Простая проверка параметра
- JwtClaim=roles,admin # Дорогая проверка JWT
Это позволяет отсеивать неподходящие запросы до выполнения дорогих операций.
Мониторинг и метрики
Интеграция с Micrometer для мониторинга эффективности предикатов:
public Predicate<ServerWebExchange> apply(Config config) {
Counter matchCounter = meterRegistry.counter(
"gateway.predicate.matches",
"predicate", config.getName()
);
Counter totalCounter = meterRegistry.counter(
"gateway.predicate.checks",
"predicate", config.getName()
);
Timer timer = meterRegistry.timer(
"gateway.predicate.duration",
"predicate", config.getName()
);
return exchange -> {
totalCounter.increment();
return timer.record(() -> {
boolean matches = internalTest(exchange, config);
if (matches) {
matchCounter.increment();
}
return matches;
});
};
}#Java #middle #Spring_Cloud_Gateway
👍3
Gateway Filters: Глубокая архитектура и реактивная реализация
Реактивная природа фильтров в Spring Cloud Gateway
В Spring Cloud Gateway каждый фильтр реализует интерфейс GatewayFilter, который определяет единственный метод:
Ключевой аспект — возвращаемый тип Mono<Void>. Это реактивный тип, представляющий отложенное вычисление, которое может завершиться успешно, с ошибкой или никогда не завершиться. Фильтры не блокируют поток выполнения (thread) — вместо этого они описывают pipeline обработки, который выполняется асинхронно.
Пример неблокирующего фильтра:
Цепочка фильтров как реактивный pipeline
GatewayFilterChain представляет собой последовательность фильтров, организованную как связанный список. Когда вызывается chain.filter(exchange), выполнение передаётся следующему фильтру в цепочке. Последний фильтр в цепочке вызывает фактическую маршрутизацию запроса к целевому сервису через NettyRoutingFilter.
Внутренняя реализация DefaultGatewayFilterChain:
Важное наблюдение: цепочка фильтров не выполняется немедленно. Она описывается как последовательность операторов реактивного программирования, которые будут выполнены позже, когда на них подпишутся.
Порядок выполнения фильтров
Фильтры выполняются в строго определённом порядке:
GlobalFilter с наименьшим значением getOrder() (отрицательные значения имеют наивысший приоритет)
Остальные GlobalFilter в порядке возрастания значения getOrder()
Фильтры маршрута в порядке их объявления в конфигурации
Пример конфликта порядка:
#Java #middle #Spring_Cloud_Gateway #Filters
Реактивная природа фильтров в Spring Cloud Gateway
В Spring Cloud Gateway каждый фильтр реализует интерфейс GatewayFilter, который определяет единственный метод:
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
Ключевой аспект — возвращаемый тип Mono<Void>. Это реактивный тип, представляющий отложенное вычисление, которое может завершиться успешно, с ошибкой или никогда не завершиться. Фильтры не блокируют поток выполнения (thread) — вместо этого они описывают pipeline обработки, который выполняется асинхронно.
Пример неблокирующего фильтра:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Этот код выполняется синхронно при построении цепочки
long startTime = System.currentTimeMillis();
// Возвращаем Mono, который описывает асинхронную операцию
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// Этот код выполняется асинхронно ПОСЛЕ завершения цепочки
long duration = System.currentTimeMillis() - startTime;
log.info("Request processed in {} ms", duration);
}));
}Цепочка фильтров как реактивный pipeline
GatewayFilterChain представляет собой последовательность фильтров, организованную как связанный список. Когда вызывается chain.filter(exchange), выполнение передаётся следующему фильтру в цепочке. Последний фильтр в цепочке вызывает фактическую маршрутизацию запроса к целевому сервису через NettyRoutingFilter.
Внутренняя реализация DefaultGatewayFilterChain:
public class DefaultGatewayFilterChain implements GatewayFilterChain {
private final List<GatewayFilter> filters;
private final int index;
private final Publisher<Void> currentPublisher;
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(
this, this.index + 1);
return filter.filter(exchange, chain);
} else {
return Mono.empty(); // Конец цепочки
}
});
}
}Важное наблюдение: цепочка фильтров не выполняется немедленно. Она описывается как последовательность операторов реактивного программирования, которые будут выполнены позже, когда на них подпишутся.
Порядок выполнения фильтров
Фильтры выполняются в строго определённом порядке:
GlobalFilter с наименьшим значением getOrder() (отрицательные значения имеют наивысший приоритет)
Остальные GlobalFilter в порядке возрастания значения getOrder()
Фильтры маршрута в порядке их объявления в конфигурации
Пример конфликта порядка:
@Component
@Order(-1) // Выполняется ПЕРВЫМ
public class FirstGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("First global filter");
return chain.filter(exchange);
}
}
@Component
@Order(0) // Выполняется ВТОРЫМ
public class SecondGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Second global filter");
return chain.filter(exchange);
}
}
#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Детальный разбор встроенных фильтров
AddRequestHeader / AddResponseHeader
Эти фильтры добавляют заголовки к запросу или ответу.
Важно понимать их реактивную природу:
Особенность: Заголовки добавляются в момент модификации запроса, который происходит синхронно при построении цепочки. Однако сама отправка модифицированного запроса к целевому сервису происходит асинхронно.
RemoveRequestHeader
Удаляет заголовки из запроса.
Важная деталь — фильтр работает с копией заголовков:
RewritePath / SetPath
Эти фильтры модифицируют путь запроса. RewritePath использует регулярные выражения для замены, а SetPath устанавливает фиксированный путь.
Внутренняя механика RewritePath:
#Java #middle #Spring_Cloud_Gateway #Filters
AddRequestHeader / AddResponseHeader
Эти фильтры добавляют заголовки к запросу или ответу.
Важно понимать их реактивную природу:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// Модификация запроса (PRE-фильтр)
ServerHttpRequest mutatedRequest = request.mutate()
.header(headerName, headerValue)
.build();
// Создание нового обмена с модифицированным запросом
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}Особенность: Заголовки добавляются в момент модификации запроса, который происходит синхронно при построении цепочки. Однако сама отправка модифицированного запроса к целевому сервису происходит асинхронно.
RemoveRequestHeader
Удаляет заголовки из запроса.
Важная деталь — фильтр работает с копией заголовков:
ServerHttpRequest mutated = exchange.getRequest().mutate()
.headers(headers -> {
headers.remove(headerName);
// или headers.removeIf(key -> key.startsWith("X-Secret-"));
})
.build();
RewritePath / SetPath
Эти фильтры модифицируют путь запроса. RewritePath использует регулярные выражения для замены, а SetPath устанавливает фиксированный путь.
Внутренняя механика RewritePath:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getRawPath();
// Применение регулярного выражения
String newPath = path.replaceAll(regexp, replacement);
// Сохранение переменных из регулярного выражения
Map<String, String> variables = extractVariables(path, regexp);
exchange.getAttributes().put(URI_TEMPLATE_VARIABLES_ATTRIBUTE, variables);
// Построение нового URI
URI newUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.replacePath(newPath)
.build(true)
.toUri();
ServerHttpRequest mutated = exchange.getRequest().mutate()
.uri(newUri)
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}#Java #middle #Spring_Cloud_Gateway #Filters
👍2
RequestRateLimiter
Один из самых сложных фильтров, реализующий ограничение частоты запросов. Работает с Redis для распределённого лимитирования.
Архитектура:
Алгоритм token bucket в Redis:
#Java #middle #Spring_Cloud_Gateway #Filters
Один из самых сложных фильтров, реализующий ограничение частоты запросов. Работает с Redis для распределённого лимитирования.
Архитектура:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. Определение ключа для лимитирования
return keyResolver.resolve(exchange)
.switchIfEmpty(Mono.error(new IllegalStateException("Key not found")))
.flatMap(key -> {
// 2. Проверка лимита через Redis
return rateLimiter.isAllowed(key, config.getReplenishRate(),
config.getBurstCapacity(), config.getRequestedTokens())
.flatMap(response -> {
// 3. Добавление заголовков с информацией о лимите
exchange.getResponse().getHeaders()
.add("X-RateLimit-Remaining",
String.valueOf(response.getTokensLeft()));
exchange.getResponse().getHeaders()
.add("X-RateLimit-Replenish-Rate",
String.valueOf(config.getReplenishRate()));
if (response.isAllowed()) {
return chain.filter(exchange);
} else {
// 4. Отклонение запроса при превышении лимита
exchange.getResponse().setStatusCode(
HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
});
})
.onErrorResume(Exception.class, e -> {
// Обработка ошибок Redis
log.warn("Rate limiter error", e);
return chain.filter(exchange); // Разрешить запрос при ошибке
});
}Алгоритм token bucket в Redis:
-- Lua-скрипт, выполняемый атомарно в Redis
local tokens_key = KEYS[1] -- ключ для хранения токенов
local timestamp_key = KEYS[2] -- ключ для метки времени
local rate = tonumber(ARGV[1]) -- replenishRate
local capacity = tonumber(ARGV[2]) -- burstCapacity
local now = tonumber(ARGV[3]) -- текущее время
local requested = tonumber(ARGV[4]) -- requestedTokens
local fill_time = capacity / rate
local ttl = math.floor(fill_time * 2)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
new_tokens = filled_tokens - requested
end
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed, new_tokens }
#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Retry / Circuit Breaker
Фильтры устойчивости, построенные на Resilience4J.
Retry фильтр:
Circuit Breaker фильтр:
ModifyResponseBody / ModifyRequestBody
Эти фильтры позволяют трансформировать тела запросов и ответов. Они требуют особого внимания из-за работы с потоком данных.
Архитектура ModifyRequestBody:
Критическое ограничение: Эти фильтры требуют чтения всего тела в память, что делает их непригодными для работы с большими файлами или стриминговыми данными.
#Java #middle #Spring_Cloud_Gateway #Filters
Фильтры устойчивости, построенные на Resilience4J.
Retry фильтр:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return retryOperator.apply(chain.filter(exchange))
.onErrorResume(RetryExhaustedException.class, e -> {
// Все попытки исчерпаны
exchange.getResponse().setStatusCode(
HttpStatus.SERVICE_UNAVAILABLE);
return exchange.getResponse().setComplete();
});
}Circuit Breaker фильтр:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return circuitBreaker.run(
chain.filter(exchange),
throwable -> {
// Fallback при открытом контуре или ошибке
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] fallbackBody = "{\"status\":\"unavailable\"}".getBytes();
DataBuffer buffer = response.bufferFactory()
.wrap(fallbackBody);
return response.writeWith(Mono.just(buffer));
}
);
}ModifyResponseBody / ModifyRequestBody
Эти фильтры позволяют трансформировать тела запросов и ответов. Они требуют особого внимания из-за работы с потоком данных.
Архитектура ModifyRequestBody:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. Проверка Content-Type
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
if (!config.getInClass().isInstance(contentType)) {
return chain.filter(exchange);
}
// 2. Чтение тела запроса в память (ограничение по size)
return DataBufferUtils.join(exchange.getRequest().getBody(), config.getMaxInMemorySize())
.switchIfEmpty(Mono.just(EMPTY_BUFFER))
.flatMap(dataBuffer -> {
// 3. Десериализация
return decode(dataBuffer, config.getInClass())
.flatMap(decodedBody -> {
// 4. Трансформация
return config.getRewriteFunction()
.apply(exchange, decodedBody)
.flatMap(encodedBody -> {
// 5. Сериализация обратно
byte[] bytes = encode(encodedBody, config.getOutClass());
// 6. Замена тела запроса
ServerHttpRequest mutated = exchange.getRequest().mutate()
.body(Flux.just(exchange.getResponse().bufferFactory()
.wrap(bytes)))
.build();
return chain.filter(
exchange.mutate().request(mutated).build()
);
});
})
.doFinally(signalType -> {
// 7. Освобождение буфера
DataBufferUtils.release(dataBuffer);
});
});
}Критическое ограничение: Эти фильтры требуют чтения всего тела в память, что делает их непригодными для работы с большими файлами или стриминговыми данными.
#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Работа с телом запроса: проблемы и решения
Проблема повторного чтения body
Тело HTTP-запроса в реактивном стеке представлено как Flux<DataBuffer> — поток байтов, который может быть прочитан только один раз. При попытке повторного чтения возникает ошибка.
Неправильный подход:
Решения проблемы
1. Кэширование тела в памяти (для небольших запросов):
2. Использование ModifyRequestBody для трансформации:
3. Работа с потоком без буферизации (для больших данных):
#Java #middle #Spring_Cloud_Gateway #Filters
Проблема повторного чтения body
Тело HTTP-запроса в реактивном стеке представлено как Flux<DataBuffer> — поток байтов, который может быть прочитан только один раз. При попытке повторного чтения возникает ошибка.
Неправильный подход:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ПЕРВОЕ чтение
return exchange.getRequest().getBody()
.collectList()
.flatMap(buffers -> {
// ВТОРОЕ чтение (ОШИБКА!)
return exchange.getRequest().getBody()
.collectList()
.flatMap(...);
});
}Решения проблемы
1. Кэширование тела в памяти (для небольших запросов):
@Component
public class CacheRequestBodyFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Фильтр, который кэширует тело для последующего использования
return DataBufferUtils.join(exchange.getRequest().getBody())
.defaultIfEmpty(EMPTY_BUFFER)
.flatMap(dataBuffer -> {
// Сохраняем кэшированное тело в атрибутах
exchange.getAttributes().put(CACHED_REQUEST_BODY, dataBuffer);
// Создаём новый запрос с кэшированным телом
ServerHttpRequest mutated = exchange.getRequest().mutate()
.body(Flux.just(dataBuffer))
.build();
return chain.filter(exchange.mutate().request(mutated).build());
});
}
public static DataBuffer getCachedBody(ServerWebExchange exchange) {
return exchange.getAttribute(CACHED_REQUEST_BODY);
}
}
2. Использование ModifyRequestBody для трансформации:
filters:
- name: ModifyRequestBody
args:
inClass: String
outClass: String
rewriteFunction: "#{@myTransformer}"
3. Работа с потоком без буферизации (для больших данных):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Преобразование потока на лету
Flux<DataBuffer> transformedBody = exchange.getRequest().getBody()
.map(dataBuffer -> {
// Трансформация каждого буфера
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
// Пример: преобразование в верхний регистр
String transformed = new String(bytes).toUpperCase();
return exchange.getResponse().bufferFactory()
.wrap(transformed.getBytes());
});
ServerHttpRequest mutated = exchange.getRequest().mutate()
.body(transformedBody)
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}#Java #middle #Spring_Cloud_Gateway #Filters
👍2