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; для получения изменений в реальном времени).
![](https://habrastorage.org/getpro/habr/upload_files/774/567/25c/77456725cb928b63cd917edd84460565.png)
Как показано выше, результаты возвращаются в форме, соответствующей запросу (вы получаете то, что просите).
Запросы GraphQL могут быть отправлены через HTTP POST или HTTP GET. Запросы, отправленные по HTTP POST, включают детали в виде JSON в теле запроса. Статус ответа HTTP всегда 200 OK, а любая замеченная ошибка при выполнении запроса GraphQL отображается в разделе "ошибки" ответа GraphQL.
![](https://habrastorage.org/getpro/habr/upload_files/763/54c/39a/76354c39a55ab0235e43f9ecd2a8cb19.png)
В GraphQL java выполнение запроса заключается в создании объекта GraphQl с соответствующими аргументами и последующем вызове его метода execute или executeAsync.
Объект GraphQl строится путем передачи схемы GraphQl, которая определяет каждое поле, которое можно запросить или изменить, и типы этих полей.
![](https://habrastorage.org/getpro/habr/upload_files/a6e/029/7a7/a6e0297a73052e7a158848157411c4bb.png)
Схемы могут быть определены программно или с помощью специального graphql dsl. Каждое определение поля имеет Datafetcher (см. раздел runtime wiring). Если таковой не настроен, используется PropertyDataFetcher. PropertyDataFetcher
проверяет Maps и POJO Java beans на наличие значений, соответствующих нужному имени.
Предыстория — GraphQL DataLoaders
Хотя GraphQL предлагает множество преимуществ по сравнению с REST API, он подвержен проблеме N+1. Это связано с природой фетчеров, которые не знают о том, что происходит за пределами их мира. GraphQL может исполнять отдельный фетчер данных для каждого встречающегося поля, что приводит к ненужным циклам для извлечения вложенных данных.
![](https://habrastorage.org/getpro/habr/upload_files/a76/9d8/c0b/a769d8c0b6af2607ec8f157902e56737.png)
К счастью, Facebook предложил решение для этой проблемы: Dataloaders.
DataLoaders может объединять исходящие запросы в один запрос и откладывать выборку данных. Вместо того, чтобы сначала разрешить дочерние поля, фетчер отвечает обещанием (Promise), что данные в конечном итоге будут возвращены, откладывает выполнение запроса и переходит к следующему фетчеру на том же уровне. Как только все дочерние поля будут просмотрены (по принципу Depth First Traversal), выполняется один запрос для получения данных для всех этих полей (размер и порядок должны быть сохранены в этом случае).
Spring GraphQl
В этом разделе мы подробно рассмотрим реализацию Spring GraphQl, которая построена на основе graphql-java.
![](https://habrastorage.org/getpro/habr/upload_files/0de/38f/28e/0de38f28e3baa27d4cc500684ee0106e.png)
Сервер GraphQl
Spring GraphQL поддерживает запросы GraphQL через HTTP и WebSockets. Мы сосредоточимся на обработке HTTP-запросов.
GraphQlHttpHandler
GraphQlHttpHandler
обрабатывает GraphQL через HTTP-запросы и делегирует веб-обработчику выполнение запросов. Есть два варианта, один для Spring MVC и один для Spring WebFlux. Мы перенаправим наше внимание на Spring WebFlux.
![](https://habrastorage.org/getpro/habr/upload_files/e8a/9da/338/e8a9da3382d4fa1cb92b816abb7d4e13.png)
GraphQlHttpHandler
может быть связан как HTTP путем объявления бина (bean) RouterFunction и использования RouterFunctions из Spring MVC или WebFlux для создания маршрутов (см. раздел Spring GraphQl Configuration).
Как показано ниже, когда запрос был получен, применяется следующая логика:
инстанцирование
WebGraphQlRequest
делегирование
GraphQlHanlder
для обработки запросавозврат ответа сервера
![](https://habrastorage.org/getpro/habr/upload_files/2ac/3fa/cad/2ac3facadb562092ed1749798ec2aebe.png)
DefaultWebGraphQlHandlerBuilder
DefaultWebGraphQlHandlerBuilder
— это реализация по умолчанию для создания экземпляров WebGraphQlHandler
.
![](https://habrastorage.org/getpro/habr/upload_files/82d/abd/09f/82dabd09f858d83a87f153ecba3788ec.png)
Метод build
показан ниже.
![](https://habrastorage.org/getpro/habr/upload_files/3da/df8/c1d/3dadf8c1d4a1f6063b22ececa6c94f0d.png)
Цепочка выполнения строится с использованием настроенных перехватчиков, запрос в итоге делегируется ExecutionGraphQlService
. Используя заданные аксессоры ThreadLocal
значения извлекаются и сохраняются в Reactor контексте. Контекст Reactor представляет собой неизменяемый Map или хранилище ключей/значений. Он привязывается к каждой последовательности и передается вверх через подписку. Эта функция уникальна для Reactor и не работает с другими спецификациями реактивных потоков.
DefaultExecutionGraphQlService
DefaultExecutionGraphQlService
— это ExecutionGraphQlService
, который использует GraphQlSource для получения экземпляра GraphQL и выполнения запроса.
![](https://habrastorage.org/getpro/habr/upload_files/2d1/71d/4ca/2d171d4ca04a31cf559bbfbce7b7e9c4.png)
Ниже показан его метод execute
.
![](https://habrastorage.org/getpro/habr/upload_files/0de/1e8/0be/0de1e80be7ee69c3d7db0fc217e736a6.png)
Он состоит из:
Поиска по контексту и сохранение контекста Reactor executionInput для последующего доступа через
DataFetchingEnvironment
.Регистрации Dataloaders для
executionInput
, в результате чего возвращаются обновленныеexecutionInput
для выполнения.
![](https://habrastorage.org/getpro/habr/upload_files/47f/38b/8e0/47f38b8e0656632142a4484876f545c2.png)
Асинхронного выполнения запроса и сопоставления его с ответом graphql.
![](https://habrastorage.org/getpro/habr/upload_files/702/934/195/702934195cd744be1caa7a8b4ca2e446.png)
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.
![](https://habrastorage.org/getpro/habr/upload_files/490/976/605/490976605971007f72b08ee4f8b35468.png)
Метод build показан ниже.
![](https://habrastorage.org/getpro/habr/upload_files/7b8/421/bf5/7b8421bf519beed59424c9d67ce0e663.png)
Применяется следующая логика:
Получение определения схемы — заключается в парсинге dsl-файла схемы.
RuntimeWiring
инициализируется,runTimeConfigurers
вносит вклад в конфигурациюruntimeWiring
.
![](https://habrastorage.org/getpro/habr/upload_files/8b0/7db/b12/8b07dbb1296607e35456dfd9622f979a.png)
Регистрируется преобразователь типов по умолчанию, возвращающийся к ClassNameTypeResolver.
![](https://habrastorage.org/getpro/habr/upload_files/410/431/ee3/410431ee3fa2fa5b7d90d2fbf005e76f.png)
Применяется визитор типов.
GraphQLCodeRegistry
содержит тот код выполнения, который связан с типами graphql, а именно DataFetchers, связанные с полями, TypeResolvers, связанные с абстрактными типами и GraphqlFieldVisibility.
![](https://habrastorage.org/getpro/habr/upload_files/84c/84c/211/84c84c211a0700d9d550379d3a651063.png)
Наконец, создается экземпляр GraphQL и оборачивается в
CachedGraphQlSource
.
![](https://habrastorage.org/getpro/habr/upload_files/c85/ad0/148/c85ad0148d04ad54e7618a42e9ab78bf.png)
GraphQl DataFetchers
Ключевым компонентом серверов GraphQL Java является DataFetcher. Когда GraphQL Java выполняет запрос, он вызывает настроенный DataFetcher для каждого поля в запросе. DataFetcher – это интерфейс с единственным методом, принимающим один параметр типа DataFetcherEnvironment
![](https://habrastorage.org/getpro/habr/upload_files/21e/e1c/e49/21ee1ce49c6b1a43bf775c1b118e8f2a.png)
Перенаправляем внимание на ReactiveSingleEntityFetcher
и ReactiveManyEntityFetcher
.
ReactiveSingleEntityFetcher
![](https://habrastorage.org/getpro/habr/upload_files/a83/d48/05d/a83d4805df4ac2960968462e616870d3.png)
Метод get ReactiveSingleEntityFetcher
показан выше; используя DataFetchingEnvironment
, он делегирует его QuerydslPredicateExecutor
(см. раздел GraphQL Querydsl). Кроме того, добавляются операторы, основанные на конфигурации запроса.
ReactiveManyEntityFetcher
ReactiveManyEntityFetcher
очень похож, единственное отличие заключается в том, что вместо Mono
возвращается Flux publisher
.
![](https://habrastorage.org/getpro/habr/upload_files/a0c/5a6/096/a0c5a6096b1c4cd0555c3f677e126fbd.png)
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 https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions](https://habrastorage.org/getpro/habr/upload_files/294/d70/255/294d7025536d083bc8a2e51e1aab4a15.png)
Например, мы можем объявить репозиторий как QuerydslPredicateExecutor
:
![https://docs.spring.io/spring-graphql/docs/1.0.0-M6/ https://docs.spring.io/spring-graphql/docs/1.0.0-M6/](https://habrastorage.org/getpro/habr/upload_files/e05/555/289/e05555289808bc316ff4032faa3505e6.png)
Затем использовать его для создания DataFetcher
.
![https://docs.spring.io/spring-graphql/docs/1.0.0-M6/ https://docs.spring.io/spring-graphql/docs/1.0.0-M6/](https://habrastorage.org/getpro/habr/upload_files/1bc/11b/1f7/1bc11b1f7152d1bf14589f8d37f1244e.png)
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
![](https://habrastorage.org/getpro/habr/upload_files/e43/45f/174/e4345f1742d2ad2496981030eca0e45a.png)
AnnotatedControllerConfigurer
— это RuntimeWiringConfigurer
, он будет влиять на конфигурацию RuntimeWiring
. Он обнаруживает аннотированные методы-обработчики SchemaMapping в классах Controller и регистрирует их как DataFetchers
. Аннотация @SchemaMapping сопоставляет метод обработчика с полем в схеме GraphQL и объявляет его DataFetcher
для этого поля.
![](https://habrastorage.org/getpro/habr/upload_files/dee/a06/33a/deea0633a5c6529045d6c8640e8337ba.png)
Как показано ниже, он сканирует бины в контексте приложения и выбирает те классы бинов, которые аннотированы аннотацией Contoller
. Затем выполняется проверка для поиска методов обработчика, аннотированных @SchemaMapping или @BatchMapping. @BatchMapping используется для пакетной загрузки. HandlerMethod
инкапсулирует информацию о методе обработчика, состоящем из методов getMethod()
и getBean()
. Он обеспечивает удобный доступ к параметрам метода, возвращаемому значению метода, аннотациям метода и т.д.
![](https://habrastorage.org/getpro/habr/upload_files/781/80e/fd8/78180efd88eeb01b5b881473ee3c869f.png)
В зависимости от наличия @BatchMapping или @SchemaMapping, регистрируется BatchMappingDataFetcher
или SchemaMappingDataFetcher
соответственно.
![](https://habrastorage.org/getpro/habr/upload_files/b33/644/f75/b33644f75e2ae5f7f3eed6db81902a3b.png)
![](https://habrastorage.org/getpro/habr/upload_files/95d/391/607/95d3916077c2768897a2506df219c092.png)
Обратите внимание, что при сканировании контекста приложения мы получаем имя бина (String), а не экземпляр, в этом случае мы откладываем установку объекта бина, соответствующего методу обработчика, на потом.
CreateHandlerMethod
показан ниже. Когда мы проходим через метод-кандидат, мы проверяем, является ли бин-обработчик String. Если это так, мы вызываем соответствующий конструктор.
![](https://habrastorage.org/getpro/habr/upload_files/1cf/b37/81b/1cfb3781bf505ecb9687161dfc37c03e.png)
createWithResolvedBean
вызовем позже, чтобы установить бин, обернутый внутри метода обработчика.
![](https://habrastorage.org/getpro/habr/upload_files/1b8/7be/7e1/1b87be7e1226ca21f541df5b0d0ab607.png)
Хорошим примером будет вызов метода SchemaMappingDataFetcher#get
, который создаст новый DataFetcherHandlerMethod
(InvocableHandlerMethodSupport
)
![](https://habrastorage.org/getpro/habr/upload_files/e8b/4d3/2f5/e8b4d32f5d5368e53df05996b0eb73c7.png)
Как показано ниже, это приведет к тому, что bean будет разрешен в контексте приложения. На самом деле, если мы выполняем запрос, это означает, что приложение Spring запущено и экземпляр bean находится там.
![](https://habrastorage.org/getpro/habr/upload_files/a70/e00/e1d/a70e00e1d81833cb32c04e6312d185b6.png)
![](https://habrastorage.org/getpro/habr/upload_files/671/ab3/ccd/671ab3ccd9bfa5e19ec0ceb2e4a888c7.png)
@GraphQlRepository — автоматическая регистрация
GraphQlRepository — это специализация стереотипа Repository, которая маркирует хранилище как предназначенное для использования в GraphQL-приложении для получения данных.
![](https://habrastorage.org/getpro/habr/upload_files/484/8a8/a10/4848a8a1099769393498c05378ef0f1b.png)
Если хранилище аннотировано при помощи @GraphQlRepository, оно будет автоматически зарегистрировано для запросов, не имеющих уже зарегистрированного DataFetcher.
![](https://habrastorage.org/getpro/habr/upload_files/203/a4d/a6c/203a4da6c0bd00b3876fba774051d798.png)
Учитывая карту имен типов GraphQL и фабрик DataFetcher, AutoRegistrationRuntimeWiringConfigurer
находит запросы с соответствующим возвращаемым типом и регистрирует для них DataFetcher'ы, если они еще не зарегистрированы.
![QuerydslDataFetcher.java QuerydslDataFetcher.java](https://habrastorage.org/getpro/habr/upload_files/6ce/fc2/a39/6cefc2a396115360284a28dd33db4727.png)
Его метод configure
показан ниже, он добавляет WiringFactory
; AutoRegistrationWiringFactory
.
![](https://habrastorage.org/getpro/habr/upload_files/bed/3e2/e62/bed3e2e62c7175ec6839f251404d90f7.png)
![](https://habrastorage.org/getpro/habr/upload_files/72f/bd1/38c/72fbd138c261552dfea85b173768cc43.png)
AutoRegistrationWiringFactory
getDataFetcher
показан ниже.
![](https://habrastorage.org/getpro/habr/upload_files/d99/11f/c19/d9911fc19b02e34d5303a85e0d36d29d.png)
По умолчанию имя типа GraphQL, возвращаемого запросом, должно совпадать с простым именем типа домена хранилища. DataFetcher'ы будут зарегистрированы, если у них уже нет регистраций.
![](https://habrastorage.org/getpro/habr/upload_files/9a6/998/42b/9a699842b79c89076f46c6b93bdabc4b.png)
Boot starter автоматически обнаруживает бины @GraphQlRepository и использует их для инициализации RuntimeWiringConfigurer
.
![GraphQlReactiveQuerydslAutoConfiguration.java GraphQlReactiveQuerydslAutoConfiguration.java](https://habrastorage.org/getpro/habr/upload_files/66f/12b/40a/66f12b40a41a44599daa3e0b9e8e5ebb.png)
@Argument — привязка аргумента
В GraphQL Java DataFetchingEnvironment
предоставляет доступ к специфическим для поля значениям аргументов. Аргументы доступны как простые скалярные значения, такие как String, или как Map of values для более сложного ввода, или как List of values. @Argument можно использовать для доступа к аргументу поля, который сопоставляется с методом обработчика.
![](https://habrastorage.org/getpro/habr/upload_files/211/0a2/f05/2110a2f05468b689a5f9f075fb287b6b.png)
ArgumentMapMethodArgumentResolver
делегирует GraphQlArgumentInitializer
для инстанцирования целевого типа и связывания данных из аргументов graphql.schema.DataFetchingEnvironment
. Затем метод вызывается вместе с разрешенными аргументами.
![](https://habrastorage.org/getpro/habr/upload_files/66b/c26/d88/66bc26d88f0d3714bbae859775050c8f.png)
Если тип аргумента — map и мы имеем дело с классами java bean, он будет делегирован DataBinder после правильного извлечения значений свойств, которые будут переданы для связывания.
![](https://habrastorage.org/getpro/habr/upload_files/139/c4a/af0/139c4aaf0492ded1da547f5f34961dad.png)
GraphQlArgumentBinder
GraphQlArgumentBinder
связывает аргумент GraphQL или полную карту аргументов с целевым объектом.
![](https://habrastorage.org/getpro/habr/upload_files/82f/36c/6b7/82f36c6b7c63d1877abe219585cd9bd5.png)
Его метод bind
показан ниже
![](https://habrastorage.org/getpro/habr/upload_files/9d0/e17/f21/9d0e17f211b705c947de17858f80dec4.png)
Применяется следующая логика:
Используя среду выборки данных, получаем необработанное значение. Затем необходимо определить класс, который используем для привязки.
Для преобразования исходных данных в целевой тип используем TypeConverter (по умолчанию SimpleTypeConverter).
Выполняем окончательную проверку привязки (проверяем, не возникла ли какая-либо ошибка).
Возвращаем целевой объект.
@ContextValue
Аннотация @ContextValue предоставляет удобный доступ к значениям в GraphQLContext, которые могут быть разрешены в качестве аргументов методов контроллера.
ContextValueMethodArgumentResolver
— это преобразователь для аннотированных параметров метода ContextValue
.
![](https://habrastorage.org/getpro/habr/upload_files/250/360/3fb/2503603fbcd25a94c27cddc2e50832fd.png)
@ProjectedPayload
В качестве альтернативы использованию полных объектов с @Argument также можно использовать интерфейс проекции для доступа к аргументам запроса GraphQL через четко определенный, минимальный интерфейс. Проекции аргументов предоставляются проекциями интерфейсов Spring Data, когда Spring Data находится на пути класса.
ProjectedPayloadMethodArgumentResolver
— это преобразователь для получения ProjectedPayload
@ProjectedPayload либо на основе полной карты DataFetchingEnvironment#getArguments()
, либо на основе определенного аргумента внутри карты, когда параметр метода аннотирован с @Argument.
Чтобы воспользоваться этим, создайте интерфейс, аннотированный @ProjectedPayload, и объявите его в качестве параметра метода контроллера. Если параметр аннотирован с @Argument, он применяется к отдельному аргументу в карте DataFetchingEnvironment.getArguments()
. Если параметр объявлен без @Argument, проекция действует на аргументы верхнего уровня в полной карте аргументов.
![](https://habrastorage.org/getpro/habr/upload_files/91e/4a9/725/91e4a972527bc587a8a27bc907f0c274.png)
Конфигурация Spring GraphQl Auto
В этом разделе мы рассмотрим конфигурацию бинов, добавляемую стартером для разработки приложения Spring WebFlux.
Класс GraphQlWebFluxAutoConfiguration
GraphQlWebFluxAutoConfiguration
— это класс конфигурации, позволяющий использовать Spring GraphQL через WebFlux.
![](https://habrastorage.org/getpro/habr/upload_files/4bf/0c6/b0f/4bf0c6b0fa89f1dd9f8a6a9520a91a0b.png)
Он объявляет GraphQL RouterFunction bean для заполнения в контексте приложения. В Spring WebFlux бины типа RouterFunction
собираются и получают делегированный трафик, если запрос соответствует.
Класс GraphQlAutoConfiguration
GraphQlAutoConfiguration
— это класс конфигурации для создания базовой инфраструктуры Spring GraphQL.
GraphQlSource конфигурируется ниже.
![](https://habrastorage.org/getpro/habr/upload_files/55c/9c7/f41/55c9c7f41c58b97bf834089e6b278c38.png)
Сначала он передаст ресурсы схемы и настроит конфигураторы wiring, найденные в контексте приложения, такие как AnnotatedControllerConfigurer
.
![](https://habrastorage.org/getpro/habr/upload_files/f6d/6cd/e5e/f6d6cde5e2e9249341f060040baafb90.png)
Кроме того, GraphQlAutoConfiguration
объявляет два условных бина типа ExecutionGraphQlService
и BatchLoaderRegistry
.
![](https://habrastorage.org/getpro/habr/upload_files/568/30b/c33/56830bc33d1c82983654a0337a99f393.png)
Заключение
В этой статье мы рассмотрели реализацию Spring GraphQl, сосредоточились на WebFlux для построения реактивных веб-приложений, однако проекты обеспечивают поддержку других протоколов связи, таких как RSocket.
Первый и последний релиз-кандидат Spring for GraphQL 1.0 уже доступен по ссылке.
Приглашаем всех желающих на открытое занятие «События в Spring Data JPA». На нем затронем такую важную тему, как работа с событиями, генерируемыми при взаимодействии с JPA-сущностями. Записаться на занятие можно на странице онлайн-курса «Разработчик на Spring Framework».