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

В этой статье мы рассмотрим Spring для GraphQL, преемника проекта GraphQL Java Spring от команды GraphQL Java. Проект находится на этапе подготовки к выпуску версии 1.0.0 (релиз запланирован на 17 мая). (Прим. переводчика. Апдейт по дате выпуска: 20 апреля 2023 года произошел релиз Spring for GraphQL версии 1.2.0-RC1).

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

Примеры проектов Spring boot можно найти здесь. Настоятельно рекомендую взглянуть на них, прежде чем продолжить.

Предыстория — выполнение запросов GraphQL

Операция GraphQL — это либо запрос (Query; чтение), либо мутация (Mutation; запись), либо подписка (Subscription; для получения изменений в реальном времени).

Как показано выше, результаты возвращаются в форме, соответствующей запросу (вы получаете то, что просите).

Запросы GraphQL могут быть отправлены через HTTP POST или HTTP GET. Запросы, отправленные по HTTP POST, включают детали в виде JSON в теле запроса. Статус ответа HTTP всегда 200 OK, а любая замеченная ошибка при выполнении запроса GraphQL отображается в разделе "ошибки" ответа GraphQL.

В GraphQL java выполнение запроса заключается в создании объекта GraphQl с соответствующими аргументами и последующем вызове его метода execute или executeAsync.

Объект GraphQl строится путем передачи схемы GraphQl, которая определяет каждое поле, которое можно запросить или изменить, и типы этих полей.

Схемы могут быть определены программно или с помощью специального graphql dsl. Каждое определение поля имеет Datafetcher (см. раздел runtime wiring). Если таковой не настроен, используется PropertyDataFetcher. PropertyDataFetcher проверяет Maps и POJO Java beans на наличие значений, соответствующих нужному имени.

Предыстория — GraphQL DataLoaders

Хотя GraphQL предлагает множество преимуществ по сравнению с REST API, он подвержен проблеме N+1. Это связано с природой фетчеров, которые не знают о том, что происходит за пределами их мира. GraphQL может исполнять отдельный фетчер данных для каждого встречающегося поля, что приводит к ненужным циклам для извлечения вложенных данных.

К счастью, Facebook предложил решение для этой проблемы: Dataloaders.

DataLoaders может объединять исходящие запросы в один запрос и откладывать выборку данных. Вместо того, чтобы сначала разрешить дочерние поля, фетчер отвечает обещанием (Promise), что данные в конечном итоге будут возвращены, откладывает выполнение запроса и переходит к следующему фетчеру на том же уровне. Как только все дочерние поля будут просмотрены (по принципу Depth First Traversal), выполняется один запрос для получения данных для всех этих полей (размер и порядок должны быть сохранены в этом случае).

Spring GraphQl

В этом разделе мы подробно рассмотрим реализацию Spring GraphQl, которая построена на основе graphql-java.

Сервер GraphQl

Spring GraphQL поддерживает запросы GraphQL через HTTP и WebSockets. Мы сосредоточимся на обработке HTTP-запросов.

GraphQlHttpHandler

GraphQlHttpHandler обрабатывает GraphQL через HTTP-запросы и делегирует веб-обработчику выполнение запросов. Есть два варианта, один для Spring MVC и один для Spring WebFlux. Мы перенаправим наше внимание на Spring WebFlux.

GraphQlHttpHandler может быть связан как HTTP путем объявления бина (bean) RouterFunction и использования RouterFunctions из Spring MVC или WebFlux для создания маршрутов (см. раздел Spring GraphQl Configuration).

Как показано ниже, когда запрос был получен, применяется следующая логика:

  • инстанцирование WebGraphQlRequest

  • делегирование GraphQlHanlder для обработки запроса

  • возврат ответа сервера

DefaultWebGraphQlHandlerBuilder

DefaultWebGraphQlHandlerBuilder — это реализация по умолчанию для создания экземпляров WebGraphQlHandler.

Метод build показан ниже.

