Форум программистов, компьютерный форум, киберфорум
Javaican
Войти
Регистрация
Восстановить пароль

GraphQL Federation в Spring Boot и создание API с помощью Apollo

Запись от Javaican размещена 09.05.2025 в 11:49
Показов 915 Комментарии 0

Нажмите на изображение для увеличения
Название: 147e6448-87c4-4805-ab14-2b11d9ed1ca1.jpg
Просмотров: 38
Размер:	223.4 Кб
ID:	10772
REST долгое время царствовал в экосистеме API, как дизайн-подход №1. Его относительная простота, понятный жизненый цикл ресурсов и стриктная иерархичность превратили REST в стандарт де-факто для разработки веб-сервисов любой сложности. Однако практика показала, что у этой методики есть пара-тройка тонких мест, особенно при работе с микросервисными архитектурами.

Первый удар по REST — проблема переизбытка данных (overfetching). Почему нужно загружать всю информацию о пользователе, когда нам нужно всего лишь его имя? В нагруженных системах эта проблема становится болезенной. Клиент получает тонны ненужных данных, тратя трафик и вычислительные ресурсы. Вторая прореха — недостаточная выборка (underfetching). Скажем, для отображения информации о заказе нам нужны еще и сведения о товаре, и о продавце. В мире REST это означает запустить целую цепочку дополнительных запросов, что сказывается на скорости работы.

GraphQL Federation в Spring Boot: эволюция API и Apollo в действии



GraphQL — язык запросов, позволяющий клиентам точно указывать, какие данные им нужны. Никакого переизбытка, никаких цепочек запросов. Однако в монолитном виде GraphQL быстро упирается в свой потолок.

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
# Типичный GraphQL-запрос 
{
  user(id: "123") {
    name
    orders {
      id
      products {
        title
        price
      }
    }
  }
}
В монолитных GraphQL-системах каждая команда разработчиков вынуждена вносить изменения в общую схему данных. С ростом приложения это становится узким горлышком. Но что, если разделить схему на части? Что, если каждая команда будет отвечать только за свой доменый участок, а затем эти фрагменты будут соединяться в единое целое? Именно такой подход реализует GraphQL Federation.

Федерация в мире GraphQL — это как виртуальная база данных над множеством источников. Она позволяет разбить монолитную систему на микросервисы, где каждый сервис управляет строго своей частью данных, но при этом обладая знанием о структуре всей системы. Прелесть федерации в том, что клиент по-прежнему видит только один эндпоинт и даже не догадывается, что под капотом работают десятки микросервисов. Gateway (шлюз) принимает запрос, разбивает его на подзапросы к нужным сервисам, собирает результаты воедино и отдает клиенту.

В мире Spring Boot имплементация федеративного GraphQL стала реальной благодаря Apollo Federation. По сути, каждый микросервис поднимает свой GraphQL-сервер с собственной схемой, а затем Apollo Gateway объединяет их в единое API. Преимущества такого подхода очевидны:
1. Автономность команд — каждая команда может разрабатывать и выпускать свой сервис независимо.
2. Лучшая производительность — запрашиваются только нужные данные.
3. Типобезопасность — вся схема строго типизирована.
4. Единая точка входа — клиенты работают с одним API.

Если сравнивать производительность REST и федеративного GraphQL, то нередко последний выигрывает в несколько раз. Представьте, что для отображения профиля пользователя с заказами и корзиной в REST-архитектуре потребуется 3-4 последовательных запроса, а в GraphQL — один. И это еще не учитывая, что REST вернет избыточные данные для каждого запроса! На практике переход на федеративную архитектуру может сократить объем передаваемых данных на 60-70% и уменьшить количество запросов на 80-90%.

JSON
1
2
3
4
5
6
# Как выглядит директива @key в Federation
type User @key(fields: "id") {
  id: ID!
  name: String!
  email: String!
}
На архитектурном уровне федеративный GraphQL дополняет концепцию Domain-Driven Design (DDD). Каждый субграф представляет собой ограниченый контекст (Bounded Context), а директивы @key и @external формируют явные связи между контекстами.

В экосистеме GraphQL наряду с Apollo Federation существуют и другие решения: Netflix DGS, Apollo Server, Spring GraphQL. Но именно федеративный подход Apollo стал фактическим стандартом из-за своей гибкости и масштабируемости.

Основательная проблема микросервисной архитектуры — согласованость данных и четкое разграничение ответствености. Федеративный GraphQL с его явными интерфейсами между сервисами и строгой типизацией идеально вписывается в эту парадигму, предлагая решение, которое одновременно гибкое и структурированное. Поэтому, если ваш проект страдает от "Swagger hell" (ада из множества REST-эндпоинтов), а клиентские приложения выполняют десятки запросов для отображения одного экрана — возможно, пришло время познакомиться с федеративным GraphQL на Spring Boot поближе.

Project 'org.springframework.boot:spring-boot-starter-parent:2.3.2.RELEASE' not found
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" ...

Что такое Spring, Spring Boot?
Здравствуйте. Никогда не использовал Spring, Spring Boot. Возник такой вопрос можно ли его...

