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

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

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
4. Типичная архитектура GraphQL-сервера (Spring Boot)

Структура проекта в Java обычно такая:
/graphql
/schema
schema.graphqls

/resolvers
QueryResolver.java
MutationResolver.java
UserResolver.java
PostResolver.java

/services
UserService.java
PostService.java

/repositories
UserRepository.java
PostRepository.java


Описание этапов:

Схема
Файл .graphqls описывает типы и операции.

Резолверы
Связывают поля схемы с Java-методами.

Сервисы
Логика бизнес-операций.

Репозитории
SQL/NoSQL/REST/gRPC слой, через который сервер реально получает данные.

Context
Сюда кладут:
JWT токен
объект пользователя
транзакции
общие ресурсы



5. Пример полного цикла запроса

Клиент отправляет запрос:

query {
user(id: 10) {
id
name
posts {
id
title
}
}
}


Что делает сервер:
Парсит запрос → AST
Проверяет типы по схеме
Вызывает QueryResolver.user(id=10)
Получает объект User
Чтобы отдать поле posts, вызывает UserResolver.posts(user)
Формирует объект ответа, возвращает клиенту JSON

Клиент всегда получает именно ту форму данных, которую указал.
Сервер не отдаёт лишних полей.



6. Почему GraphQL — это слой поверх данных, а не база данных

GraphQL:
не знает SQL;
не оптимизирует запросы;
не управляет транзакциями;
не индексирует данные;
не проверяет связи между таблицами.


GraphQL — это универсальный контракт между клиентом и сервером, а не способ хранения данных.

Он лишь обеспечивает:
строгую структуру данных (схему)
гибкие запросы от клиента
единый интерфейс ко множеству источников
оптимизацию на уровне поля (через резолверы)

Всё остальное — на стороне backend-логики.



#Java #middle #GraphQL
👍4
GraphQL vs REST vs gRPC

REST — ресурсно-ориентированный HTTP API, обычно JSON по HTTP/1.1. Прост, совместим с любым клиентом, хорош для публичных и простых API.
GraphQL — язык запросов + схема; клиент сам описывает форму данных. Один эндпоинт, строгая схема, гибкость запросов, удобен для фронтенда.
gRPC — RPC-фреймворк на основе HTTP/2 + Protocol Buffers; строгие контракты (.proto), бинарная сериализация, встроенный стриминг. Ориентирован на высокую производительность и внутренние сервисы.



Формат данных

REST → JSON (текстовый, человекочитаемый).
GraphQL → JSON-ответ, но запросы декларативны и строго типизированы схемой.
gRPC → Protocol Buffers (бинарный), компактно, фиксированные поля.
Вывод: protobuf обычно экономит трафик и парсится быстрее; JSON удобнее для дебага и совместимости.

Протокол транспорта

REST → HTTP/1.1 (обычно), можно HTTP/2, но без нативного стриминга.
GraphQL → обычно HTTP/1.1 POST/GET для Query/Mutation; Subscription — WebSocket или HTTP/2 SSE.
gRPC → HTTP/2 по умолчанию, мультиплексирование, потоковые фреймы, двунаправленность.
Вывод: для потоковых сценариев и мультиплексирования gRPC технически лучше.


Типы взаимодействия

REST → запрос/ответ (stateless).
GraphQL → query/mutation/subscription; один запрос может агрегировать много сущностей.
gRPC → unary, server streaming, client streaming, bidirectional streaming (всё на уровне протокола).
Вывод: если нужны сложные стримы и двунаправленные каналы — gRPC.


Типизация и контракт

REST → схема часто нестрогая (OpenAPI/Swagger добавляет контракт).
GraphQL → строгая схема (SDL).
gRPC → строгие контракты (.proto).
Вывод: GraphQL и gRPC обеспечивают статический контракт; REST требует дополнительных инструментов для той же дисциплины.


Производительность (в общих чертах)

gRPC обычно выигрывает по пропускной способности и латентности за счёт бинарной сериализации и HTTP/2.
GraphQL может быть эффективным по числу запросов (агрегация) и по уменьшению round-trip’ов, но JSON+резолверы добавляют накладные расходы.
REST проще и зачастую медленнее при больших объёмах и при множественных запросах с агрегацией.
Вывод: для внутренней коммуникации между сервисами с высокой нагрузкой — gRPC; для клиентского слоя — GraphQL/REST в зависимости от потребностей.


Эволюция API и версияция

REST → часто версионируют (/v1/) или добавляют параметры; ломкость при изменениях.
GraphQL → расширяемость без версий (добавление полей безопасно), депрекация поддерживается.
gRPC → совместимость через правила protobuf (резервирование, добавление новых полей безопасно при соблюдении правил).
Вывод: GraphQL и gRPC дают удобные инструменты для эволюции без жесткой версии.


Инструментальная поддержка и экосистема


REST → универсальная поддержка, простые средства тестирования.
GraphQL → богатая экосистема: GraphiQL, Apollo, Relay, Codegen. Отлично для фронтенда.
gRPC → генерация стубов под любые языки, хорошие SDK для серверов и клиентов; браузерная поддержка требует gRPC-Web.
Вывод: выбор зависит от клиентской среды: браузер напрямую лучше работает с REST/GraphQL; для gRPC нужен шлюз (gRPC-Web) или прокси.



#Java #middle #GraphQL
👍3
Практические сценарии

публичный HTTP API для сторонних клиентов (публичная документация)

Выбор: REST или GraphQL
Почему: совместимость, простота, отсутствие необходимости требованиям к бинарному протоколу.
REST, если API простое, CRUD-ориентированное, нужно кэширование по URL и широчайшая совместимость.
GraphQL, если клиенты (много разных) требуют разные наборы данных и важно уменьшить число запросов.


фронтенд (SPA, мобильные клиенты) с разнообразными представлениями

Выбор: GraphQL
Почему: клиент сам формирует shape ответов; экономия round-trips; отличная интеграция с Apollo/Relay; кодогенерация типов для TS/Swift/Kotlin.

внутренняя связь между микросервисами с высокой нагрузкой

Выбор: gRPC
Почему: низкая латентность, компактность, стриминг, строгие контракты, удобная кодогенерация для множества языков.