Цепочка выполнения строится с использованием настроенных перехватчиков, запрос в итоге делегируется ExecutionGraphQlService. Используя заданные аксессоры ThreadLocal значения извлекаются и сохраняются в Reactor контексте. Контекст Reactor представляет собой неизменяемый Map или хранилище ключей/значений. Он привязывается к каждой последовательности и передается вверх через подписку. Эта функция уникальна для Reactor и не работает с другими спецификациями реактивных потоков.

DefaultExecutionGraphQlService

DefaultExecutionGraphQlService — это ExecutionGraphQlService, который использует GraphQlSource для получения экземпляра GraphQL и выполнения запроса.

Ниже показан его метод execute.

Он состоит из:

  • Поиска по контексту и сохранение контекста Reactor executionInput для последующего доступа через DataFetchingEnvironment.

  • Регистрации Dataloaders для executionInput, в результате чего возвращаются обновленные executionInput для выполнения.

  • Асинхронного выполнения запроса и сопоставления его с ответом graphql.

GraphQlSource

GraphQlSource — это основная абстракция Spring GraphQL для доступа к экземплярам graphql.GraphQL request execution. Он предоставляет builder API для инициализации GraphQL Java и создания GraphQlSource.

GraphQlSource поддерживает Reactive DataFetcher, Context Propagation и Exception Resolution через стандартный сборщик, доступный через GraphQlSource.builder().

DefaultGraphQlSourceBuilder

Реализация GraphQlSource.Builder по умолчанию, которая инициализирует экземпляр GraphQL и оборачивает его возвращаемым GraphQlSource.

Метод build показан ниже.

Применяется следующая логика:

  • Получение определения схемы — заключается в парсинге dsl-файла схемы.

  • RuntimeWiring инициализируется, runTimeConfigurers вносит вклад в конфигурацию runtimeWiring.

  • Регистрируется преобразователь типов по умолчанию, возвращающийся к ClassNameTypeResolver.

  • Применяется визитор типов. GraphQLCodeRegistry содержит тот код выполнения, который связан с типами graphql, а именно DataFetchers, связанные с полями, TypeResolvers, связанные с абстрактными типами и GraphqlFieldVisibility.

  • Наконец, создается экземпляр GraphQL и оборачивается в CachedGraphQlSource.

GraphQl DataFetchers

Ключевым компонентом серверов GraphQL Java является DataFetcher. Когда GraphQL Java выполняет запрос, он вызывает настроенный DataFetcher для каждого поля в запросе. DataFetcher – это интерфейс с единственным методом, принимающим один параметр типа DataFetcherEnvironment

Перенаправляем внимание на ReactiveSingleEntityFetcher и ReactiveManyEntityFetcher.

ReactiveSingleEntityFetcher

Метод get ReactiveSingleEntityFetcher показан выше; используя DataFetchingEnvironment, он делегирует его QuerydslPredicateExecutor (см. раздел GraphQL Querydsl). Кроме того, добавляются операторы, основанные на конфигурации запроса.

ReactiveManyEntityFetcher

ReactiveManyEntityFetcher очень похож, единственное отличие заключается в том, что вместо Mono возвращается Flux publisher.

GraphQL Querydsl

Spring for GraphQL поддерживает использование Querydsl для вызова данных через расширение Spring Data Querydsl. Querydsl — это фреймворк, который позволяет строить статически типизированные SQL-подобные запросы с помощью своего API.

Несколько модулей Spring Data предлагают интеграцию с Querydsl через QuerydslPredicateExecutor.

https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions

Например, мы можем объявить репозиторий как QuerydslPredicateExecutor:

https://docs.spring.io/spring-graphql/docs/1.0.0-M6/

Затем использовать его для создания DataFetcher.

https://docs.spring.io/spring-graphql/docs/1.0.0-M6/

DataFetcher строит Querydsl Predicate из параметров запроса GraphQL и использует его для получения данных. Spring Data поддерживает QuerydslPredicateExecutor для JPA, MongoDB и LDAP.

