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
.
Например, мы можем объявить репозиторий как QuerydslPredicateExecutor
:
Затем использовать его для создания DataFetcher
.
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'ы, если они еще не зарегистрированы.
Его метод configure
показан ниже, он добавляет WiringFactory
; AutoRegistrationWiringFactory
.
AutoRegistrationWiringFactory
getDataFetcher
показан ниже.
По умолчанию имя типа GraphQL, возвращаемого запросом, должно совпадать с простым именем типа домена хранилища. DataFetcher'ы будут зарегистрированы, если у них уже нет регистраций.
Boot starter автоматически обнаруживает бины @GraphQlRepository и использует их для инициализации RuntimeWiringConfigurer
.
@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».