real-time (чат, телеметрия, видео/аудио)

Выбор: gRPC (bidirectional) или GraphQL Subscriptions (WebSocket)

Почему: gRPC даёт нативный поток; GraphQL Subscriptions удобны для фронтенда, но требуют инфраструктуры и могут быть сложнее в масштабировании.

интеграция с legacy REST-сервисами

Выбор: GraphQL в качестве BFF (Backend-for-Frontend) или REST-шлюз
Почему: GraphQL может агрегировать несколько REST-вызовов и отдать клиенту нужную структуру.


Почему часто комбинируют: GraphQL для клиентов, gRPC для микросервисов

Это распространённый и рациональный паттерн:
Внутренние сервисы общаются по gRPC (эффективно, типобезопасно, стриминг).
BFF / API Gateway (или отдельный GraphQL-сервер) агрегирует данные из gRPC/REST/БД и предоставляет фронту единый, гибкий интерфейс.
Фронтенд работает с GraphQL (или REST), не заботясь о том, как именно данные доставлены внутри инфраструктуры.


Преимущества паттерна:
Отделение оптимизации внутренней сетевой коммуникации (gRPC) от удобства клиентских API (GraphQL).
Централизация логики агрегации и адаптации под клиентов.
Возможность менять внутреннюю реализацию без ломки фронта.


Пример (псевдокод Java resolver, вызывающий gRPC-stub):
// GraphQL resolver
public class UserResolver {
private final UserGrpc.UserBlockingStub userStub;

public UserResolver(UserGrpc.UserBlockingStub userStub) {
this.userStub = userStub;
}

public User getUser(String id) {
UserRequest req = UserRequest.newBuilder().setId(id).build();
UserResponse resp = userStub.getUser(req); // gRPC call to internal service
return mapToGraphQLUser(resp);
}
}



Детальный разбор преимуществ и ограничений

Удобство разработки
REST: быстро стартовать, понятная модель. Но при сложных клиентских потребностях растёт число эндпоинтов.
GraphQL: позволяет фронтенду быстро изменять данные без координации с бэком, но требует работы со схемой и авторизацией на уровне полей.
gRPC: требует .proto и генерации стабов, но даёт сильную типизацию и меньше рутинного кода при изменениях.


Кеширование
REST: простое кеширование по URL (HTTP caches, CDNs).
GraphQL: кеширование сложнее — операция может возвращать разные поля; решения: persisted queries, apollo cache, CDN на уровне persisted queries/operation id.
gRPC: кеширование на транспортном уровне сложнее; обычно кешируют ответы в сервисах.


Отладка и наблюдаемость
REST: легко отлаживать (curl, браузер).
GraphQL: дебаг через GraphiQL/Apollo Studio; трассировка полей сложнее (нужны field-level metrics).
gRPC: бинарные пакеты труднее смотреть вручную; нужно подходящие инструменты (grpcurl, tshark + protobuf descriptors).


Безопасность и авторизация


REST: стандартные механизмы (OAuth, JWT, TLS).
GraphQL: нужно управлять авторизацией на уровне полей (field-level), чтобы не раскрывать данные; также важны depth-limiting, query complexity limiting, persisted queries.
gRPC: поддерживает mTLS, аутентификацию/авторизацию через metadata; access control реализуется в сервисах.


Количество сетевых вызовов / latency
GraphQL часто уменьшает RTT за счёт агрегации (один запрос вместо множества).
gRPC уменьшает сетевые накладные расходы: меньше байт, лучше TCP connection reuse, HTTP/2 мультиплексирование.



#Java #middle #GraphQL
👍2
Практические шаблоны интеграции (patterns)

1) BFF (Backend-for-Frontend) — GraphQL на фронте + gRPC внутри

GraphQL агрегатор (BFF) вызывает gRPC сервисы, комбинирует ответы и отдаёт клиенту.
Позволяет хранить оптимизированные внутр. контракты и независимую клиентскую схему.


2) API Gateway с трансляцией

gRPC Gateway (прокси) экспонирует REST/JSON поверх gRPC-сервисов или наоборот.
Полезно для совместимости с внешними клиентами.


3) Dual API

Предоставлять одновременно REST (для публичного потребления) и GraphQL (для интерактивного фронта).
Поддерживать один источник данных и разные фасады.

4) Persisted Queries + CDN

Для GraphQL: генерировать хэш-запросов (operationId) и кэшировать на CDN; уменьшает payload и риск DoS.


Частые ошибки

Выбор gRPC для публичного браузерного API без gRPC-Web — приведёт к дополнительной сложности (нужен прокси).
GraphQL без контроля сложности — клиенты могут генерировать тяжёлые запросы (глубокая вложенность). Нужно ставить лимиты.
REST для сложной фронт-логики — приведёт к множеству эндпоинтов и оверхеду в клиенте.
Игнорирование кэширования в GraphQL — потеря преимуществ CDN/edge caching; нужен persisted queries или отдельные REST-эндпоинты для тяжелых ресурсов.



Чек-лист для принятия решения (практический)

Клиенты — браузеры? мобильные? сторонние интеграторы?
браузер/мобильный фронт → GraphQL выгоден;
сторонние сторонние потребители → REST предпочтителен (совместимость).

Нужен ли стриминг / двунаправленная связь?
да → gRPC;
нет → GraphQL/REST.


Высокая нагрузка внутри сети (RTT, пропускная способность)?
да → gRPC;

Требуется гибкость выбора полей клиентом и уменьшение запросов?
да → GraphQL;

Требуется простое кеширование через CDN/HTTP?
да → REST (или реализовать persisted queries для GraphQL).

Нужно строгая схема и codegen для многих языков?
gRPC или GraphQL (оба дают schema/codegen).


Рекомендованные архитектурные сочетания (рецепты)

Внутренние микросервисы (gRPC) + GraphQL BFF + браузерный фронт — оптимальный вариант для больших команд: скорость внутри, гибкость для фронтенда.
REST public API + GraphQL internal BFF for web clients — если нужно максимально простое публичное API, но гибкость для своих фронтенд-команд.
gRPC end-to-end — когда все клиенты контролируются и могут использовать gRPC (например, мобильный клиент с gRPC-Web или нативный клиент).


Пример архитектуры