Если репозиторий является ReactiveQuerydslPredicateExecutor, builder возвращает DataFetcher<Mono<Account>> или DataFetcher<Flux<Account>>.

GraphQL RuntimeWiring

Wiring здесь в основном означает подключение поля к сборщику данных. RuntimeWiringConfigurer можно использовать для регистрации:

  • Пользовательских скалярных типов.

  • Кода обработки директив.

  • TypeResolver, если нужно переопределить Default TypeResolver для типа.

  • DataFetcher для поля, хотя большинство приложений будут просто конфигурировать AnnotatedControllerConfigurer, который обнаруживает аннотированные методы-обработчики DataFetcher. Spring Boot стартер добавляет AnnotatedControllerConfigurer по умолчанию.

AnnotatedControllerConfigurer

AnnotatedControllerConfigurer — это RuntimeWiringConfigurer, он будет влиять на конфигурацию RuntimeWiring. Он обнаруживает аннотированные методы-обработчики SchemaMapping в классах Controller и регистрирует их как DataFetchers. Аннотация @SchemaMapping сопоставляет метод обработчика с полем в схеме GraphQL и объявляет его DataFetcher для этого поля.

Как показано ниже, он сканирует бины в контексте приложения и выбирает те классы бинов, которые аннотированы аннотацией Contoller. Затем выполняется проверка для поиска методов обработчика, аннотированных @SchemaMapping или @BatchMapping. @BatchMapping используется для пакетной загрузки. HandlerMethod инкапсулирует информацию о методе обработчика, состоящем из методов getMethod() и getBean(). Он обеспечивает удобный доступ к параметрам метода, возвращаемому значению метода, аннотациям метода и т.д.

В зависимости от наличия @BatchMapping или @SchemaMapping, регистрируется BatchMappingDataFetcher или SchemaMappingDataFetcher соответственно.

Обратите внимание, что при сканировании контекста приложения мы получаем имя бина (String), а не экземпляр, в этом случае мы откладываем установку объекта бина, соответствующего методу обработчика, на потом.

CreateHandlerMethod показан ниже. Когда мы проходим через метод-кандидат, мы проверяем, является ли бин-обработчик String. Если это так, мы вызываем соответствующий конструктор.

createWithResolvedBean вызовем позже, чтобы установить бин, обернутый внутри метода обработчика.

Хорошим примером будет вызов метода SchemaMappingDataFetcher#get, который создаст новый DataFetcherHandlerMethod (InvocableHandlerMethodSupport)

Как показано ниже, это приведет к тому, что bean будет разрешен в контексте приложения. На самом деле, если мы выполняем запрос, это означает, что приложение Spring запущено и экземпляр bean находится там.

@GraphQlRepository — автоматическая регистрация

GraphQlRepository — это специализация стереотипа Repository, которая маркирует хранилище как предназначенное для использования в GraphQL-приложении для получения данных.

Если хранилище аннотировано при помощи @GraphQlRepository, оно будет автоматически зарегистрировано для запросов, не имеющих уже зарегистрированного DataFetcher.

Учитывая карту имен типов GraphQL и фабрик DataFetcher, AutoRegistrationRuntimeWiringConfigurer находит запросы с соответствующим возвращаемым типом и регистрирует для них DataFetcher'ы, если они еще не зарегистрированы.

QuerydslDataFetcher.java
QuerydslDataFetcher.java

Его метод configure показан ниже, он добавляет WiringFactory; AutoRegistrationWiringFactory.

AutoRegistrationWiringFactory getDataFetcher показан ниже.

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

Boot starter автоматически обнаруживает бины @GraphQlRepository и использует их для инициализации RuntimeWiringConfigurer.

GraphQlReactiveQuerydslAutoConfiguration.java

@Argument — привязка аргумента

В GraphQL Java DataFetchingEnvironment предоставляет доступ к специфическим для поля значениям аргументов. Аргументы доступны как простые скалярные значения, такие как String, или как Map of values для более сложного ввода, или как List of values. @Argument можно использовать для доступа к аргументу поля, который сопоставляется с методом обработчика.

