Java for Beginner
776 subscribers
773 photos
220 videos
12 files
1.3K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Работа с телом запроса: проблемы и решения

Проблема повторного чтения 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
Создание собственных фильтров

Реализация GatewayFilter через AbstractGatewayFilterFactory

Базовый фильтр с конфигурацией:
@Component
public class LoggingGatewayFilterFactory extends
AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

public LoggingGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// PRE-обработка
if (config.isLogRequest()) {
logRequest(exchange, config);
}

long startTime = System.nanoTime();

// POST-обработка через then()
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
if (config.isLogResponse()) {
long duration = System.nanoTime() - startTime;
logResponse(exchange, config, duration);
}
}));
};
}

public static class Config {
private boolean logRequest = true;
private boolean logResponse = true;
private LogLevel level = LogLevel.INFO;

// Геттеры и сеттеры
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("logRequest", "logResponse", "level");
}
}


Использование в YAML:
filters:
- Logging=true,true,DEBUG


Глобальные фильтры через GlobalFilter

Глобальные фильтры применяются ко всем маршрутам автоматически.

Пример: фильтр для добавления заголовка трассировки:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdGlobalFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Генерация или извлечение TraceId
String traceId = exchange.getRequest().getHeaders()
.getFirst("X-Trace-Id");

if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}

// Добавление в заголовки запроса
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();

// Добавление в атрибуты для использования в других фильтрах
exchange.getAttributes().put("traceId", traceId);

return chain.filter(exchange.mutate().request(mutated).build());
}
}


Фильтры с порядком выполнения

Порядок может быть задан через:
Аннотацию @Order
Реализацию интерфейса Ordered
Метод getOrder() в GatewayFilterFactory


Фильтр с динамическим порядком:

@Component
public class DynamicOrderFilter implements GlobalFilter, Ordered {

private final Environment environment;
private int order = 0;

public DynamicOrderFilter(Environment environment) {
this.environment = environment;
// Динамическое определение порядка из конфигурации
this.order = environment.getProperty("gateway.filter.order",
Integer.class, 0);
}

@Override
public int getOrder() {
return order;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Логика фильтра
return chain.filter(exchange);
}
}



#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Сложный фильтр с асинхронными операциями

Пример: фильтр с внешним вызовом для валидации:
@Component
public class ExternalValidationFilter implements GlobalFilter {

private final WebClient webClient;
private final CircuitBreaker circuitBreaker;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. Извлечение данных для валидации
String apiKey = exchange.getRequest().getHeaders()
.getFirst("X-API-Key");

if (apiKey == null) {
return unauthorized(exchange);
}

// 2. Асинхронная валидация через внешний сервис
return circuitBreaker.run(
webClient.get()
.uri("/validate?apiKey={key}", apiKey)
.retrieve()
.bodyToMono(ValidationResponse.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> Mono.just(new ValidationResponse(false))),
throwable -> Mono.just(new ValidationResponse(false))
)
.flatMap(validationResponse -> {
// 3. Обработка результата валидации
if (validationResponse.isValid()) {
// Добавление информации о пользователе в заголовки
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", validationResponse.getUserId())
.header("X-User-Roles",
String.join(",", validationResponse.getRoles()))
.build();

return chain.filter(exchange.mutate().request(mutated).build());
} else {
return unauthorized(exchange);
}
});
}

private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}


Фильтр с доступом к телу запроса

Безопасная работа с телом запроса:

@Component
public class RequestBodyLoggingFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Только для определенных Content-Type
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
if (contentType == null || !contentType.includes(MediaType.APPLICATION_JSON)) {
return chain.filter(exchange);
}

// Ограничение размера для логирования
final int maxBodySize = 1024 * 10; // 10KB

return DataBufferUtils.join(exchange.getRequest().getBody(), maxBodySize)
.defaultIfEmpty(EMPTY_BUFFER)
.flatMap(dataBuffer -> {
try {
// Чтение и логирование
byte[] bytes = new byte[Math.min(dataBuffer.readableByteCount(), maxBodySize)];
dataBuffer.read(bytes);

String body = new String(bytes, StandardCharsets.UTF_8);
log.debug("Request body (first {} bytes): {}", bytes.length, body);

// Восстановление тела
DataBuffer restoredBuffer = exchange.getResponse().bufferFactory()
.wrap(bytes);

// Продолжение цепочки с восстановленным телом
ServerHttpRequest mutated = exchange.getRequest().mutate()
.body(Flux.just(restoredBuffer)
.concatWith(exchange.getRequest().getBody()
.skipUntil(buffer -> false))) // Пропускаем уже прочитанное
.build();

return chain.filter(exchange.mutate().request(mutated).build());

} finally {
DataBufferUtils.release(dataBuffer);
}
});
}
}



#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Оптимизация и best practices

Избегание блокирующих операций

Неправильно (блокирующий вызов):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Блокирующий вызов в реактивном контексте
String result = blockingService.call(); // ОПАСНО!

return chain.filter(exchange);
}


Правильно (асинхронная обёртка):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Обёртка блокирующего вызова в отдельном пуле потоков
return Mono.fromCallable(() -> blockingService.call())
.subscribeOn(Schedulers.boundedElastic()) // Выделенный пул для блокирующих операций
.flatMap(result -> {
// Использование результата
return chain.filter(exchange);
});
}


Управление памятью и ресурсами

Освобождение DataBuffer:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
try {
// Работа с буфером
return processBuffer(dataBuffer)
.flatMap(result -> chain.filter(exchange));
} finally {
// Обязательное освобождение
DataBufferUtils.release(dataBuffer);
}
});
}



Мониторинг и метрики фильтров

@Component
public class MetricsGlobalFilter implements GlobalFilter {

private final MeterRegistry meterRegistry;
private final Map<String, Timer> timerCache = new ConcurrentHashMap<>();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.nanoTime();
String routeId = exchange.getAttribute(ROUTE_ATTRIBUTE_ID);

return chain.filter(exchange)
.doOnSuccess(v -> recordSuccess(routeId, startTime))
.doOnError(e -> recordError(routeId, startTime, e));
}

private void recordSuccess(String routeId, long startTime) {
long duration = System.nanoTime() - startTime;

Timer timer = timerCache.computeIfAbsent(routeId, id ->
Timer.builder("gateway.request.duration")
.tag("route", id)
.tag("status", "success")
.register(meterRegistry));

timer.record(duration, TimeUnit.NANOSECONDS);
}
}


#Java #middle #Spring_Cloud_Gateway #Filters
👍2