Сценарий: крупное приложение с веб-клиентом и мобильными приложениями + множество микросервисов.
Внутренние микросервисы: общаются по gRPC (protobuf).
Aggregation Layer: GraphQL BFF. Он:
вызывает gRPC сервисы (stub-ы),
использует DataLoader / batching для борьбы с N+1,
кеширует часто запрашиваемые фрагменты,
реализует field-level авторизацию.
Фронтенд: запрашивает GraphQL; для критичных статических ресурсов (изображения) используется CDN.
Преимущество: фронтенд получает гибкий API, внутренние сервисы остаются быстрыми и типобезопасными.



#Java #middle #GraphQL
👍2
Advanced GraphQL: реактивность и Federation

GraphQL уже давно не ограничивается статическими запросами к одной базе.

Современные системы требуют:
реактивности: live updates, push-события на фронтенд;
масштабируемости: объединение схем из разных сервисов;
микросервисной интеграции: разные источники данных и форматы;
единый клиентский интерфейс: фронт видит единую схему, хотя данные приходят из нескольких микросервисов.


Эти задачи решаются через Subscriptions, Federation, Schema Stitching, GraphQL Gateway.


1. Subscriptions и live updates

1.1 Что такое Subscription

Subscription — это тип операции GraphQL, который подписывается на события и получает данные по мере их появления, в отличие от Query/Mutation, где данные запрашиваются один раз.

Используется для:
чатов и уведомлений;
реального мониторинга (метрики, логи);
обновления UI при изменении данных на сервере.


1.2 Механика на сервере

Клиент подписывается на событие через WebSocket или Server-Sent Events (SSE).
Сервер регистрирует подписку и хранит её в памяти или через pub/sub (Redis, Kafka).
При событии вызываются соответствующие резолверы Subscription, результат отправляется клиенту.


1.3 Пример на Spring Boot с graphql-java

Схема (schema.graphqls)
type Subscription {
postAdded: Post!
}


Резолвер Subscription
@Component
public class PostSubscription implements GraphQLSubscriptionResolver {

private final Publisher<Post> postPublisher;

public PostSubscription(Publisher<Post> postPublisher) {
this.postPublisher = postPublisher;
}

public Publisher<Post> postAdded() {
return postPublisher;
}
}


Публикация события (например, после мутации)
@Component
public class PostMutation implements GraphQLMutationResolver {

private final Publisher<Post> postPublisher;
private final PostService postService;

public PostMutation(PostService postService, Publisher<Post> postPublisher) {
this.postService = postService;
this.postPublisher = postPublisher;
}

public Post createPost(CreatePostInput input) {
Post newPost = postService.create(input);
postPublisher.publish(newPost); // пушим в подписчиков
return newPost;
}
}

Таким образом фронтенд автоматически получает новые посты без повторных запросов.


1.4 Реактивная интеграция с gRPC

Микросервис может уведомлять GraphQL через gRPC стриминг (Server Streaming).
GraphQL Gateway принимает события и пушит их клиентам через Subscription.
Реализуется через Publisher или Flux (Project Reactor) в Java.


Пример с Project Reactor:
public Publisher<Post> postAdded() {
return Flux.from(postGrpcStub.subscribePosts());
}



#Java #middle #GraphQL
👍2
2. Federation / Schema stitching

2.1 Зачем нужна Federation

В микросервисной архитектуре каждая команда может иметь свой GraphQL-сервис.
Фронтенду нужен единый endpoint, а не десятки отдельных.

Schema stitching: объединяет схемы в один endpoint вручную.

Apollo Federation: более продвинутый стандарт, позволяющий каждому сервису быть федеративным узлом.

2.2 Принцип работы Federation

Subgraph Service — каждый сервис предоставляет свою часть схемы: User, Post, Comment.
Gateway / Apollo Gateway — объединяет схемы subgraph и решает, какой сервис вызывать для каждого запроса.
Reference resolver — позволяет связать типы из разных сервисов (например, User в Post).

Пример на Java (с Spring Boot + GraphQL Federation, библиотека graphql-java-federation):

Сервис Users
type User @key(fields: "id") {
id: ID!
name: String!
}


Сервис Posts
type Post {
id: ID!
title: String!
author: User @provides(fields: "name")
}



Java resolver для
Post.author
@Component
public class PostResolver implements GraphQLResolver<Post> {

private final UserGrpc.UserBlockingStub userStub;

public PostResolver(UserGrpc.UserBlockingStub userStub) {
this.userStub = userStub;
}

public User author(Post post) {
UserRequest req = UserRequest.newBuilder().setId(post.getAuthorId()).build();
UserResponse resp = userStub.getUser(req);
return mapToGraphQLUser(resp);
}
}

Gateway собирает всю федеративную схему и возвращает фронтенду единый API.



3. GraphQL Gateway и объединение данных

3.1 Роль Gateway

Аггрегирует данные из нескольких микросервисов (REST, gRPC, базы, Kafka).
Решает проблемы N+1 через batching (DataLoader).
Управляет кешированием и throttling.
Поддерживает Subscriptions и Federation.


3.2 Пример архитектуры
[Frontend SPA / Mobile] --GraphQL--> [GraphQL Gateway] --gRPC--> [UserService]
|--> [PostService]
|--> [CommentService]
|--> [External REST API]


Особенности:

Gateway использует DataLoader для агрегации запросов, уменьшения количества вызовов к сервисам.
Subscriptions могут получать события из gRPC стримов или Kafka и пушить клиенту.



4. Примеры использования и кейсы

4.1 Live feed

Мобильное приложение подписывается на postAdded.
PostService пушит новые посты через gRPC или внутренний EventBus.
Gateway трансформирует данные в GraphQL Subscription → клиент получает обновления моментально.


4.2 Микросервисная интеграция


UserService и PostService разрабатываются разными командами.
Gateway объединяет их схемы через Federation.
Фронтенд видит единый API: user(id: 1) { name posts { title } }, не зная, что posts приходит из другого сервиса.


4.3 Agreggation + caching

Gateway кеширует данные User на 5 минут.
PostService вызывается только для новых постов.
DataLoader агрегирует все запросы к UserService за одну операцию.



5. Лучшие практики