ArgumentMapMethodArgumentResolver делегирует GraphQlArgumentInitializer для инстанцирования целевого типа и связывания данных из аргументов graphql.schema.DataFetchingEnvironment. Затем метод вызывается вместе с разрешенными аргументами.

Если тип аргумента — map и мы имеем дело с классами java bean, он будет делегирован DataBinder после правильного извлечения значений свойств, которые будут переданы для связывания.

GraphQlArgumentBinder

GraphQlArgumentBinder связывает аргумент GraphQL или полную карту аргументов с целевым объектом.

Его метод bind показан ниже

Применяется следующая логика:

  • Используя среду выборки данных, получаем необработанное значение. Затем необходимо определить класс, который используем для привязки.

  • Для преобразования исходных данных в целевой тип используем TypeConverter (по умолчанию SimpleTypeConverter).

  • Выполняем окончательную проверку привязки (проверяем, не возникла ли какая-либо ошибка).

  • Возвращаем целевой объект.

@ContextValue

Аннотация @ContextValue предоставляет удобный доступ к значениям в GraphQLContext, которые могут быть разрешены в качестве аргументов методов контроллера.

ContextValueMethodArgumentResolver — это преобразователь для аннотированных параметров метода ContextValue.

@ProjectedPayload

В качестве альтернативы использованию полных объектов с @Argument также можно использовать интерфейс проекции для доступа к аргументам запроса GraphQL через четко определенный, минимальный интерфейс. Проекции аргументов предоставляются проекциями интерфейсов Spring Data, когда Spring Data находится на пути класса.

ProjectedPayloadMethodArgumentResolver — это преобразователь для получения ProjectedPayload @ProjectedPayload либо на основе полной карты DataFetchingEnvironment#getArguments(), либо на основе определенного аргумента внутри карты, когда параметр метода аннотирован с @Argument.

Чтобы воспользоваться этим, создайте интерфейс, аннотированный @ProjectedPayload, и объявите его в качестве параметра метода контроллера. Если параметр аннотирован с @Argument, он применяется к отдельному аргументу в карте DataFetchingEnvironment.getArguments(). Если параметр объявлен без @Argument, проекция действует на аргументы верхнего уровня в полной карте аргументов.

Конфигурация Spring GraphQl Auto

В этом разделе мы рассмотрим конфигурацию бинов, добавляемую стартером для разработки приложения Spring WebFlux.

Класс GraphQlWebFluxAutoConfiguration

GraphQlWebFluxAutoConfiguration — это класс конфигурации, позволяющий использовать Spring GraphQL через WebFlux.

Он объявляет GraphQL RouterFunction bean для заполнения в контексте приложения. В Spring WebFlux бины типа RouterFunction собираются и получают делегированный трафик, если запрос соответствует.

Класс GraphQlAutoConfiguration

GraphQlAutoConfiguration — это класс конфигурации для создания базовой инфраструктуры Spring GraphQL.

GraphQlSource конфигурируется ниже.

Сначала он передаст ресурсы схемы и настроит конфигураторы wiring, найденные в контексте приложения, такие как AnnotatedControllerConfigurer.

Кроме того, GraphQlAutoConfiguration объявляет два условных бина типа ExecutionGraphQlService и BatchLoaderRegistry.

Заключение

В этой статье мы рассмотрели реализацию Spring GraphQl, сосредоточились на WebFlux для построения реактивных веб-приложений, однако проекты обеспечивают поддержку других протоколов связи, таких как RSocket.

Первый и последний релиз-кандидат Spring for GraphQL 1.0 уже доступен по ссылке


Приглашаем всех желающих на открытое занятие «События в Spring Data JPA». На нем затронем такую важную тему, как работа с событиями, генерируемыми при взаимодействии с JPA-сущностями. Записаться на занятие можно на странице онлайн-курса «Разработчик на Spring Framework».

Комментарии (0)