Spring в Spring Boot context
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( ...

Spring Boot VS Tomcat+Spring - что выбрать?
Всем доброго дня! Я наверное еще из старой школы - пилю мелкие проект на Spring + Tomcat... ...


Основы GraphQL Federation



Федеративная архитектура GraphQL — фундаментально новый подход к построению распределённых API. Представьте себе оркестр, где каждый сервис играет свою партию, а дирижёр (Gateway) координирует их работу так, чтобы звучала единая симфония. В сердце федерации лежит принцип "разделяй и властвуй". Вместо громоздкой монолитной схемы данных мы получаем набор небольших, легко управляемых субграфов (subgraphs). Каждый субграф обслуживает конкретный домен бизнес-логики и может разрабатываться, тестироватся и деплоиться независимо от других.

Архитектура федерации строится вокруг двух ключевых компонентов: субграфы и шлюз (Gateway). Субграфы — это отдельные GraphQL-сервисы, каждый с собственной схемой. Gateway — центральный элемент, собирающий схемы всех субграфов в единую и маршрутизирующий запросы:

Java
1
2
3
4
5
6
7
8
9
10
11
                ┌─────────────────┐
                │   Apollo     │
                │   Gateway    │
                └───────┬───────┘
                        │ Routes queries
            ┌───────────┼──────────────┐
            │           │              │
┌──────────▼─────┐ ┌────▼───────────┐ ┌─────▼─────────┐
│ User Service  │ │ Order Service  │ │ Product      │
│ (Spring Boot) │ │ (Spring Boot)  │ │ Service      │
└───────────────┘ └────────────────┘ └───────────────┘
Когда клиент отправляет запрос, происходит интересная цепочка событий. Gateway разбирает запрос, определяет, какие субграфы должны обработать каждую часть, и затем отправляет соответствующие подзапросы. Как только субграфы возвращают результаты, Gateway соединяет их воедино и отправляет клиенту. Весь этот процесс для пользователя полностью прозрачен — он думает, что работает с единым API. Жизненный цикл запроса в федеративной системе выглядит примерно так:
1. Клиент отправляет запрос в Gateway.
2. Gateway анализирует запрос и создаёт план запроса (query plan).
3. На основе плана Gateway отправляет подзапросы в соответствующие сервисы.
4. Каждый сервис обрабатывает запрос и возвращает результат.
5. Gateway объединяет результаты и возвращает клиенту.
Скажем, клиент запрашивает данные о пользователе и его заказах. User Service отвечает за данные пользователя, а Order Service — за заказы. Gateway разбивает запрос на части, отправляет "пользовательскую" часть в User Service, "заказную" — в Order Service, а затем объединяет ответы. Но как сделать так, чтобы схемы разных сервисов сочетались между собой? Здесь на помощь приходят специальные директивы федерации:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# В User Service
type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
}
 
# В Order Service
type Order @key(fields: "id") {
    id: ID!
    products: [Product]
    buyer: User @external
}
 
# В Product Service
extend type Product @key(fields: "id") {
    id: ID!
    title: String!
    price: Float!
}
Директива @key указывает на поля, которые могут быть использованы для идентификации объекта между сервисами. @external говорит о том, что поле определено в другом сервисе. Директива extend позволяет расширять типы, определённые в других сервисах.

Одна из сложнейших задач в федерации — разделение схемы между сервисами. Это не просто технический вопрос, а скорее вопрос дизайна и организации. Стратегия разделения должна отражать бизнес-домены вашей организации. Вот несколько принципов разделения схемы:
1. Принцип единого владельца — каждый тип должен иметь один основной сервис-владелец.
2. Граници доменов — сервисы должны соответствовать ограниченным контекстам в DDD.
3. Минимизация зависимостей — стремитесь к минимизации связей между сервисами.
4. Эволюционное проектирование — начинайте с монолитной схемы и постепенно разделяйте её.

Самый увлекательный аспект федерации — механизмы разрешения ссылок между сервисами. Представим, что Order Service хочет получить информацию о покупателе, которая хранится в User Service. Это делается с помощью "entity references" — ссылок на сущности:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
# В Order Service
type Order {
    id: ID!
    buyerId: ID!
    buyer: User
}
 
# Резолвер для поля buyer
@SchemaMapping
public User buyer(Order order) {
    return userClient.getUser(order.getBuyerId());
}
Apollo Federation использует механизм под названием "query plan" для оптимизации этих межсервисных запросов. Вместо последовательных запросов Gateway группирует их вместе когда это возможно, сокращая задержку и уменьшая количество сетевых вызовов.

Кэширование в федеративных системах — многоуровневое. На уровне Gateway можно кэшировать планы запросов и результаты запросов. На уровне субграфов — результаты обработки подзапросов. Spring Boot предлагает интеграцию с популярными решениями для кэширования, такими как Redis или Caffeine.

Один из ключевых паттернов в федерации — DataLoader. Он позволяет группировать несколько запросов за идентичными данными в один пакетный запрос, что помогает избежать проблемы N+1 запросов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class UserDataLoader {
    private final BatchLoader<String, User> userBatchLoader = ids -> 
        CompletableFuture.supplyAsync(() -> userService.findAllByIds(ids));
    
    private final DataLoader<String, User> dataLoader = 
        DataLoader.newDataLoader(userBatchLoader);
    
    public CompletableFuture<User> load(String id) {
        return dataLoader.load(id);
    }
}
Эффективная федеративная система требует тщательного проектирования границ между сервисами. Когда границы определены неверно, возникает феномен, который Эванс в своей книге "Domain-Driven Design" называл "непонятной моделью" (anemic model) — модель становится просто контейнером данных без настоящей логики.

Практический опыт показывает, что наиболее устойчивые федеративные системы строятся вокруг бизнес-возможностей, а не технических слоёв. То есть вместо сервисов "база данных", "бизнес-логика" и "UI" лучше иметь сервисы "пользователи", "заказы" и "продукты". Федерация не панацея — она вносит дополнительную сложность в систему. Прежде чем внедрять федерацию, стоит задать себе вопрос: действительно ли масштаб вашей системы оправдывает эту сложность? Для небольших приложений монолитный GraphQL может быть более подходящим выбором.