Использовать Federation для масштабируемых командных проектов.
Subscription через WebSocket + Publisher/Flux для реактивных интерфейсов.
DataLoader для оптимизации N+1 вызовов в распределённых сервисах.
Разделять ответственность: микросервисы предоставляют свои типы и резолверы, Gateway агрегирует.
Event-driven подход для live updates: gRPC streaming, Kafka, Redis Pub/Sub.
Мониторинг: трассировка на уровне каждого subgraph, latency, throughput.
Эволюция схем: добавление новых полей без ломки клиентов, депрекация старых.



#Java #middle #GraphQL
👍2
Spring Cloud Gateway: Архитектурный страж микросервисов

В монолитной архитектуре приложение имеет одну точку входа — HTTP-порт, через который проходят все запросы. Клиент взаимодействует с единым целым. При переходе к микросервисам эта модель разрушается: вместо одного приложения появляются десятки или сотни независимых сервисов, каждый со своим API и сетевым адресом. Прямое обращение клиентов ко всем сервисам создаёт фундаментальные проблемы: клиент должен знать топологию сети, обеспечивать отказоустойчивость для каждого вызова, дублировать логику аутентификации и преобразования данных. Именно здесь возникает необходимость в паттерне API Gateway — единой интеллектуальной точке входа, которая инкапсулирует внутреннюю структуру системы и предоставляет клиентам унифицированный интерфейс. Spring Cloud Gateway (SCG) — это реализация этого паттерна в экосистеме Spring, построенная на реактивной парадигме для удовлетворения требований современных высоконагруженных распределённых систем.


Архитектурное позиционирование и место в экосистеме

Spring Cloud Gateway функционирует как шлюз прикладного уровня (Layer 7) в модели OSI.
Его позиция строго определена: между внешними клиентами (мобильные приложения, браузеры, сторонние системы) и внутренним кластером микросросервисов. Он не является заменой балансировщику нагрузки сетевого уровня (например, AWS NLB или hardware-балансировщику), но работает в тесной связке с ним. Типичная многоуровневая архитектура включает внешний балансировщик, который распределяет трафик между несколькими инстансами SCG для обеспечения отказоустойчивости и масштабируемости, а сам SCG уже занимается интеллектуальной маршрутизацией к конкретным сервисам.

В экосистеме Spring Cloud Gateway является эволюционным преемником Zuul 1.x. Zuul 1, построенный на блокирующем сервлетном API, имел архитектурные ограничения, связанные с выделением потока на каждый соединение, что создавало проблемы при большом количестве одновременных соединений, особенно с длительными запросами (например, Server-Sent Events, WebSockets). SCG был создан с нуля на реактивном стеке Spring WebFlux и проекте Reactor, что позволило реализовать полностью неблокирующую, асинхронную архитектуру, способную эффективно работать с тысячами одновременных соединений на скромных аппаратных ресурсах. Это стратегическое выравнивание с реактивной парадигмой, которую Spring продвигает для построения масштабируемых систем.


#Java #middle #Spring_Cloud_Gateway
👍3
Детальный разбор решаемых проблем

1. Интеллектуальная маршрутизация и абстракция сервисов
Базовая и критически важная функция — динамическая маршрутизация запроса к соответствующему backend-сервису на основе содержимого запроса. SCG анализирует HTTP-запросы (путь, заголовки, параметры) и, используя механизм предикатов, определяет, какой маршрут должен быть применён. Каждый маршрут связан с определённым URI назначения (например, lb://SERVICE-NAME при использовании Service Discovery через Spring Cloud LoadBalancer). Это позволяет полностью скрыть от клиента реальные сетевые адреса и даже имена хостов сервисов. Клиент обращается к api.company.com/orders, а шлюз решает, что этот запрос должен уйти в сервис order-service-v2, работающий в трёх экземплярах. Механизм балансировки нагрузки на стороне клиента (client-side load balancing) интегрирован непосредственно в процесс проксирования.

2. Централизованная безопасность и контроль доступа

Вместо того чтобы встраивать идентификацию и авторизацию в каждый микросервис, что приводит к дублированию кода и сложности управления политиками, SCG позволяет централизовать эту логику. Типичный сценарий: фильтр шлюза (Gateway Filter) перехватывает входящий запрос, извлекает JWT-токен из заголовка Authorization, валидирует его подпись и срок действия, декодирует claims (утверждения) и либо добавляет обогащённую информацию (например, роли пользователя) в заголовки для передачи в нижестоящий сервис, либо сразу отвергает запрос с кодом 401 или 403. Это реализует паттерн "валидация токена на периметре". Таким образом, внутренние сервисы могут доверять данным из заголовков (которые, впрочем, должны проверяться на целостность в средах с высокими требованиями безопасности), что избавляет их от необходимости иметь доступ к секретам для проверки подписи JWT.

3. Применение кросс-сервисных политик

Многие требования, такие как ограничение частоты запросов (rate limiting), применяются на уровне всего API или конкретного пользователя, а не отдельного сервиса. SCG может интегрироваться с системами вроде Redis для реализации алгоритмов ограничения (например, "token bucket" или "sliding window"). Фильтр шлюза считает количество запросов с определённого ключа (IP, user ID, API key) за временное окно и блокирует превысившие лимит запросы до того, как они создадут нагрузку на бизнес-сервисы. Аналогично реализуется политика Circuit Breaker (размыкатель цепи): SCG отслеживает ошибки или задержки при вызовах к конкретному сервису и при достижении порога временно "разрывает цепь", перенаправляя запросы на заранее определённый fallback-ответ (например, кэшированные данные или заглушку), не нагружая падающий сервис. Это повышает отказоустойчивость всей системы.

4. Наблюдаемость и анализ трафика (Observability)
Как критически важный узел, через который проходит весь трафик, SCG является идеальным местом для сбора телеметрии. Он может автоматически генерировать метрики (например, количество запросов в секунду, задержки, процент ошибок) для каждого маршрута и экспортировать их в системы мониторинга типа Prometheus через Micrometer. Кроме того, он присваивает и распространяет уникальные идентификаторы запросов (trace ID), которые позволяют агрегаторам трассировки, таким как Zipkin или Sleuth, восстановить полный путь запроса по всем микросервисам. Это обеспечивает сквозную видимость, без которой отладка распределённой системы крайне затруднена.

5. Трансформация запросов и ответов
SCG выполняет роль адаптера между внешним и внутренним API. Это может быть простая перезапись путей (rewrite path): клиент отправляет запрос на /api/v1/user, а шлюз перенаправляет его на внутренний сервис по пути /user. Более сложные сценарии включают модификацию заголовков (добавление, удаление), трансформацию тела запроса/ответа (например, из XML в JSON) с помощью встроенных или кастомных фильтров. Это позволяет внутренним сервисам эволюционировать независимо от клиентов, а шлюзу — обеспечивать обратную совместимость.


#Java #middle #Spring_Cloud_Gateway
👍2🤯1
Конкурентный ландшафт и выбор технологии

Рынок gateway-решений насыщен. SCG не единственный и не универсальный вариант.

Kong
Сильный, промышленный, плагинно-ориентированный API-gateway на базе NGINX и LuaJIT. Скорее всего превосходит SCG по пропускной способности на уровне низкоуровневой сети. Но менее гибок для JVM-экосистемы и глубокой интеграции с Spring.

NGINX
Высокопроизводительный L7-proxy, но без богатой программируемости. SCG выигрывает там, где нужны сложные фильтры и логика на Java/Kotlin.

Envoy
Современный proxy, облачная стандартизация, основа для Istio. Максимально производительный, нативный, ориентирован на service mesh. SCG выигрывает на уровне интеграции с приложением и кастомизируемости внутри JVM.

Traefik
Простой, легкий, динамический, ориентирован на Docker/Kubernetes. SCG более мощен при построении сложных политик, при наличии Spring-экосистемы.

Spring Cloud Gateway vs. Spring MVC (и Zuul 1)

Здесь различие фундаментально и лежит в плоскости архитектурной парадигмы.
Spring MVC и Zuul 1 построены на сервлетной модели, которая привязывает каждый HTTP-запрос к потоку (thread) на всё время его обработки. Потоки — дорогой ресурс, их количество в пуле ограничено (часто 200-500). Когда все потоки заняты ожиданием ответа от медленного backend-сервиса, шлюз перестаёт принимать новые запросы, даже если CPU простаивает. SCG, построенный на WebFlux и Reactor Netty, использует событийно-ориентированную, неблокирующую модель. Небольшое количество потоков (часто равное количеству ядер CPU) обрабатывает множество соединений. Когда запрос проксируется к backend-сервису, поток не блокируется в ожидании ответа, а освобождается для обработки других событий (новых запросов или пришедших ответов). Колбэк вызывается, когда ответ готов. Это позволяет одному экземпляру SCG эффективно обслуживать десятки тысяч одновременных long-lived соединений (например, для streaming API) с предскатуемым потреблением памяти. С точки зрения JVM, это означает отказ от пула потоков Tomcat/Jetty и работу на основе NIO-селекторов в Netty, где события диспетчеризуются ядром Reactor.


Архитектура: Reactor Netty и WebFlux

Работа Spring Cloud Gateway начинается с автоконфигурации Spring Boot. Ядром является ReactorNettyHttpPredicateHandlerMapping. Весь входящий трафик принимается сервером Netty, который работает на реактивном канале HttpServer.

Когда Netty принимает новый HTTP-запрос, он преобразует его в реактивный тип ServerHttpRequest и запускает цепочку обработки.

Основной цикл выглядит так:

Сопоставление маршрута (Route Predicate Handler Mapping): Для входящего ServerHttpRequest проверяются все определённые в контексте маршруты (Route). Каждый маршрут содержит коллекцию Predicate. Предикаты — это условия на основе запроса (например, Path=/api/**, Header=X-Request-Id, Method=GET). Проверка выполняется последовательно до первого совпадения. Этот процесс не блокирующий, все предикаты оцениваются в том же реактивном потоке.

Сборка цепочки фильтров (Filtering Web Handler): Найденный маршрут содержит упорядоченный список фильтров (GatewayFilter) и URI назначения. Формируется цепочка обработки DefaultGatewayFilterChain. Фильтры делятся на два типа: "pre-filters" (выполняются до вызова проксируемого сервиса) и "post-filters" (выполняются после получения ответа). Сам вызов проксируемого сервиса также реализован как специальный фильтр — NettyRoutingFilter.

Выполнение цепочки фильтров (Reactive Pipeline): Цепочка выполняется как реактивный пайплайн. Например, пре-фильтр аутентификации проверяет токен, используя реактивный ReactiveJwtDecoder, который не блокирует поток. Если вызов к сервису ключей необходим, он выполняется асинхронно. Затем фильтр преобразования путей модифицирует ServerHttpRequest. Ключевой фильтр LoadBalancerClientFilter взаимодействует с реактивным ReactorLoadBalancer для преобразования логического имени сервиса (из lb://SERVICE) в реальный физический адрес, выбранный с учётом балансировки.


#Java #middle #Spring_Cloud_Gateway
👍1🤯1
Проксирование запроса (NettyRoutingFilter): Это кульминация. Фильтр берет обогащённый ServerHttpRequest, конвертирует его в запрос Netty (HttpClientRequest) и отправляет через неблокирующий HTTP-клиент Netty (HttpClient) к целевому сервису. Клиент Netty использует ту же событийно-ориентированную модель. Запрос ставится в очередь на отправку, и текущий поток немедленно освобождается. Когда от backend-сервиса приходит ответ (HttpClientResponse), Netty генерирует событие, которое подхватывается реактивным пайплайном, преобразуя ответ в ServerHttpResponse.

Обработка ответа (Post-Filters): Запускается "post-filter" часть цепочки. Здесь могут работать фильтры добавления стандартных заголовков, логирования, преобразования тела ответа. Итоговый ServerHttpResponse записывается в исходный канал Netty к клиенту.

В памяти JVM это проявляется как доминирование объектов реактивных стримов (Mono, Flux), цепочек операторов и лямбда-выражений в heap. Стек вызовов глубокий, но асинхронный: в дампе потока вы не увидите блокирующего вызова HttpClient. Вместо этого увидите фреймы типа onNext, onComplete из реализации Reactor. Пул потоков Netty (обычно reactor-http-nio-) активен, но количество потоков мало. Основное потребление памяти связано с буферами Netty для HTTP-сообщений (которые используют пул ByteBuf для эффективного управления), и объектами, представляющими запросы/ответы.


Типичные сценарии реализации

Rate Limiting с использованием Redis: Фильтр, реализующий алгоритм "скользящего окна". Для каждого запроса вычисляется ключ (например, user:123:path:/api/orders). С помощью реактивного клиента Redis (ReactiveRedisTemplate) выполняются команды ZREMRANGEBYSCORE (удаление старых записей) и ZADD + ZCOUNT (добавление текущего запроса и подсчёт количества запросов в окне). Вся эта последовательность выполняется как атомарный Lua-скрипт на стороне Redis для обеспечения консистентности. Если лимит превышен, цепочка прерывается с возвратом 429 Too Many Requests.

Аутентификация и передача контекста: Кастомный GatewayFilter в порядке pre извлекает JWT из заголовка. Используя ReactiveJwtDecoder (который может кэшировать JWK для проверки подписи), токен декодируется. Из claims извлекается идентификатор пользователя и его роли, которые затем добавляются в заголовки запроса, например, X-User-Id и X-User-Roles. Важный нюанс: для предотвращения подмены заголовков внутренними сервисами, эти заголовки должны либо очищаться шлюзом от входящих значений, либо внутренние сервисы должны доверять только конкретным заголовкам, установленным шлюзом (что может быть обеспечено настройками сетевой безопасности).

API Composition (Агрегация): Хотя SCG не является специализированным агрегатором (как GraphQL BFF), он может выполнять простую агрегацию с помощью фильтра ModifyResponseBodyGatewayFilterFactory. Например, клиенту нужны данные пользователя вместе с его последним заказом. Шлюз может последовательно вызвать user-service и, используя данные из ответа, вызвать order-service, а затем объединить результаты в единый JSON. Эта операция выполняется неблокирующе с помощью операторов Reactor flatMap или zip. Однако для сложных агрегаций с множественными зависимостями предпочтительнее выделенный BFF-сервис.

Circuit Breaker с Resilience4J: SCG интегрируется с Resilience4J через конфигурацию. Для маршрута определяется конфигурация Circuit Breaker с параметрами порога ошибок, временем ожидания в полуоткрытом состоянии и т.д. Когда фильтр активирован, все вызовы через него оборачиваются в защитный контур. В случае открытия контура запросы не идут к падающему сервису, а перенаправляются на заданный fallbackUri, который может указывать на статический ответ или простой сервис-заглушку, возвращающий данные из кэша.


#Java #middle #Spring_Cloud_Gateway
👍1🤯1
Архитектура Spring Cloud Gateway: Внутренние компоненты и жизненный цикл запроса

В основе Spring Cloud Gateway лежат три ключевые абстракции, которые формируют декларативную модель конфигурации: Route, Predicate и Filter. Эти сущности организованы в строгую иерархию, где каждая выполняет свою роль в обработке входящего запроса.

Route (Маршрут)

Является центральной конфигурационной единицей. Он определяет полный путь обработки конкретного запроса от получения до возврата ответа. Маршрут инкапсулирует три основных элемента: уникальный идентификатор, целевой URI (куда будет перенаправлен запрос после обработки) и упорядоченные коллекции предикатов и фильтров. Внутренне маршрут представлен классом org.springframework.cloud.gateway.route.Route, который является иммутабельным объектом. Иммутабельность критически важна, поскольку маршруты могут динамически обновляться в runtime (например, через обновление конфигурации из Spring Cloud Config), и необходимо гарантировать согласованное состояние во время обработки запроса.

Конфигурация маршрута в YAML демонстрирует эту структуру:
spring:
cloud:
gateway:
routes:
- id: user_service_route
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- AddRequestHeader=X-Gateway-Tag, processed


Predicate (Предикат)
Условие, которое определяет, должен ли данный маршрут быть применён к входящему запросу. Предикаты реализуют функциональный интерфейс Predicate<ServerWebExchange>, где ServerWebExchange является контейнером для HTTP-запроса и ответа, а также дополнительных атрибутов, накопленных в процессе обработки. Предикаты оцениваются в определённом порядке, и первый маршрут, чьи предикаты возвращают true для данного ServerWebExchange, выбирается для дальнейшей обработки. Типичные предикаты включают проверку пути (Path=/api/**), метода HTTP (Method=GET,POST), наличия заголовков (Header=X-Request-Id, \\d+), параметров запроса, хоста, кук и даже сложные временные условия (After=2023-01-20T17:42:47.789-07:00[America/Denver]). Механизм предикатов позволяет реализовать сложную логику маршрутизации без написания imperative-кода.

Filter (Фильтр)
Компонент, который модифицирует ServerWebExchange до или после вызова целевого сервиса. Фильтры организованы в цепочку (Gateway Filter Chain) и выполняются в строгом порядке. Они реализуют интерфейс GatewayFilter, который содержит единственный метод Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain). Фильтры разделяются на две категории по моменту выполнения: "pre-filters" выполняются до передачи запроса целевому сервису (модификация запроса, аутентификация, логирование), а "post-filters" — после получения ответа от целевого сервиса (модификация ответа, добавление заголовков, метрик). Порядок выполнения фильтров внутри цепочки определяется их конфигурацией в маршруте.


Reactor Netty как HTTP-сервер: архитектурный фундамент

Spring Cloud Gateway построен на реактивном стеке Spring WebFlux, который в свою очередь использует Reactor Netty в качестве HTTP-сервера. Это фундаментальный архитектурный выбор, отличающий SCG от традиционных сервлетных контейнеров.


#Java #middle #Spring_Cloud_Gateway
Netty — это асинхронный event-driven фреймворк сетевого программирования, работающий на уровне NIO (Non-blocking I/O). В контексте JVM это означает, что вместо использования blocking socket operations и пула потоков (thread-per-connection модель), Netty использует небольшое количество потоков-селекторов (event loop threads), которые обрабатывают события на множестве соединений. Каждое соединение ассоциировано с каналом (Channel), а события (чтение данных, запись данных, изменение состояния соединения) диспетчеризуются через цепочки обработчиков (ChannelPipeline).

Когда Spring Boot приложение со SCG стартует, автоконфигурация ReactorNettyAutoConfiguration создаёт и настраивает экземпляр HttpServer Netty. Конфигурация по умолчанию устанавливает количество потоков event loop group равным количеству доступных процессорных ядер (Runtime.getRuntime().availableProcessors()), что является оптимальным для CPU-bound операций. Каждый поток event loop обслуживает множество соединений, переключаясь между ними по мере появления событий ввода-вывода.

В памяти JVM это приводит к совершенно иной структуре по сравнению с традиционными сервлетными контейнерами. Вместо большого пула потоков (200-500 объектов Thread в heap), каждый со своим стеком (1MB по умолчанию), создаётся небольшое количество долгоживущих потоков Netty. Основные структуры данных в heap — это буферы ByteBuf (которые Netty эффективно пуллит через ByteBufAllocator), объекты Channel и их контексты, а также реактивные потоки (Mono, Flux) и их операторы, представляющие pipeline обработки запроса.


Жизненный цикл запроса: от байтов в сокете до ответа

Фаза 1: Обработка в Netty и преобразование в ServerWebExchange


Когда клиент устанавливает TCP-соединение и отправляет HTTP-запрос, Netty event loop thread получает событие channelRead. Сырые байты из сокета декодируются в объект HttpRequest Netty. Затем через адаптер ReactorServerHttpRequest этот запрос оборачивается в реактивный тип ServerHttpRequest Spring WebFlux. Создаётся контейнер ServerWebExchange, который будет нести состояние запроса через весь pipeline обработки. Критически важно, что на этом этапе тело запроса ещё не читается полностью — оно представлено как реактивный поток Flux<DataBuffer>, что позволяет обрабатывать запросы потоково, без буферизации всего тела в памяти.

Фаза 2: Сопоставление маршрута через HandlerMapping


Обработка передаётся в DispatcherHandler Spring WebFlux, который ищет подходящий обработчик для запроса. В контексте SCG ключевым является RoutePredicateHandlerMapping — специализированная реализация HandlerMapping. Его задача — найти подходящий маршрут для текущего запроса.

Процесс сопоставления начинается с получения всех доступных маршрутов через RouteLocator. RouteLocator — это абстракция, которая предоставляет поток маршрутов. Реализация по умолчанию CachingRouteLocator кэширует маршруты для производительности, но поддерживает механизмы инвалидации при динамическом обновлении конфигурации. Каждый маршрут проверяется последовательно: для каждого предиката маршрута вызывается метод test(ServerWebExchange). Проверка предикатов выполняется синхронно (хотя сами предикаты могут выполнять асинхронные операции) до первого совпадения.

Сложность предиката Path демонстрирует детали реализации: при конфигурации Path=/api/users/** создаётся PathRoutePredicateFactory. Внутри он использует PathPatternParser из Spring WebFlux для компиляции строки шаблона в оптимизированную структуру данных PathPattern. При сопоставлении выполняется не простое строковое сравнение, а эффективный алгоритм сопоставления с извлечением переменных пути (например, /api/users/{id}). Это существенно быстрее, чем регулярные выражения, и не создает издержек при большом количестве маршрутов.

Когда маршрут найден, он сохраняется в атрибутах ServerWebExchange под ключом Route.class.getName(), и управление передаётся соответствующему обработчику — FilteringWebHandler.


#Java #middle #Spring_Cloud_Gateway
Фаза 3: Выполнение цепочки фильтров

FilteringWebHandler — это сердце логики преобразования запроса. Он получает выбранный маршрут и строит цепочку фильтров, упорядочивая их согласно конфигурации. Цепочка представляет собой реактивный pipeline, где каждый фильтр — это оператор, трансформирующий ServerWebExchange.

Порядок выполнения фильтров строго определён:
Сначала выполняются все GlobalFilter (глобальные фильтры), зарегистрированные в контексте приложения. Глобальные фильтры выполняются в порядке, определённом их значением getOrder().
Затем выполняются фильтры, специфичные для маршрута, в том порядке, в котором они объявлены в конфигурации маршрута.

Каждый фильтр получает контроль над ServerWebExchange и может либо модифицировать его, либо передать управление следующему фильтру в цепочке через вызов chain.filter(exchange), либо завершить обработку, вернув ответ непосредственно из шлюза. Последний сценарий используется, например, когда фильтр аутентификации обнаруживает невалидный токен и возвращает 401 Unauthorized.

Пример кастомного pre-фильтра для логирования:
@Component
public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

private static final Logger log = LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);

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

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();

log.info("Incoming request: {} {} from {}",
request.getMethod(),
request.getURI().getPath(),
request.getRemoteAddress());

return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();

log.info("Completed request: {} {} -> {} in {}ms",
request.getMethod(),
request.getURI().getPath(),
response.getStatusCode(),
duration);
}));
};
}

public static class Config {
// Конфигурационные свойства фильтра
}
}

Этот фильтр демонстрирует важный паттерн: логирование в pre-фильтре, а измерение времени и логирование результата — в post-части, реализованной через then(Mono.fromRunnable(...)). Обратите внимание, что весь фильтр — это функция, возвращающая Mono<Void>, и логирование выполняется в реактивном стиле без блокирования потоков.



#Java #middle #Spring_Cloud_Gateway
Фаза 4: Проксирование запроса к целевому сервису

После выполнения всех pre-фильтров управление доходит до ключевого фильтра — NettyRoutingFilter (или WebClientHttpRoutingFilter в альтернативной реализации). Именно этот фильтр выполняет фактическое проксирование запроса к целевому сервису.

Процесс проксирования включает несколько шагов:
Преобразование URI назначения: Если URI маршрута использует схему lb:// (например, lb://USER-SERVICE), вызывается LoadBalancerClientFilter (или реактивный ReactorLoadBalancer), который преобразует логическое имя сервиса в физический адрес, выбирая конкретный экземпляр с учётом алгоритма балансировки нагрузки.

Подготовка запроса прокси: NettyRoutingFilter создаёт новый HTTP-запрос Netty, копируя метод, заголовки и тело из оригинального запроса. При этом он может применять трансформации, определённые фильтрами (например, перезапись пути, добавление заголовков).

Асинхронное выполнение запроса: Запрос отправляется через реактивный HTTP-клиент Netty (HttpClient). Клиент Netty работает в неблокирующем режиме — он ставит запрос в очередь на отправку и немедленно возвращает Mono<HttpClientResponse> без блокировки потока. Event loop thread освобождается для обработки других соединений.

Обработка ответа: Когда от целевого сервиса приходит ответ, Netty генерирует событие, которое обрабатывается реактивным pipeline. Тело ответа также остаётся в реактивном представлении (Flux<DataBuffer>), что позволяет streamingly передавать большие ответы без буферизации в памяти.

Конфигурация для балансировки нагрузки с помощью Spring Cloud LoadBalancer:

spring:
cloud:
gateway:
routes:
- id: user_service_lb
uri: lb://user-service
predicates:
- Path=/users/**

loadbalancer:
configurations: default

# Конфигурация для кэширования resolved адресов
discovery:
locator:
enabled: true
lower-case-service-id: true
При использовании lb:// схема автоматически активирует интеграцию с Service Discovery (Eureka, Consul) через ReactiveLoadBalancer. Выбор экземпляра выполняется с учётом состояния здоровья и выбранного алгоритма (round-robin по умолчанию).


Фаза 5: Пост-обработка ответа и завершение


После получения ответа от целевого сервиса выполняется оставшаяся часть цепочки фильтров — post-фильтры. Эти фильтры получают доступ как к оригинальному запросу, так и к ответу от целевого сервиса. Они могут модифицировать статус-код, заголовки, тело ответа.

Пример post-фильтра для добавления стандартных заголовков безопасности:
@Component
public class SecurityHeadersGlobalFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
if (!response.getHeaders().containsKey("X-Content-Type-Options")) {
response.getHeaders().add("X-Content-Type-Options", "nosniff");
}
if (!response.getHeaders().containsKey("X-Frame-Options")) {
response.getHeaders().add("X-Frame-Options", "DENY");
}
if (!response.getHeaders().containsKey("Content-Security-Policy")) {
response.getHeaders().add("Content-Security-Policy",
"default-src 'self'; frame-ancestors 'none'");
}
}));
}
}
Важно отметить, что этот фильтр реализует интерфейс GlobalFilter и будет применён ко всем маршрутам автоматически. Глобальные фильтры выполняются до фильтров, специфичных для маршрута, если только их порядок (через аннотацию @Order или реализацию Ordered) не указывает иное.


После выполнения всех post-фильтров финальный ответ записывается обратно в исходный канал Netty к клиенту. Netty берёт на себя эффективную отправку данных, включая chunked encoding для stream-ответов.


#Java #middle #Spring_Cloud_Gateway
Типы фильтров и их специализация

Pre-filters / Post-filters — это логическая группировка, определяемая моментом выполнения относительно вызова целевого сервиса. Технически большинство фильтров могут работать и как pre, и как post, в зависимости от их реализации. Паттерн разделения на pre/post часто реализуется через вызов chain.filter(exchange).then(...) для post-логики.

Сетевые фильтры (Netty layer) работают на более низком уровне, ближе к транспорту. NettyRoutingFilter и NettyWriteResponseFilter являются примерами таких фильтров. Они манипулируют непосредственно объектами Netty (HttpClientRequest, HttpClientResponse) и работают с реактивными потоками байтов (ByteBuf). Эти фильтры критически важны для производительности, так как обеспечивают эффективную передачу данных без лишних копирований.

Global filters применяются ко всем маршрутам автоматически. Они регистрируются как Spring Beans и могут быть упорядочены. Типичные use cases: централизованное логирование, сбор метрик, добавление стандартных заголовков, кэширование. Глобальные фильтры выполняются до фильтров маршрута, если только их порядок не указывает иное.

Per-route filters конфигурируются для конкретных маршрутов и применяются только к ним. Они декларативно задаются в конфигурации маршрута (YAML, properties) или через Java DSL.

Пример сложного per-route фильтра с кастомной конфигурацией:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("custom_rewrite", r -> r
.path("/legacy/api/**")
.filters(f -> f
.rewritePath("/legacy/api/(?<segment>.*)", "/modern/${segment}")
.addRequestParameter("source", "gateway")
.circuitBreaker(config -> config
.setName("myCircuitBreaker")
.setFallbackUri("forward:/fallback/default"))
)
.uri("lb://backend-service"))
.build();
}
В этой конфигурации DSL демонстрируется несколько фильтров в цепочке: rewritePath изменяет путь запроса с помощью регулярного выражения, addRequestParameter добавляет параметр, а circuitBreaker оборачивает вызов в контур устойчивости с fallback.



Управление памятью и производительностью в JVM

Архитектура SCG на Reactor Netty имеет глубокие последствия для управления памятью в JVM.

Вместо пулов потоков с фиксированным размером стека, основное потребление памяти связано с:
Буферами Netty (ByteBuf): Netty использует пул буферов через ByteBufAllocator. Это позволяет эффективно переиспользовать буферы для чтения/записи данных, минимизируя аллокации и сборку мусора. Буферы могут быть off-heap (direct buffers), что уменьшает нагрузку на GC, но требует явного управления памятью.

Реактивные потоки и лямбда-выражения: Каждый запрос создаёт цепочку реактивных операторов (Mono, Flux), которые представляются как объекты в heap. Лямбда-выражения в фильтрах также становятся объектами. Хотя эти объекты обычно недолгоживущие (short-lived) и эффективно обрабатываются молодым поколением сборщика мусора (young generation GC), при высокой нагрузке может создаваться значительное давление на GC.

Кэши маршрутов и предикатов: CachingRouteLocator и компилированные PathPattern кэшируются, что увеличивает постоянное потребление памяти (old generation), но значительно ускоряет обработку запросов.

Для оптимизации производительности важно:
Настроить размеры пулов буферов Netty согласно ожидаемому размеру запросов/ответов
Использовать профилирование для выявления memory leaks в кастомных фильтрах (невыполненные подписки на реактивные потоки)
Настроить сборщик мусора для низких пауз (G1GC или Shenandoah для больших heap-ов)
Мониторить количество аллокаций и pressure на young generation через JMX или Micrometer


#Java #middle #Spring_Cloud_Gateway