Директива @requires – еще один мощный инструмент в арсенале федерации. Она указывает, какие поля нужны из внешнего типа для разрешения текущего поля. Представьте, что вам нужно рассчитать скидку на заказ, которая зависит от статуса VIP пользователя:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
# В Order Service
type Order {
  id: ID!
  totalWithDiscount: Float @requires(fields: "buyer { isVip }")
  buyer: User @external
}
 
# В User Service
type User @key(fields: "id") {
  id: ID!
  isVip: Boolean!
}
Резолвер для вычисления скидки сможет использовать информацию о VIP-статусе пользователя:

Java
1
2
3
4
5
@SchemaMapping
public Float totalWithDiscount(Order order, @SchemaMapping.Context User user) {
    float discount = user.isVip() ? 0.1f : 0.0f;
    return order.getTotal() * (1 - discount);
}
В реальных проектах особую роль играет согласованость данных между сервисами. Если Order Service хранит подмножество информации о пользователе, как синхронизировать эти данные с User Service? Существует несколько подходов:

1. Event-driven architecture — User Service публикует события изменения данных, Order Service подписывается и обновляет свои копии.
2. Федеративные запросы — Order Service запрашивает данные по требованию через Gateway.
3. Материализованные представления — использование специальных сервисов для поддержания согласованных представлений.

Согласно исследованию Ли Байрона, одного из создателей GraphQL, оптимальная стратегия — использовать федеративные запросы для чтения данных и событийную модель для синхронизации при записи.

В федеративных системах важно контролировать "расползание" схемы — ситуацию, когда схема разрастается без контроля. Один из подходов — использовать "контракты" между сервисами, явно определяющие, какие типы и поля экспортируются и импортируются:

JSON
1
2
3
4
5
6
# В User Service
type User @key(fields: "id") {
  id: ID!
  name: String! @external(reason: "Used by Order Service for display")
  email: String! @external(reason: "Used by Notification Service for alerts")
}
Комментарии с обоснованиями (@external с пояснением) помогают документировать зависимости и облегчить аудит схемы.
Версионирование в федеративных системах требует особого внимания. В отличие от REST, где можно просто добавить /v2 в URL, GraphQL предполагает эволюционный подход. Вот несколько стратегий:

1. Additive changes — добавляйте новые типы и поля, но не удаляйте существующие.
2. Deprecation — помечайте устаревшие поля как @deprecated перед удалением.
3. Feature flags — используйте флаги функциональности для постепенного внедрения изменений.

Особую сложность представляет обработка ошибок. Когда запрос затрагивает несколько сервисов, как клиент должен обрабатывать частичные сбои? Apollo Federation предлагает механизм "partial results", позволяющий возвращать данные даже при сбое в некоторых субграфах:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "data": {
    "user": {
      "id": "123",
      "name": "John Doe",
      "orders": null
    }
  },
  "errors": [
    {
      "message": "Order Service is temporarily unavailable",
      "path": ["user", "orders"]
    }
  ]
}
Такой подход обеспечивает отказоустойчивость системы — клиент может отобразить доступную информацию, даже если часть системы недоступна.

Защита данных в федерации заслуживает отдельного разговора. Поскольку разные сервисы могут иметь разные требования безопасности, возникает вопрос: как обеспечить согласованный контроль доступа? Популярное решение — использовать схему авторизации на основе графа:

JSON
1
2
3
4
5
6
# В User Service
type User @key(fields: "id") @auth(rules: [{role: ADMIN}]) {
  id: ID!
  name: String!
  email: String! @auth(rules: [{role: OWNER, selectors: ["id"]}])
}
Такие директивы указывают, что полный доступ к типу User имеют только администраторы, а доступ к email конкретного пользователя — только сам этот пользователь.
Отдельная история — инструментирование и мониторинг федеративных систем. Apollo Gateway предоставляет богатые возможности для сбора метрик:

Java
1
2
3
4
// Настройка трассировки запросов
GatewayConfig config = GatewayConfig.builder()
    .tracer(openTracingTracer)
    .build();
С правильно настроенной трассировкой вы можете видеть полный путь запроса через все сервисы, выявлять узкие места и аномалии.

На практике федеративные системы часто сталкиваются с проблемой разнородных источников данных. Некоторые сервисы могут использовать реляционные БД, другие — NoSQL, третьи — внешние REST API. Федерация абстрагирует эти различия, предоставляя единый GraphQL-интерфейс.

Java
1
2
3
4
5
6
7
// Резолвер, обращающийся к REST API
@SchemaMapping
public CompletableFuture<List<Product>> products(Category category) {
    return restTemplate
        .getForEntity("/products?category=" + category.getId(), ProductList.class)
        .thenApply(response -> response.getBody().getItems());
}
В сложных, высоконагруженных системах может возникнуть потребность в федерации на нескольких уровнях. Например, "супер-федерация", объединяющая несколько федеративных Gateway. Такие архитектуры встречаются в крупных организациях с множеством команд и доменов. Исследования в области производительности показывают, что федеративные системы могут обрабатывать запросы примерно на 20-30% медленнее, чем монолитные GraphQL-серверы из-за дополнительных сетевых вызовов. Однако эти же исследования демонстрируют, что при правильном кэшировании и батчинге разрыв сокращается до минимума.

Оптимизация межсервисной коммуникации становится критически важной по мере роста системы. В этом помогают различные техники:

1. Batching — группировка нескольких запросов в один.
2. Prefetching — упреждающая загрузка данных, которые, вероятно, понадобятся.
3. Local caching — кэширование результатов на уровне Gateway.
4. Hot paths optimization — особая оптимизация наиболее частых запросов.

Современные федеративные системы всё чаще внедряют возможности real-time с помощью подписок (subscriptions). Реализация подписок в федерации требует специального подхода — обычно это отдельный сервис, ответственный за управление WebSocket-соединениями и маршрутизацию событий.

Подводя итог: федерация в GraphQL — мощный подход к построению масштабируемых, распределённых API. Она позволяет совместить преимущества микросервисной архитектуры с удобством единого API. Но как любая сложная технология, она требует тщательного планирования и глубокого понимания взаимодействий между компонентами системы.

Интеграция с Spring Boot



Экосистема Spring предлагает поддержку GraphQL через официальный стартер spring-boot-starter-graphql, который автоматически настраивает всё необходимое для полноценной работы.
Для начала нам понадобится базовая настройка зависимостей в pom.xml:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
<dependency>
    <groupId>com.apollographql.federation</groupId>
    <artifactId>federation-graphql-java-support</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Заметьте, что мы добавляем специальную библиотеку поддержки федерации от Apollo. Именно она обеспечивает корректную работу директив @key, @external и других федеративных фишек.
Настройка графила в application.properties (или application.yml) тоже предельно проста:

Java
1
2
3
spring.graphql.path=/graphql
spring.graphql.graphiql.enabled=true
server.port=8081
Теперь давайте создадим сервис пользователей. Сначала определим схему в файле schema.graphqls:

JSON
1
2
3
4
5
6
7
8
9
10
type User @key(fields: "id") {
    id: ID!
    username: String!
    email: String!
}
 
type Query {
    user(id: ID!): User
    users: [User]
}
Обратите внимание на директиву @key — она указывает, что пользователей можно идентифицировать по полю id, и это поле будет использоваться для соединения данных между сервисами.
Теперь нужно создать Java-модель:

Java
1
2
3
4
5
6
7
8
9
@Node // Аннотация для поддержки федерации
public class User {
    @Id
    private String id;
    private String username;
    private String email;
    
    // Геттеры и сеттеры
}
Аннотация @Node соответствует директиве @key в GraphQL-схеме. Она указывает Spring, что этот класс должен обрабатываться по правилам федерации.
Самое интересное — резолверы. В Spring для них используются аннотации @QueryMapping и @SchemaMapping:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
public class UserController {
 
    private final UserRepository userRepository;
    
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @QueryMapping
    public User user(@Argument String id) {
        return userRepository.findById(id).orElse(null);
    }
    
    @QueryMapping
    public List<User> users() {
        return userRepository.findAll();
    }
    
    // Ключевой метод для федерации!
    @SchemaMapping(typeName = "__Federation")
    public User findUserById(Map<String, Object> representations) {
        String id = (String) representations.get("id");
        return userRepository.findById(id).orElse(null);
    }
}
Метод findUserById — особенный. Он реализует протокол федерации, позволяя другим сервисам "резолвить" пользователя по его ID.
Теперь повторим те же шаги для сервиса заказов. Сначала схема:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Order @key(fields: "id") {
    id: ID!
    productIds: [ID!]!
    buyer: User
}
 
extend type User @key(fields: "id") {
    id: ID! @external
    orders: [Order]
}
 
type Query {
    order(id: ID!): Order
    orders: [Order]
}
Здесь мы видим интересный момент — мы расширяем тип User, определённый в другом сервисе, и добавляем ему поле orders. Это и есть волшебство федерации — пользователь физически хранится в одном сервисе, но логически расширяется в другом! В Java-коде для заказов уже появляются дополнительные элементы:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Controller
public class OrderController {
 
    private final OrderRepository orderRepository;
    private final UserClient userClient; // REST-клиент для сервиса пользователей
    
    // Конструктор...
    
    @QueryMapping
    public Order order(@Argument String id) {
        return orderRepository.findById(id).orElse(null);
    }
    
    @QueryMapping
    public List<Order> orders() {
        return orderRepository.findAll();
    }
    
    // Резолвер для поля buyer
    @SchemaMapping
    public User buyer(Order order) {
        // Делаем запрос к сервису пользователей
        return userClient.getUser(order.getBuyerId());
    }
    
    // Резолвер для поля orders в User
    @SchemaMapping(typeName = "User")
    public List<Order> orders(User user) {
        return orderRepository.findByBuyerId(user.getId());
    }
    
    // Метод для федеративного протокола
    @SchemaMapping(typeName = "__Federation")
    public Order findOrderById(Map<String, Object> representations) {
        String id = (String) representations.get("id");
        return orderRepository.findById(id).orElse(null);
    }
}
Интеграция с существующими REST-сервисами через адаптеры — типичный сценарий при внедрении GraphQL в уже работающую систему. Нет нужды переписывать все сервисы сразу — можно обернуть существующие REST API в GraphQL-интерфейс:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class UserClient {
    
    private final RestTemplate restTemplate;
    
    public UserClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
    
    public User getUser(String id) {
        return restTemplate.getForObject("/api/users/{id}", User.class, id);
    }
}
Авторизация и аутентификация в федеративной архитектуре заслуживает отдельного разговора. Обычно используется схема, при которой Gateway проверяет JWT-токен и передаёт информацию о пользователе в запросах к сервисам:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class GraphQLConfig {
    
    @Bean
    public ServletRegistrationBean<HttpServlet> graphQLServlet(GraphQL graphQL) {
        ServletRegistrationBean<HttpServlet> servlet = new ServletRegistrationBean<>(
            new GraphQLHttpServlet() {
                @Override
                protected GraphQL getGraphQL() {
                    return graphQL;
                }
                
                @Override
                protected GraphQLContext createContext(
                    HttpServletRequest request, 
                    HttpServletResponse response
                ) {
                    // Извлекаем JWT токен из заголовка
                    String token = request.getHeader("Authorization");
                    
                    // Создаём контекст с информацией о пользователе
                    return new GraphQLContext(request, response, token);
                }
            },
            "/graphql"
        );
        return servlet;
    }
}
А в резолверах можно использовать этот контекст для проверки прав доступа:

Java
1
2
3
4
5
6
7
8
9
@QueryMapping
public User user(@Argument String id, @ContextValue String token) {
    // Проверяем права доступа на основе токена
    if (!authService.canAccessUser(token, id)) {
        throw new AccessDeniedException("Не авторизован для доступа к пользователю");
    }
    
    return userRepository.findById(id).orElse(null);
}
Более продвинутый подход — использование директив для декларативного описания правил авторизации прямо в схеме:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
directive @auth(
    requires: Role = USER
) on FIELD_DEFINITION | OBJECT
 
enum Role {
    ADMIN
    USER
    ANONYMOUS
}
 
type User @key(fields: "id") @auth(requires: ADMIN) {
    id: ID!
    username: String!
    email: String! @auth(requires: ADMIN)
    profile: Profile
}
Имплементация такой директивы выглядит примерно так:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class AuthDirectiveWiring implements SchemaDirectiveWiring {
    
    @Override
    public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> env) {
        GraphQLFieldDefinition field = env.getElement();
        GraphQLFieldsContainer parentType = env.getFieldsContainer();
        
        // Получаем ресурчер поля
        DataFetcher<?> originalFetcher = env.getCodeRegistry().getDataFetcher(parentType, field);
        
        // Создаём обёртку с проверкой авторизации
        DataFetcher<?> authFetcher = dataFetchingEnvironment -> {
            Map<String, Object> args = dataFetchingEnvironment.getArguments();
            GraphQLContext context = dataFetchingEnvironment.getContext();
            
            // Проверяем права на основе токена
            if (!authService.isAuthorized(context.getToken(), role)) {
                throw new AccessDeniedException("Не авторизован");
            }
            
            // Если авторизован, выполняем исходный резолвер
            return originalFetcher.get(dataFetchingEnvironment);
        };
        
        // Регистрируем новый резолвер
        env.getCodeRegistry().dataFetcher(parentType, field, authFetcher);
        
        return field;
    }
}
В Spring Boot также возможна интеграция с промышленными системами авторизации, такими как OAuth 2.0 и Spring Security. Типичный сценарий — использование Spring Security для защиты GraphQL-эндпоинта и проверки JWT-токенов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/graphql").permitAll() // Публичный доступ к graphql
                .anyRequest().authenticated()
            .and()
            .oauth2ResourceServer() // Настройка JWT валидации
                .jwt();
    }
}
Комбинируя эти подходы, вы получаете гибкую систему авторизации, которая может работать как на уровне Gateway, так и на уровне отдельных сервисов.

Apollo как федеративный шлюз



После настройки всех наших микросервисов настало время собрать их воедино. Именно здесь в игру вступает Apollo Gateway — ключевой элемент федеративной архитектуры, объединяющий разрозненные схемы в единую и маршрутизирующий запросы к нужным сервисам. Apollo Gateway — это Node.js-приложение, построенное на базе Apollo Server, специально заточеное под работу с федеративными GraphQL API. Настроить его на удивление просто. Вот минимальная конфигурация:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// gateway.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { ApolloGateway } = require('@apollo/gateway');
 
const gateway = new ApolloGateway({
  serviceList: [
    { name: 'users', url: 'http://localhost:8081/graphql' },
    { name: 'orders', url: 'http://localhost:8082/graphql' },
    { name: 'products', url: 'http://localhost:8083/graphql' }
  ]
});
 
const server = new ApolloServer({
  gateway,
  subscriptions: false,
});
 
startStandaloneServer(server, {
  listen: { port: 4000 }
}).then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
Всё что нужно — указать список сервисов, и Apollo сам соберёт их схемы, проверит их согласованность и создаст единую точку входа. Разумеется, в реальных проектах конфигурация может быть более сложной, с поддержкой аутентификации, кэширования и других аспектов. Для удобства управления конфигурацией можно выносить настройки в отдельный файл:

YAML
1
2
3
4
5
6
7
8
# gateway.yaml
services:
  user-service:
    url: [url]http://localhost:8081/graphql[/url]
  order-service:
    url: [url]http://localhost:8082/graphql[/url]
  product-service:
    url: [url]http://localhost:8083/graphql[/url]
А затем загружать его в приложение:

JavaScript
1
2
3
4
5
6
7
8
9
10
const fs = require('fs');
const yaml = require('js-yaml');
 
const config = yaml.load(fs.readFileSync('./gateway.yaml', 'utf8'));
const gateway = new ApolloGateway({
  serviceList: Object.entries(config.services).map(([name, service]) => ({
    name,
    url: service.url
  }))
});
Что происходит под капотом, когда Apollo Gateway запускается? Сервер выполняет несколько ключевых шагов:
1. Интроспекция сервисов — Gateway получает схемы всех сервисов через их GraphQL-эндпоинты.
2. Валидация схем — проверяются непротиворечивость типов и полей между сервисами.
3. Объединение схем — создаётся комбинированная схема с правилами маршрутизации.
4. Запуск сервера — Gateway начинает принимать запросы и направлять их сервисам.

Одна из самых мощных возможностей Apollo Gateway — автоматическая композиция запросов. Когда клиент отправляет сложный запрос, затрагивающий несколько сервисов, Gateway автоматически разбивает его на части, отправляет их в соответствующие сервисы и собирает результаты воедино.

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Этот запрос будет автоматически разбит между сервисами
query {
  user(id: "123") {
    id
    username
    orders {
      id
      products {
        title
        price
      }
    }
  }
}
В контейнерной среде Apollo Gateway обычно запускается в собственном контейнере, взаимодействующем с контейнерами сервисов через сеть. Вот пример Dockerfile для Gateway:

Java
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:14-alpine
 
WORKDIR /app
 
COPY package*.json ./
RUN npm install
 
COPY . .
 
EXPOSE 4000
 
CMD ["node", "gateway.js"]
А в Docker Compose настройка может выглядеть так:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
version: '3'
services:
  gateway:
    build: ./gateway
    ports:
      - "4000:4000"
    environment:
      - NODE_ENV=production
    depends_on:
      - user-service
      - order-service
      - product-service
 
  user-service:
    build: ./user-service
    ports:
      - "8081:8081"
    # ...
 
  order-service:
    build: ./order-service
    ports:
      - "8082:8082"
    # ...
 
  product-service:
    build: ./product-service
    ports:
      - "8083:8083"
    # ...
В Kubernetes настройка становится более сложной, но и более гибкой. Gateway может быть развёрнут как Deployment с соответствующим Service, который маршрутизирует трафик:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apollo-gateway
spec:
  replicas: 2
  selector:
    matchLabels:
      app: apollo-gateway
  template:
    metadata:
      labels:
        app: apollo-gateway
    spec:
      containers:
      - name: apollo-gateway
        image: my-registry/apollo-gateway:latest
        ports:
        - containerPort: 4000
        env:
        - name: USER_SERVICE_URL
          value: "http://user-service:8081/graphql"
        - name: ORDER_SERVICE_URL
          value: "http://order-service:8082/graphql"
        # ...
---
apiVersion: v1
kind: Service
metadata:
  name: apollo-gateway
spec:
  selector:
    app: apollo-gateway
  ports:
  - port: 80
    targetPort: 4000
  type: LoadBalancer
Интересная фишка Apollo Gateway — динамическое обнаружение сервисов. Вместо жёстко прописанных URL можно использовать сервис обнаружения, такой как Consul или Kubernetes DNS:

JavaScript
1
2
3
4
5
6
const gateway = new ApolloGateway({
  serviceList: [
    { name: 'users', url: process.env.USER_SERVICE_URL || 'http://user-service/graphql' },
    // ...
  ]
});
Балансировка нагрузки в Apollo Gateway реализуется на нескольких уровнях. На уровне запросов Gateway может оптимально распределять подзапросы между сервисами, минимизируя количество сетевых вызовов. На уровне инфраструктуры можно использовать несколько экземпляров Gateway за балансировщиком нагрузки:

YAML
1
2
3
4
5
6
7
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apollo-gateway
spec:
  replicas: 3  # Несколько экземпляров для балансировки
  # ...
Для мониторинга производительности Apollo предлагает интеграцию с Apollo Studio — мощной платформой для аналитики GraphQL API:

JavaScript
1
2
3
4
5
6
const gateway = new ApolloGateway({
  // ...
  // Apollo Studio integration
  graphVariant: 'production',
  apiKey: process.env.APOLLO_KEY
});
Apollo Studio позволяет отслеживать производительность запросов, выявлять проблемные места и оптимизировать схему на основе реальных данных использования.

Отказоустойчивость — одна из сильных сторон Apollo Gateway. Если один из сервисов недоступен, Gateway может продолжать обслуживать запросы, возвращая частичные результаты:

JavaScript
1
2
3
4
5
6
const gateway = new ApolloGateway({
  // ...
  // Enable partial results
  experimental_approximateQueryPlanSizeLimitBytes: 10000000,
  experimental_allowDeepPartialResults: true,
});
Это особенно полезно в микросервисной архитектуре, где сбои отдельных компонентов неизбежны, а система должна продолжать функционировать, даже если некоторые её части временно недоступны.
Для обеспечения устойчивости при развёртывании можно использовать техники, такие как rolling updates и blue-green deployments, позволяющие обновлять сервисы без простоев:

Bash
1
2
3
# Пример kubectl команды для rolling update
kubectl rollout update deployment/apollo-gateway \
  --image=my-registry/apollo-gateway:new-version
Федеративный шлюз Apollo — сердце всей системы, связующее звино между клиентами и сервисами. Правильная настройка и управление им имеют решающее значение для успеха всей федеративной архитектуры. В следующей главе мы углубимся в продвинутые техники оптимизации производительности GraphQL Federation в Spring Boot.

Продвинутые техники и оптимизация



Окей, теперь когда базовая архитектура нашей федерации построена, пришло время поговорить о том, как заставить её летать. Производительность – настоящее больное место федеративных систем. Да, разделение получилось красивым на бумаге, но без правильной оптимизации ваша распределённая схема может оказаться медленнее монолита.

Начнём с самой острой проблемы – N+1 запросов. Если вы не жили последние 10 лет в пещере, то знаете эту головную боль. В контексте федерации проблема становится еще острее, поскольку N+1 может размножаться по микросервисам как грибы после дождя.

Java
1
2
3
4
5
// Плохо - N+1 запросов к сервису пользователей
@SchemaMapping
public User buyer(Order order) {
    return userClient.getUser(order.getBuyerId()); // Отдельный запрос для каждого заказа
}
Решение – наш старый добрый друг DataLoader. Этот паттерн позволяет группировать мелкие запросы в пакеты, радикально сокращая количество сетевых вызовов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class UserDataLoader {
    private final UserClient userClient;
    
    public UserDataLoader(UserClient userClient) {
        this.userClient = userClient;
    }
    
    private final BatchLoader<String, User> batchLoader = ids -> 
        CompletableFuture.supplyAsync(() -> {
            // Один запрос вместо N запросов
            return userClient.getUsersByIds(new ArrayList<>(ids));
        });
    
    private final DataLoader<String, User> dataLoader = 
        DataLoader.newDataLoader(batchLoader);
    
    public CompletableFuture<User> load(String id) {
        return dataLoader.load(id);
    }
}
И теперь в резолвере:

Java
1
2
3
4
@SchemaMapping
public CompletableFuture<User> buyer(Order order) {
    return userDataLoader.load(order.getBuyerId());
}
Аналогичный подход можно применить и в других сервисах – везде, где есть потенциал для N+1 запросов.
Кэширование – ещё одно мощное оружие. В федеративных системах оно может быть многоуровневым:
1. Кэширование на уровне Gateway – хранение результатов частых запросов.
2. Распределённый кэш между сервисами – например, Redis для хранения общих данных.
3. Локальный кэш в каждом сервисе – Caffeine или EhCache для снижения нагрузки на базу.
Spring Boot предлагает отличную интеграцию с системами кэширования:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableCaching
public class CachingConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .maximumSize(1000));
        return cacheManager;
    }
}
И использование кэша в резолвере:

Java
1
2
3
4
@Cacheable(value = "users", key = "#id")
public User findUserById(String id) {
    return userRepository.findById(id).orElse(null);
}
Однако тут есть одна загвостка – валидность кэша в распределённой системе. Как узнать, что данные в сервисе заказов устарели после обновления пользователя? Здесь в игру вступают паттерны инвалидации кэша.
Один из популярных подходов – использование событийной модели:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class UserService {
    
    private final UserRepository repository;
    private final ApplicationEventPublisher eventPublisher;
    
    // Конструктор...
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        User updated = repository.save(user);
        // Публикуем событие об изменении
        eventPublisher.publishEvent(new UserUpdatedEvent(updated));
        return updated;
    }
}
А на другой стороне этого процесса можно настроить прослушивание таких событий:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class UserCacheEvictListener {
  
  private final CacheManager cacheManager;
  private final OrderClient orderClient;
  
  // Конструктор...
  
  @EventListener
  public void handleUserUpdated(UserUpdatedEvent event) {
      // Инвалидируем локальный кэш
      cacheManager.getCache("users").evict(event.getUser().getId());
      
      // Уведомляем другие сервисы о необходимости обновить данные
      orderClient.invalidateUserCache(event.getUser().getId());
  }
}
Мониторинг становится жизнено важным аспектом в федеративных системах. Без четкого понимания того, что происходит на каждом уровне, вы быстро погрязнете в проблеме "а кто виноват, что всё тормозит?". Apollo предоставляет встроенные инструменты для отслеживания производительности:

Java
1
2
3
4
@Bean
public GraphQLQueryInstrumentation apolloTracing() {
    return new ApolloTracingInstrumentation();
}
Эта инструментация добавляет к каждому ответу метаданные о времени выполнения каждого резолвера, что позволяет точно установить узкие места. Для полноценного мониторинга стоит интегрировать эти метрики с системой наблюдения, такой как Prometheus и Grafana:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MetricsConfig {
    @Bean
    public GraphQLQueryInstrumentation metricsInstrumentation(MeterRegistry registry) {
        return new GraphQLQueryInstrumentation() {
            @Override
            public InstrumentationContext<ExecutionResult> beginExecution(InstrumentationExecutionParameters parameters) {
                Timer.Sample sample = Timer.start(registry);
                return new SimpleInstrumentationContext<>() {
                    @Override
                    public void onCompleted(ExecutionResult result, Throwable t) {
                        sample.stop(registry.timer("graphql.query", 
                          "operation", parameters.getOperation()));
                    }
                };
            }
        };
    }
}
Распределённая трассировка – еще один мущный инструмент. В сложной федеративной системе запрос может проходить через 5-6 разных сервисов, и трассировка позваляет увидеть полную картину. Интеграция с Zipkin или Jaeger – стандартное решение:

Java
1
2
3
4
5
6
7
8
9
10
11
@Bean
public Filter distributedTraceFilter(Tracer tracer) {
    return (request, response, chain) -> {
        Span span = tracer.buildSpan("graphql-request").start();
        try (Scope scope = tracer.scopeManager().activate(span)) {
            chain.doFilter(request, response);
        } finally {
            span.finish();
        }
    };
}
Что касается стрестестирования, федеративные системы нуждаются в тщательном анализе под нагрузкой. JMeter или Gatling – идеальные инструменты для симуляции высокой нагрузки:

Java
1
2
3
4
5
6
7
8
9
10
// В Gatling-сценарии
http("GraphQL Query")
    .post("/graphql")
    .header("Content-Type", "application/json")
    .body(StringBody("""
        {
            "query": "{ user(id: \"123\") { name orders { id products { name price } } } }"
        }
    """))
    .check(status().is(200))
Симуляционное тестирование федеративной системы требует больше усилий, чем тестирование монолита, поскольку необходимо имитировать не только пользовательские запросы, но и взаимодействия между сервисами.
В высоконагруженных системах доказала свою эффективность стратегия "команд и запросов" (CQRS). Разделение путей чтения и записи позволяет оптимизировать каждый из них независимо:

Java
1
2
3
4
5
6
7
8
9
@QueryMapping // Оптимизировано для чтения
public User user(@Argument String id) {
    return userReadRepository.findById(id).orElse(null);
}
 
@MutationMapping // Оптимизировано для записи
public User updateUser(@Argument UserInput input) {
    return userWriteService.update(input);
}
При этом оптимальная конфигурация пулов потоков тоже имеет значение. По умолчанию Spring Boot использует настройки Tomcat, но их можно тонко подстроить:

Java
1
2
3
server.tomcat.max-threads=200
server.tomcat.min-spare-threads=20
server.connection-timeout=5000
Не стоит забывать и про настройку JVM. В федеративных системах, где возможны пики нагрузки, правильная конфигурация сборщика мусора решает многие проблемы:

Bash
1
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4
И наконец, для экстремальных сценариев производительности расмотрите использование реактивного подхода. Spring WebFlux и GraphQL могут работать вместе, обеспечивая неблокирующую обработку запросов:

Java
1
2
3
4
@QueryMapping
public Mono<User> user(@Argument String id) {
    return userRepository.findById(id);
}
Реактивный стэк особенно эффективен, когда сервисы обменивается большими объемами данных или имеют высокую латентность. Однако, переход на реактивное программирование требует пересмотра всей архитектуры и может быть сложным для команд, привыкших к императивному подходу.
Продвинутые техники оптимизации федеративного GraphQL – обширная тема, выходящая далеко за рамки одной статьи. Ключ к успеху – постоянное измерение, анализ и итеративная оптимизация, основанные на реальных потребностях вашей системы.

Spring Boot или Spring MVC?
Добрый день форумчане. Прошу совета у опытных коллег знающих и работающих с фреймворком Spring....

Wiki.js GraphQL создание страницы
Здравствуйте. Никто не занимался? В официальной документации на Wiki.js нет примеров (вот как...

Реализация REST/API (JSON) в Spring Boot
Я создал проект на спринг буте, используя Ваадин фреймворк создал чат, где несколько людей могут...

API Яндекс карт и spring boot
Хочу разобраться как использовать api яндекс карт и spring boot, но пока даже карту отобразить не...

ManyToMany в Spring Boot (Rest API)
Добрый вечер, такой вопрос. Есть таблицы tag и image, тег и картинка соответственно. У одной...

Как создать REST сервис на Spring boot с использованием java servlet api
Я уже создавал rest-сервис на spring boot без сервлетов. Но я не понимаю, как совместить этот...

Как в Java Spring Boot сформировать метод кторый будет работать как API на TomCat
Имеется тестовый проект который сформирован на базе этого примера...

Недоступны аннотации JPA в собранном с помощью Spring Boot проекте и возможность работать с БД
Здравствуйте. Использую Java 17. С помощью https://start.spring.io/, сделал конфигурацию проекта...

GraphQL validation error
..... const app = Relay.createContainer(App, { fragments: { viewer: () =&gt; Relay.QL` ...

MeteorJS и GraphQL
Привет, Хочу сказать что до недавнего времени MeteorJS и GraphQL являются какойто мутью в...

Graphql n+1 во вложенных запросах, даже с prefetch_related
Всем привет, на проекте использую graphql из библиотеки graphene-django для django. И заметил такую...

Куда нужно устанавливать GraphQL?
GraphQL пр использовании нужно настраивать только на одной стороне, либо на двух? Допустим, есть...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Использование Linq2Db в проектах C# .NET
UnmanagedCoder 21.05.2025
Среди множества претендентов на корону "идеального ORM" особое место занимает Linq2Db — микро-ORM, балансирующий между мощью полноценных инструментов и легковесностью ручного написания SQL. Что. . .
Реализация Domain-Driven Design с Java
Javaican 20.05.2025
DDD — это настоящий спасательный круг для проектов со сложной бизнес-логикой. Подход, предложенный Эриком Эвансом, позволяет создавать элегантные решения, которые точно отражают реальную предметную. . .
Возможности и нововведения C# 14
stackOverflow 20.05.2025
Выход версии C# 14, который ожидается вместе с . NET 10, приносит ряд интересных нововведений, действительно упрощающих жизнь разработчиков. Вы уже хотите опробовать эти новшества? Не проблема! Просто. . .
Собеседование по Node.js - вопросы и ответы
Reangularity 20.05.2025
Каждому разработчику рано или поздно приходится сталкиватся с техническими собеседованиями - этим стрессовым испытанием, где решается судьба карьерного роста и зарплатных ожиданий. В этой статье я. . .
Cython и C (СИ) расширения Python для максимальной производительности
py-thonny 20.05.2025
Python невероятно дружелюбен к начинающим и одновременно мощный для профи. Но стоит лишь заикнуться о высокопроизводительных вычислениях — и энтузиазм быстро улетучивается. Да, Питон медлительнее. . .
Безопасное программирование в Java и предотвращение уязвимостей (SQL-инъекции, XSS и др.)
Javaican 19.05.2025
Самые распространёные векторы атак на Java-приложения за последний год выглядят как классический "топ-3 хакерских фаворитов": SQL-инъекции (31%), межсайтовый скриптинг или XSS (28%) и CSRF-атаки. . .
Введение в Q# - язык квантовых вычислений от Microsoft
EggHead 19.05.2025
Microsoft вошла в гонку технологических гигантов с собственным языком программирования Q#, специально созданным для разработки квантовых алгоритмов. Но прежде чем погружаться в синтаксические дебри. . .
Безопасность Kubernetes с Falco и обнаружение вторжений
Mr. Docker 18.05.2025
Переход организаций к микросервисной архитектуре и контейнерным технологиям сопровождается лавинообразным ростом векторов атак — от тривиальных попыток взлома до многоступенчатых кибератак, способных. . .
Аугментация изображений с Python
AI_Generated 18.05.2025
Собрать достаточно большой датасет для обучения нейронной сети — та ещё головная боль. Часами вручную размечать картинки, скармливать их ненасытным алгоритмам и молиться, чтобы модель не сдулась при. . .
Исключения в Java: советы, примеры кода и многое другое
Javaican 18.05.2025
Исключения — это объекты, созданные когда программа сталкивается с непредвиденной ситуацией: файл не найден, сетевое соединение разорвано, деление на ноль. . . Список можно продолжать до бесконечности. . . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru