1.概述

Spring for GraphQL 为基于GraphQL Java构建的 Spring 应用程序提供支持 。这是两个团队之间的联合协作。我们的共同理念是减少固执己见,更专注于全面和广泛​​的支持。

Spring for GraphQL 是 GraphQL Java 团队的 GraphQL Java Spring项目的继承者。它旨在成为所有 Spring、GraphQL 应用程序的基础。

该项目目前正处于迈向 1.0 版本的里程碑阶段,并正在寻找反馈。请使用我们的 问题跟踪器报告问题、讨论设计问题或请求功能。

要开始使用,请查看 start.spring.io 上的 Spring GraphQL starterSamples 部分

2. 要求

Spring for GraphQL 需要以下内容作为基线:

  • JDK8

  • Spring框架 5.3

  • GraphQL Java 18

  • QueryDSL 或 Query by Example 的 Spring Data 2021.1.0 或更高版本

3. 服务器传输

Spring for GraphQL 支持服务器通过 HTTP、WebSocket 和 RSocket 处理 GraphQL 请求。

3.1。HTTP

GraphQlHttpHandler通过 HTTP 请求处理 GraphQL,并委托给 拦截链以执行请求。有两种变体,一种用于 Spring MVC,一种用于 Spring WebFlux。两者都异步处理请求并具有等效的功能,但分别依赖阻塞和非阻塞 I/O 来编写 HTTP 响应。

请求必须使用 HTTP POST 和 GraphQL 请求详细信息作为 JSON 包含在请求正文中,如提议的 GraphQL over HTTP 规范中所定义。成功解码 JSON 正文后,HTTP 响应状态始终为 200(OK),GraphQL 请求执行的任何错误都会出现在 GraphQL 响应的“错误”部分。媒体类型的默认和首选选择是 "application/graphql+json",但"application/json"也受支持,如规范中所述。

GraphQlHttpHandlerRouterFunction 可以通过声明一个bean 并使用RouterFunctionsSpring MVC 或 WebFlux 创建路由来公开为 HTTP 端点。Boot starter 会执行此操作,有关详细信息,请参阅 Web Endpoints部分,或检查GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration 包含实际配置。

Spring for GraphQL 存储库包含一个 Spring MVC HTTP 示例应用程序。

3.2. 网络套接字

GraphQlWebSocketHandler基于 graphql-ws库中定义 的协议处理 GraphQL over WebSocket 请求。在 WebSocket 上使用 GraphQL 的主要原因是订阅允许发送 GraphQL 响应流,但它也可以用于具有单个响应的常规查询。处理程序将每个请求委托给拦截链以进一步执行请求。

基于 WebSocket 协议的 GraphQL

有两种这样的协议,一种在 subscriptions-transport-ws 库中,另一种在 graphql-ws库中。前者不主动,由后者接替。阅读这篇 文了解历史。

有两种变体GraphQlWebSocketHandler,一种用于 Spring MVC,一种用于 Spring WebFlux。两者都异步处理请求并具有等效的功能。WebFlux 处理程序还使用非阻塞 I/O 和背压来流式传输消息,这很有效,因为在 GraphQL Java 中,订阅响应是 Reactive Streams Publisher

graphql-ws项目列出了许多 供客户使用的配方。

GraphQlWebSocketHandlerSimpleUrlHandlerMapping通过声明一个bean 并使用它将处理程序映射到 URL 路径,可以将其公开为 WebSocket 端点 。Boot starter 具有启用此功能的选项,有关详细信息,请参阅 Web Endpoints部分,或者检查GraphQlWebMvcAutoConfigurationGraphQlWebFluxAutoConfiguration 它包含的实际配置。

Spring for GraphQL 存储库包含一个 WebFlux WebSocket 示例应用程序。

3.3. RSocket

GraphQlRSocketHandler通过 RSocket 请求处理 GraphQL。查询和突变是预期并作为 RSocketrequest-response交互处理,而订阅则作为request-stream.

GraphQlRSocketHandler可以使用@Controller映射到 GraphQL 请求路由的委托。例如:

@Controller
public class GraphQlRSocketController {

     private final GraphQlRSocketHandler handler;

     GraphQlRSocketController(GraphQlRSocketHandler handler) {
            this.handler = handler;
     }

     @MessageMapping("graphql")
     public Mono<Map<String, Object>> handle(Map<String, Object> payload) {
            return this.handler.handle(payload);
     }

     @MessageMapping("graphql")
     public Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
            return this.handler.handleSubscription(payload);
     }
}

3.4. 拦截

用于HTTPWebSocket的 Spring MVC 和 Spring WebFlux 传输处理程序都委托给同一个WebGraphQlInterceptor链,然后ExecutionGraphQlService调用 GraphQL Java 引擎。您可以使用它来拦截任何 Web 传输上的 GraphQL 请求。

AWebGraphQlInterceptor公开底层传输(HTTP 或 WebSocket 握手)请求的详细信息,并允许自定义graphql.ExecutionInput 为 GraphQL Java 准备的请求。例如,要提取 HTTP 标头并通过以下方式将其提供给数据获取器GraphQLContext

class HeaderInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        List<String> headerValue = request.getHeaders().get("myHeader");
        request.configureExecutionInput((executionInput, builder) ->
                builder.graphQLContext(Collections.singletonMap("myHeader", headerValue)).build());
        return chain.next(request);
    }
}

然后ADataFetcher可以访问这个值,例如从一个带 注释的控制器方法:

@Controller
class MyController {

    @QueryMapping
    Person person(@ContextValue String myHeader) {
            // ...
    }
}

拦截器还可以自定义 HTTP 响应标头,或检查和/或 graphql.ExecutionResult从 GraphQL Java 转换:

class MyInterceptor implements WebGraphQlInterceptor {

    @Override
    public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
        return chain.next(request)
                .map(response -> {
                    Object data = response.getData();
                    Object updatedData = ... ;
                    return response.transform(builder -> builder.data(updatedData));
                });
    }
}

WebGraphQlHandler有一个构建器来创建WebGraphInterceptor链。Boot starter 使用它,请参阅 Boot 的 Web Endpoints部分。

RSocket传输处理程序委托给一个类似的链GraphQlInterceptor ,您可以使用它来拦截 GraphQL over RSocket 请求。

4. 请求执行

ExecutionGraphQlService是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,例如服务器传输,委托 ExecutionGraphQlService处理请求。

主要实现DefaultExecutionGraphQlService配置了一个 GraphQlSource用于访问graphql.GraphQL要调用的实例的方法。

4.1。GraphQLSource

GraphQlSource是用于访问 graphql.GraphQL实例以用于请求执行的核心 Spring 抽象。它提供了一个构建器 API 来初始化 GraphQL Java 并构建一个GraphQlSource.

默认GraphQlSource构建器可通过 访问 GraphQlSource.schemaResourceBuilder(),支持 ReactiveDataFetcherContext PropagationException Resolution

Spring Boot启动器通过默认值初始化一个 GraphQlSource实例,GraphQlSource.Builder并且还启用了以下功能:

对于进一步的自定义,您可以声明自己的GraphQlSourceBuilderCustomizerbean;例如,用于配置您自己的ExecutionIdProvider

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

    @Bean
    public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
        return (builder) ->
                builder.configureGraphQl(graphQlBuilder ->
                        graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
    }
}

4.1.1。架构资源

GraphQlSource.Builder可以配置一个或多个Resource实例来解析和合并在一起。这意味着可以从几乎任何位置加载模式文件。

默认情况下,Spring Boot 启动器 从众所周知的类路径位置查找模式文件FileSystemResource,但您可以通过 将其更改为文件系统上的位置,通过字节内容ByteArrayResource,或实现 Resource从远程位置加载模式文件的自定义或贮存。

4.1.2. 模式创建

默认情况下,GraphQlSource.Builder使用 GraphQL JavaGraphQLSchemaGenerator创建graphql.schema.GraphQLSchema. 这适用于大多数应用程序,但如有必要,您可以通过构建器挂钩模式创建:

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
            // create GraphQLSchema
        })

这样做的主要原因是通过联合库创建模式。

GraphQlSource部分解释了如何使用 Spring Boot 进行配置。

4.1.3。RuntimeWiringConfigurer

您可以使用RuntimeWiringConfigurer以下方式注册:

  • 自定义标量类型。

  • 指令处理代码。

  • TypeResolver, 如果您需要覆盖类型的 默认值TypeResolver

  • DataFetcher对于一个字段,尽管大多数应用程序将简单地配置 AnnotatedControllerConfigurer,它检测带注释的DataFetcher处理程序方法。Spring Boot 启动器AnnotatedControllerConfigurer默认添加。

与 Web 框架不同,GraphQL 不使用 Jackson 注释来驱动 JSON 序列化/反序列化。自定义数据类型及其序列化必须描述为 Scalars

Spring Boot 启动器检测 bean 类型RuntimeWiringConfigurer并将它们注册到GraphQlSource.Builder. 这意味着在大多数情况下,您的配置中将包含以下内容:

@Configuration
public class GraphQlConfig {

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {

        GraphQLScalarType scalarType = ... ;
        SchemaDirectiveWiring directiveWiring = ... ;
        DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();

        return wiringBuilder -> wiringBuilder
                .scalar(scalarType)
                .directiveWiring(directiveWiring)
                .type("Query", builder -> builder.dataFetcher("book", dataFetcher));
    }
}

如果您需要添加一个WiringFactory,例如进行考虑模式定义的注册,请实现configure同时接受该 RuntimeWiring.Builder和一个输出的替代方法List<WiringFactory>。这允许您添加任意数量的工厂,然后按顺序调用这些工厂。

4.1.4。默认TypeResolver

GraphQlSource.Builder注册ClassNameTypeResolver为默认TypeResolver 用于 GraphQL 接口和联合,这些接口和联合尚未通过RuntimeWiringConfigurer. GraphQL Java中的 a 的目的TypeResolver是确定从DataFetcherGraphQL 接口或联合字段返回的值的 GraphQL 对象类型。

ClassNameTypeResolver尝试将值的简单类名与 GraphQL 对象类型匹配,如果不成功,它还会导航其超类型,包括基类和接口,寻找匹配项。ClassNameTypeResolver提供了一个选项来配置名称提取功能以及ClassGraphQL 对象类型名称映射,这应该有助于涵盖更多极端情况:

GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
    // Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);

GraphQlSource部分解释了如何使用 Spring Boot 进行配置。

4.1.5。操作缓存

GraphQL Java 必须在执行操作之前对其进行解析验证。这可能会显着影响性能。为了避免重新解析和验证的需要,应用程序可以配置PreparsedDocumentProvider缓存和重用 Document 实例。GraphQL Java 文档通过PreparsedDocumentProvider.

在 Spring GraphQL 中,您可以PreparsedDocumentProvider通过 GraphQlSource.Builder#configureGraphQl: 注册一个。

// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...

// Create provider
PreparsedDocumentProvider provider = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))

GraphQlSource部分解释了如何使用 Spring Boot 进行配置。

4.1.6。指令

GraphQL 语言支持“在 GraphQL 文档中描述备用运行时执行和类型验证行为”的指令。指令类似于 Java 中的注释,但在 GraphQL 文档中的类型、字段、片段和操作上声明。

GraphQL Java 提供SchemaDirectiveWiring合约来帮助应用程序检测和处理指令。有关更多详细信息,请参阅 GraphQL Java 文档中的模式指令

在 Spring GraphQL 中,您可以SchemaDirectiveWiring通过 RuntimeWiringConfigurer. Spring Boot 启动器会检测到此类 bean,因此您可能会遇到以下情况:

@Configuration
public class GraphQlConfig {

     @Bean
     public RuntimeWiringConfigurer runtimeWiringConfigurer() {
          return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
     }

}
有关指令支持的示例,请查看 Graphql Java 库的扩展验证。

4.2. 反应性DataFetcher

默认GraphQlSource构建器支持 aDataFetcher返回MonoFlux将其调整为将CompletableFutureFlux聚合并转换为 List 的地方,除非请求是 GraphQL 订阅请求,在这种情况下,返回值仍然是Publisher用于流式传输 GraphQL 响应的 Reactive Streams。

反应式DataFetcher可以依赖从传输层传播的 Reactor 上下文的访问,例如从 WebFlux 请求处理,请参阅 WebFlux 上下文

4.3. 上下文传播

Spring for GraphQL 支持从 Server Transports透明地传播上下文,通过 GraphQL Java,以及DataFetcher它调用的其他组件。这包括ThreadLocal来自 Spring MVC 请求处理线程的上下文和Context来自 WebFlux 处理管道的 Reactor。

4.3.1。WebMvc

GraphQL Java 调用的ADataFetcher和其他组件可能并不总是在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步 WebGraphQlInterceptorDataFetcher切换到不同的线程。

Spring for GraphQL 支持将ThreadLocal值从 Servlet 容器线程传播到线程 aDataFetcher以及由 GraphQL Java 调用以在其上执行的其他组件。为此,应用程序需要创建一个ThreadLocalAccessor来提取 ThreadLocal感兴趣的值:

public class RequestAttributesAccessor implements ThreadLocalAccessor {

    private static final String KEY = RequestAttributesAccessor.class.getName();

    @Override
    public void extractValues(Map<String, Object> container) {
        container.put(KEY, RequestContextHolder.getRequestAttributes());
    }

    @Override
    public void restoreValues(Map<String, Object> values) {
        if (values.containsKey(KEY)) {
            RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
        }
    }

    @Override
    public void resetValues(Map<String, Object> values) {
        RequestContextHolder.resetRequestAttributes();
    }

}

AThreadLocalAccessor可以在WebGraphHandler 构建器中注册。Boot starter 检测到这种类型的 bean 并自动为 Spring MVC 应用程序注册它们,请参阅 Web Endpoints部分。

4.3.2. WebFlux

ReactiveDataFetcher可以依赖对源自 WebFlux 请求处理链的 Reactor 上下文的访问。这包括由WebGraphQlInterceptor组件添加的 Reactor 上下文。

4.4. 异常解决

GraphQL Java 应用程序可以注册 aDataFetcherExceptionHandler来决定如何在 GraphQL 响应的“错误”部分中表示来自数据层的异常。

Spring for GraphQL 有一个内置的DataFetcherExceptionHandler配置供默认GraphQLSource构建器使用。它允许应用程序注册一个或多个DataFetcherExceptionResolver按顺序调用的 Spring 组件,直到将其解析Exception为(可能为空的)graphql.GraphQLError 对象列表。

DataFetcherExceptionResolver是一个异步合约。DataFetcherExceptionResolverAdapter对于大多数实现,扩展和覆盖同步解决异常的方法resolveToSingleError之一就足够了。resolveToMultipleErrors

AGraphQLError可以通过 分配给一个类别graphql.ErrorClassification。在 Spring GraphQL 中,您还可以分配 via ErrorTypewhich 具有以下常见分类,应用程序可以使用这些分类对错误进行分类:

  • BAD_REQUEST

  • UNAUTHORIZED

  • FORBIDDEN

  • NOT_FOUND

  • INTERNAL_ERROR

如果异常仍未解决,则默认情况下将其归类为INTERNAL_ERROR 带有通用消息的类,其中包括类别名称和executionIdfrom DataFetchingEnvironment。该消息是故意不透明的,以避免泄漏实现细节。应用程序可以使用 aDataFetcherExceptionResolver来自定义错误详细信息。

未解决的异常记录在 ERROR 级别以及与executionId发送到客户端的错误相关联。已解决的异常记录在 DEBUG 级别。

4.5. 批量加载

给定 aBook和 its Author,我们可以DataFetcher为一本书创建一个,为它的作者创建另一个。这允许选择有或没有作者的书籍,但这意味着书籍和作者不会一起加载,这在查询多本书时尤其低效,因为每本书的作者都是单独加载的。这被称为 N+1 选择问题。

4.5.1。DataLoader

GraphQL Java 提供了一种DataLoader批量加载相关实体的机制。您可以在 GraphQL Java 文档中找到完整的详细信息。以下是其工作原理的摘要:

  1. DataLoaderDataLoaderRegistry给定唯一键的情况下,可以加载实体的寄存器。

  2. DataFetcher's 可以访问DataLoader's 并使用它们通过 id 加载实体。

  3. ADataLoader通过返回一个 future 来推迟加载,因此它可以分批完成。

  4. DataLoader'维护加载实体的每个请求缓存,可以进一步提高效率。

4.5.2.BatchLoaderRegistry

GraphQL Java 中完整的批处理加载机制需要实现几个BatchLoader接口之一,然后将它们包装并注册为DataLoaders 并在DataLoaderRegistry.

Spring GraphQL 中的 API 略有不同。对于注册,只有一个中央BatchLoaderRegistry公开工厂方法和一个构建器来创建和注册任意数量的批量加载函数:

@Configuration
public class MyConfig {

    public MyConfig(BatchLoaderRegistry registry) {

        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
                // return Mono<Map<Long, Author>
        });

        // more registrations ...
    }

}

Spring Boot starter 声明了一个BatchLoaderRegistrybean,您可以将其注入到您的配置中,如上所示,或者注入到任何组件(如控制器)中,以便注册批量加载功能。反过来,它BatchLoaderRegistry被注入到 DefaultExecutionGraphQlService它确保DataLoader每个请求注册的地方。

默认情况下,DataLoader名称基于目标实体的类名。这允许@SchemaMapping方法声明 具有泛型类型的DataLoader 参数,而无需指定名称。但是,如有必要,可以通过 BatchLoaderRegistry构建器自定义名称以及其他DataLoader选项。

很多情况下,在加载相关实体的时候,可以使用 @BatchMapping控制器方法,这是一个快捷方式,替代了需要直接使用BatchLoaderRegistryDataLoader。s BatchLoaderRegistry也提供了其他重要的好处。GraphQLContext它支持从批量加载函数和方法访问相同的内容@BatchMapping,并确保对它们进行上下文传播。这就是为什么应用程序应该使用它的原因。可以直接执行您自己的DataLoader注册,但这样的注册将放弃上述好处。

4.5.3. 测试批量加载

首先BatchLoaderRegistry在 a 上执行注册DataLoaderRegistry

BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...

DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);

DataLoader现在您可以按如下方式访问和测试个人:

DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading

assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...

5. 数据整合

Spring for GraphQL 允许您利用现有的 Spring 技术,遵循常见的编程模型,通过 GraphQL 公开底层数据源。

本节讨论 Spring Data 的集成层,它提供了一种将 Querydsl 或 Query by Example 存储库调整DataFetcher@GraphQlRepository.

5.1。查询dsl

Spring for GraphQL 支持使用Querydsl通过 Spring Data Querydsl 扩展来获取数据 。Querydsl 提供了一种灵活但类型安全的方法,通过使用注释处理器生成元模型来表达查询谓词。

例如,将存储库声明为QuerydslPredicateExecutor

public interface AccountRepository extends Repository<Account, Long>,
            QuerydslPredicateExecutor<Account> {
}

然后用它来创建一个DataFetcher

// For single result queries
DataFetcher<Account> dataFetcher =
        QuerydslDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QuerydslDataFetcher.builder(repository).many();

您现在可以DataFetcher通过 RuntimeWiringConfigurer.

从GraphQL请求参数DataFetcher构建一个 Querydsl Predicate,并使用它来获取数据。Spring Data 支持QuerydslPredicateExecutorJPA、MongoDB 和 LDAP。

如果存储库是ReactiveQuerydslPredicateExecutor,则构建器返回 DataFetcher<Mono<Account>>DataFetcher<Flux<Account>>。Spring Data 支持 MongoDB 的这种变体。

5.1.1。构建设置

要在您的构建中配置 Querydsl,请遵循 官方参考文档

例如:

Gradle
Maven
dependencies {
    //...

    annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
            'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
            'javax.annotation:javax.annotation-api:1.3.2'
}

compileJava {
     options.annotationProcessorPath = configurations.annotationProcessor
}

webmvc -http示例将 Querydsl 用于 artifactRepositories.

5.1.2. 定制

QuerydslDataFetcher支持自定义 GraphQL 参数如何绑定到属性以创建 Querydsl Predicate。默认情况下,每个可用属性的参数都绑定为“等于”。要自定义它,您可以使用QuerydslDataFetcher构建器方法来提供QuerydslBinderCustomizer.

存储库本身可能是QuerydslBinderCustomizer. 这是在Auto-Registration期间自动检测并透明应用的。但是,在手动构建时,QuerydslDataFetcher您将需要使用构建器方法来应用它。

QuerydslDataFetcher支持接口和 DTO 投影来转换查询结果,然后返回这些结果以进行进一步的 GraphQL 处理。

要了解预测是什么,请参阅 Spring Data 文档。要了解如何在 GraphQL 中使用投影,请参阅选择集与投影

要将 Spring Data 投影与 Querydsl 存储库一起使用,请创建投影接口或目标 DTO 类,并通过该projectAs方法对其进行配置以获得 DataFetcher生成目标类型:

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

// For single result queries
DataFetcher<AccountProjection> dataFetcher =
        QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single();

// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
        QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many();

5.1.3. 自动注册

如果存储库使用 注释@GraphQlRepository,则它会自动注册用于尚未注册DataFetcher且返回类型与存储库域类型匹配的查询。这包括单值和多值查询。

默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,您可以使用 的typeName属性 @GraphQlRepository来指定目标 GraphQL 类型名称。

自动注册检测给定存储库是否通过构建器方法实现QuerydslBinderCustomizer并透明地应用它。QuerydslDataFetcher

自动注册是通过RuntimeWiringConfigurer可以从QuerydslDataFetcher. Boot starter会 自动检测@GraphQlRepositorybean 并使用它们来初始化 RuntimeWiringConfigurerwith。

自动注册不支持自定义QueryByExampleDataFetcher如果需要,您需要DataFetcher使用 RuntimeWiringConfigurer.

5.2. 示例查询

Spring Data 支持使用 Query by Example 来获取数据。Query by Example (QBE) 是一种简单的查询技术,不需要您通过特定于商店的查询语言编写查询。

首先声明一个存储库QueryByExampleExecutor

public interface AccountRepository extends Repository<Account, Long>,
            QueryByExampleExecutor<Account> {
}

用于QueryByExampleDataFetcher将存储库变成DataFecher

// For single result queries
DataFetcher<Account> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).single();

// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).many();

您现在可以DataFetcher通过 RuntimeWiringConfigurer.

使用DataFetcherGraphQL 参数映射来创建存储库的域类型,并将其用作示例对象来获取数据。Spring Data 支持 QueryByExampleDataFetcherJPA、MongoDB、Neo4j 和 Redis。

如果存储库是ReactiveQueryByExampleExecutor,则构建器返回 DataFetcher<Mono<Account>>DataFetcher<Flux<Account>>。Spring Data 支持 MongoDB、Neo4j、Redis 和 R2dbc 的这种变体。

5.2.1。构建设置

Query by Example 已包含在 Spring Data 模块中,用于支持它的数据存储,因此无需额外设置即可启用它。

5.2.2. 定制

QueryByExampleDataFetcher支持接口和 DTO 投影来转换查询结果,然后返回这些结果以进行进一步的 GraphQL 处理。

要了解投影是什么,请参阅 Spring Data 文档。要了解投影在 GraphQL 中的作用,请参阅选择集与投影

要将 Spring Data 投影与 Query by Example 存储库一起使用,请创建投影接口或目标 DTO 类,并通过该projectAs方法对其进行配置以获得 DataFetcher生成目标类型:

class Account {

    String name, identifier, description;

    Person owner;
}

interface AccountProjection {

    String getName();

    String getIdentifier();
}

// For single result queries
DataFetcher<AccountProjection> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single();

// For multi-result queries
DataFetcher<Iterable<AccountProjection>> dataFetcher =
        QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many();

5.2.3。自动注册

如果存储库使用 注释@GraphQlRepository,则它会自动注册用于尚未注册DataFetcher且返回类型与存储库域类型匹配的查询。这包括单值和多值查询。

默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,您可以使用 的typeName属性 @GraphQlRepository来指定目标 GraphQL 类型名称。

自动注册是通过RuntimeWiringConfigurer可以从QueryByExampleDataFetcher. Boot starter会 自动检测@GraphQlRepositorybean 并使用它们来初始化 RuntimeWiringConfigurerwith。

自动注册不支持自定义QueryByExampleDataFetcher如果需要,您需要DataFetcher使用 RuntimeWiringConfigurer.

5.3. 选择集与投影

出现的一个常见问题是,GraphQL 选择集与 Spring Data 投影相比如何 ,它们各自扮演什么角色?

简短的回答是 Spring for GraphQL 不是将 GraphQL 查询直接转换为 SQL 或 JSON 查询的数据网关。相反,它允许您利用现有的 Spring 技术,并且不假设 GraphQL 模式和底层数据模型之间存在一对一的映射。这就是为什么数据模型的客户端驱动选择和服务器端转换可以发挥互补作用的原因。

为了更好地理解,请考虑 Spring Data 将域驱动(DDD)设计作为管理数据层复杂性的推荐方法。在 DDD 中,遵守聚合的约束很重要。根据定义,聚合仅在全部加载时才有效,因为部分加载的聚合可能会对聚合功能施加限制。

在 Spring Data 中,您可以选择是否希望您的聚合按原样公开,或者是否在将其作为 GraphQL 结果返回之前对数据模型应用转换。有时这样做就足够了,默认情况下, QuerydslQuery by Example集成将 GraphQL 选择集转换为基础 Spring Data 模块用来限制选择的属性路径提示。

在其他情况下,减少甚至转换底层数据模型以适应 GraphQL 模式很有用。Spring Data 通过 Interface 和 DTO Projections 支持这一点。

null接口投影定义了一组固定的属性,以根据数据存储查询结果公开属性可能存在或不存在的位置。有两种接口投影,它们都决定从底层数据源加载哪些属性:

DTO 投影提供了更高级别的自定义,因为您可以将转换代码放置在构造函数或 getter 方法中。

DTO 投影从查询中具体化,其中各个属性由投影本身确定。DTO 投影通常与全参数构造函数(例如 Java 记录)一起使用,因此只有在所有必需字段(或列)都是数据库查询结果的一部分时才能构造它们。

6.带注释的控制器

Spring for GraphQL 提供了一个基于注解的编程模型,其中@Controller 组件使用注解来声明具有灵活方法签名的处理程序方法,以获取特定 GraphQL 字段的数据。例如:

@Controller
public class GreetingController {

        @QueryMapping (1)
        public String hello() { (2)
            return "Hello, world!";
        }

}
1 将此方法绑定到查询,即查询类型下的字段。
2 如果未在注解上声明,则根据方法名称确定查询。

Spring for GraphQL 使用RuntimeWiring.Builder将上述处理程序方法注册为 graphql.schema.DataFetcher名为“hello”的查询。

6.1。宣言

您可以将@Controllerbean 定义为标准 Spring bean 定义。构造 @Controller型允许自动检测,与 Spring 对检测类路径上的类和为它们自动注册 bean 定义的一般支持保持@Controller一致@Component。它还充当带注释类的原型,表明它在 GraphQL 应用程序中作为数据获取组件的角色。

AnnotatedControllerConfigurer检测@Controllerbean 并将其带注释的处理程序方法注册为DataFetchers via RuntimeWiring.Builder。它是一个RuntimeWiringConfigurer可以添加到GraphQlSource.Builder. Spring Boot starter 自动声明AnnotatedControllerConfigurer为 bean 并将所有RuntimeWiringConfigurerbean 添加到GraphQlSource.Builder并启用对带注释DataFetcher的 s 的支持,请参阅 Boot starter 文档中的 GraphQL RuntimeWiring部分。

6.2.@SchemaMapping

注释将@SchemaMapping处理程序方法映射到 GraphQL 模式中的字段,并将其声明DataFetcher为该字段的。注解可以指定父类型名称,以及字段名称:

@Controller
public class BookController {

    @SchemaMapping(typeName="Book", field="author")
    public Author getAuthor(Book book) {
        // ...
    }
}

注释也可以省略这些@SchemaMapping属性,在这种情况下,字段名称默认为方法名称,而类型名称默认为注入方法的源/父对象的简单类名称。例如,下面默认键入“Book”和字段“author”:

@Controller
public class BookController {

    @SchemaMapping
    public Author author(Book book) {
        // ...
    }
}

@SchemaMapping可以在类级别声明注解,为类中的所有处理程序方法指定默认类型名称。

@Controller
@SchemaMapping(typeName="Book")
public class BookController {

    // @SchemaMapping methods for fields of the "Book" type

}

@QueryMapping, @MutationMapping, 和@SubscriptionMapping是元注释,它们本身被注释@SchemaMapping并且 typeName 分别预设为Query, Mutation, 或Subscription。实际上,这些是 Query、Mutation 和 Subscription 类型下字段的快捷注释。例如:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }

    @SubscriptionMapping
    public Flux<Book> newPublications() {
        // ...
    }
}

@SchemaMapping处理程序方法具有灵活的签名,可以从一系列方法参数和返回值中进行选择。

6.2.1. 方法签名

模式映射处理程序方法可以具有以下任何方法参数:

方法论据 描述

@Argument

用于访问绑定到更高级别的类型化对象的命名字段参数。见@Argument

@Arguments

用于访问绑定到更高级别的类型化对象的所有字段参数。见@Arguments

@ProjectedPayload界面

用于通过项目接口访问字段参数。请参阅@ProjectedPayload接口

资源

用于访问字段的源(即父/容器)实例。请参阅来源

DataLoader

用于访问DataLoader. DataLoaderRegistryDataLoader

@ContextValue

GraphQLContext用于从 main中访问属性DataFetchingEnvironment

@LocalContextValue

GraphQLContext用于从本地in访问属性DataFetchingEnvironment

GraphQLContext

用于从DataFetchingEnvironment.

java.security.Principal

从 Spring Security 上下文中获取(如果可用)。

@AuthenticationPrincipal

用于Authentication#getPrincipal()从 Spring Security 上下文访问。

DataFetchingFieldSelectionSet

用于通过 访问查询的选择集DataFetchingEnvironment

Locale,Optional<Locale>

对于LocaleDataFetchingEnvironment.

DataFetchingEnvironment

用于直接访问底层DataFetchingEnvironment.

模式映射处理程序方法可以返回:

  • 任何类型的解析值。

  • MonoFlux异步值。支持控制器方法和ReactiveDataFetcher中描述的任何方法。DataFetcher

  • java.util.concurrent.Callable使值异步生成。为此,AnnotatedControllerConfigurer必须使用Executor.

6.2.2.@Argument

在 GraphQL Java 中,DataFetchingEnvironment提供对特定于字段的参数值映射的访问。这些值可以是简单的标量值(例如 String、Long)、Map用于更复杂输入的 a 个值或 aList个值。

使用@Argument注解将参数绑定到目标对象并注入处理程序方法。绑定是通过将​​参数值映射到预期方法参数类型的主数据构造函数来执行的,或者通过使用默认构造函数来创建对象,然后将参数值映射到其属性。这是递归重复的,使用所有嵌套的参数值并相应地创建嵌套的目标对象。例如:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }
}

默认情况下,如果方法参数名称可用(需要-parametersJava 8+ 的编译器标志或来自编译器的调试信息),则它用于查找参数。如果需要,您可以通过注解自定义名称,例如@Argument("bookInput").

@Argument注释没有“必需”标志,也没有指定默认值的选项 。这两者都可以在 GraphQL 模式级别指定,并由 GraphQL Java 强制执行。

如果绑定失败,BindException则会引发 a 绑定问题累积为字段错误,其中field每个错误的 是发生问题的参数路径。

您可以@ArgumentMap<String, Object>参数上使用,以获取所有参数值。@Argument不得设置名称属性 on 。

6.2.3.@Arguments

@Arguments如果要将完整的参数映射绑定到单个目标对象,请使用注释,@Argument与绑定特定的命名参数相反。

例如,@Argument BookInput bookInput使用参数“bookInput”的值来初始化BookInput,同时@Arguments使用完整的参数映射,在这种情况下,顶级参数绑定到BookInput属性。

6.2.4. @ProjectedPayload界面

作为使用完整对象的替代方法@Argument,您还可以使用投影接口通过定义明确的最小接口访问 GraphQL 请求参数。 当 Spring Data 在类路径上时,参数投影由 Spring Data 的接口投影提供。

要利用这一点,请创建一个带有注释的接口@ProjectedPayload并将其声明为控制器方法参数。如果参数用 注释@Argument,则它适用于DataFetchingEnvironment.getArguments() 映射中的单个参数。如果没有声明@Argument,则投影适用于完整参数映射中的顶级参数。

例如:

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(BookIdProjection bookId) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInputProjection bookInput) {
        // ...
    }
}

@ProjectedPayload
interface BookIdProjection {

    Long getId();
}

@ProjectedPayload
interface BookInputProjection {

    String getName();

    @Value("#{target.author + ' ' + target.name}")
    String getAuthorAndName();
}

6.2.5。资源

在 GraphQL Java 中,DataFetchingEnvironment提供对字段源(即父/容器)实例的访问。要访问它,只需声明预期目标类型的方法参数。

@Controller
public class BookController {

    @SchemaMapping
    public Author author(Book book) {
        // ...
    }
}

源方法参数还有助于确定映射的类型名称。如果 Java 类的简单名称与 GraphQL 类型匹配,则无需在@SchemaMapping注解中显式指定类型名称。

@BatchMapping定源/父书籍对象的列表,处理程序方法可以批量加载查询的所有作者。

6.2.6。DataLoader

当您为实体注册批量加载函数时,如 批量加载中所述,您可以DataLoader通过声明类型的方法参数来访问实体DataLoader并使用它来加载实体:

@Controller
public class BookController {

    public BookController(BatchLoaderRegistry registry) {
        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
            // return Map<Long, Author>
        });
    }

    @SchemaMapping
    public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
        return loader.load(book.getAuthorId());
    }

}

默认情况下,BatchLoaderRegistry使用值类型的完整类名(例如 的类名Author)作为注册键,因此只需DataLoader使用泛型类型声明方法参数即可提供足够的信息来将其定位在DataLoaderRegistry. 作为后备,DataLoader方法参数解析器还将尝试方法参数名称作为键,但通常这不是必需的。

请注意,对于许多加载相关实体的情况,其中@SchemaMapping简单地委托给 a ,您可以使用下一节中描述的@BatchMappingDataLoader方法来减少样板 文件。

6.2.7. 验证

找到javax.validation.Validatorbean 时,AnnotatedControllerConfigurer启用 对带注释的控制器方法的Bean Validation的支持。通常,bean 的类型为LocalValidatorFactoryBean.

Bean 验证允许您声明类型的约束:

public class BookInput {

    @NotNull
    private String title;

    @NotNull
    @Size(max=13)
    private String isbn;
}

然后,您可以在方法调用之前注释控制器方法参数@Valid以验证它:

@Controller
public class BookController {

    @MutationMapping
    public Book addBook(@Argument @Valid BookInput bookInput) {
        // ...
    }
}

如果在验证期间发生错误,则会ConstraintViolationException引发 a。您可以使用异常解决链来决定如何将其转换为错误以包含在 GraphQL 响应中,从而将其呈现给客户端。

除了@Valid,您还可以使用@Validated允许指定验证组的 Spring。

Bean 验证对 、 和 @ProjectedPayload 方法参数很有用@Argument@Arguments 普遍地适用于任何方法参数。

验证和 Kotlin 协程

Hibernate Validator 与 Kotlin Coroutine 方法不兼容,并且在自省其方法参数时失败。请参阅 spring-projects/spring-graphql#344(评论) 以获取相关问题的链接和建议的解决方法。

6.3.@BatchMapping

批量加载通过使用 org.dataloader.DataLoader延迟加载单个实体实例来解决 N+1 选择问题,因此它们可以一起加载。例如:

@Controller
public class BookController {

    public BookController(BatchLoaderRegistry registry) {
        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
            // return Map<Long, Author>
        });
    }

    @SchemaMapping
    public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
        return loader.load(book.getAuthorId());
    }

}

对于加载关联实体的直接情况,如上所示,该 @SchemaMapping方法只做委托给DataLoader. 这是可以通过@BatchMapping方法避免的样板。例如:

@Controller
public class BookController {

    @BatchMapping
    public Mono<Map<Book, Author>> author(List<Book> books) {
        // ...
    }
}

以上成为批量加载函数,BatchLoaderRegistry 其中键是Book实例,加载的值是它们的作者。此外,a DataFetcher也透明地绑定到authortype 的字段Book,它只是委托给DataLoader作者,给定它的源/父Book实例。

要用作唯一键,Book必须实现hashcodeand equals

默认情况下,字段名默认为方法名,而类型名默认为输入List元素类型的简单类名。两者都可以通过注释属性进行自定义。类型名称也可以从类级别继承 @SchemaMapping

6.3.1. 方法签名

批量映射方法支持以下参数:

方法论据 描述

List<K>

源/父对象。

java.security.Principal

从 Spring Security 上下文中获取(如果可用)。

@ContextValue

GraphQLContext用于从of访问值,该值与来自 的BatchLoaderEnvironment上下文相同DataFetchingEnvironment

GraphQLContext

用于访问来自 的上下文,该上下文与来自 的BatchLoaderEnvironment上下文相同DataFetchingEnvironment

BatchLoaderEnvironment

GraphQL Java 中可用的环境到 org.dataloader.BatchLoaderWithContext.

批量映射方法可以返回:

返回类型 描述

Mono<Map<K,V>>

以父对象为键,批量加载对象为值的映射。

Flux<V>

一系列批量加载的对象,其顺序必须与传递给方法的源/父对象的顺序相同。

Map<K,V>,Collection<V>

命令式变体,例如无需远程调用。

Callable<Map<K,V>>,Callable<Collection<V>>

异步调用的命令式变体。为此, AnnotatedControllerConfigurer必须使用Executor.

7. 安全

可以使用 HTTP URL 安全性来保护Web GraphQL 端点的路径,以确保只有经过身份验证的用户才能访问它。但是,这并不能区分单个 URL 上此类共享端点上的不同 GraphQL 请求。

要应用更细粒度的安全性,请添加 Spring Security 注释,例如 @PreAuthorize@Secured到涉及获取 GraphQL 响应的特定部分的服务方法。由于上下文传播旨在使安全性和其他上下文在数据获取级别可用,这应该可以工作。

Spring for GraphQL 存储库包含 Spring MVCWebFlux的示例。

8. 客户

Spring for GraphQL 包括通过 HTTP、WebSocket 和 RSocket 执行 GraphQL 请求的客户端支持。

8.1。GraphQlClient

GraphQlClient是一个合约,它为 GraphQL 请求声明一个独立于底层传输的通用工作流。这意味着无论底层传输是什么,请求都使用相同的 API 执行,并且任何特定于传输的内容都是在构建时配置的。

要创建一个GraphQlClient,您需要以下扩展之一:

每个都定义了一个Builder与传输相关的选项。所有构建器都从一个通用的基础 GraphQlClient 扩展而来,并Builder带有与所有扩展相关的选项。

一旦你有了一个GraphQlClient,你就可以开始提出请求了。

8.1.1。HTTP

HttpGraphQlClient使用 WebClient通过 HTTP 执行 GraphQL 请求。

WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

HttpGraphQlClient创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。如果您需要更改任何特定于传输的详细信息,请mutate()在现有实例上使用HttpGraphQlClient以创建具有自定义设置的新实例:

WebClient webClient = ... ;

HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Perform requests with graphQlClient...

HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Perform requests with anotherGraphQlClient...

8.1.2. 网络套接字

WebSocketGraphQlClient通过共享的 WebSocket 连接执行 GraphQL 请求。它是使用 Spring WebFlux 中的 WebSocketClient构建的 ,您可以按如下方式创建它:

String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

与 相比HttpGraphQlClient,它WebSocketGraphQlClient是面向连接的,这意味着它需要在发出任何请求之前建立连接。当您开始发出请求时,会以透明方式建立连接。或者,使用客户端的start()方法在任何请求之前显式建立连接。

除了是面向连接的,WebSocketGraphQlClient也是多路复用的。它为所有请求维护一个单一的共享连接。如果连接丢失,则在下一次请求或start()再次调用时重新建立。您还可以使用客户端的stop()方法来取消正在进行的请求、关闭连接并拒绝新请求。

为每个服务器使用单个WebSocketGraphQlClient实例,以便为对该服务器的所有请求提供单个共享连接。每个客户端实例都建立自己的连接,这通常不是单个服务器的意图。

WebSocketGraphQlClient创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。如果您需要更改任何特定于传输的详细信息,请mutate()在现有实例上使用WebSocketGraphQlClient以创建具有自定义设置的新实例:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use graphQlClient...

WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherGraphQlClient...
拦截器

除了执行请求之外,GraphQL over WebSocket 协议还定义了许多面向连接的消息例如,客户端发送"connection_init",服务器 "connection_ack"在连接开始时响应。

对于 WebSocket 传输特定的拦截,您可以创建一个 WebSocketGraphQlClientInterceptor

static class MyInterceptor implements WebSocketGraphQlClientInterceptor {

    @Override
    public Mono<Object> connectionInitPayload() {
        // ... the "connection_init" payload to send
    }

    @Override
    public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
        // ... the "connection_ack" payload received
    }

}

将上述拦截器 注册GraphQlClientInterceptor为任何其他拦截器,并使用它来拦截 GraphQL 请求,但请注意最多可以有一个 type 的拦截器WebSocketGraphQlClientInterceptor

8.1.3. RSocket

RSocketGraphQlClient使用 RSocketRequester 通过 RSocket 请求执行 GraphQL 请求。

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlClient client = RSocketGraphQlClient.builder()
        .clientTransport(transport)
        .build();

相比之下HttpGraphQlClient,它RSocketGraphQlClient是面向连接的,这意味着它需要在发出任何请求之前建立会话。当您开始发出请求时,会话是透明地建立的。或者,使用客户端的start()方法在任何请求之前显式建立会话。

RSocketGraphQlClient也是多路复用的。它为所有请求维护一个单一的共享会话。如果会话丢失,则在下一次请求或 start()再次调用时重新建立。您还可以使用客户端的stop()方法来取消正在进行的请求、关闭会话并拒绝新请求。

为每个服务器使用一个RSocketGraphQlClient实例,以便为对该服务器的所有请求提供一个共享会话。每个客户端实例都建立自己的连接,这通常不是单个服务器的意图。

RSocketGraphQlClient创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。

8.1.4. 建造者

GraphQlClientBuilder为所有扩展的构建器定义了一个具有通用配置选项的父级。目前,它允许您配置:

  • DocumentSource为来自文件的请求加载文档的策略

  • 拦截执行的请求

8.2. 要求

一旦你有了GraphQlClient,你就可以开始通过 retrieve()execute()来执行请求 ,前者只是后者的快捷方式。

8.2.1。取回

下面检索和解码查询的数据:

String document = "{" +
        "  project(slug:\"spring-framework\") {" +
        "   name" +
        "   releases {" +
        "     version" +
        "   }"+
        "  }" +
        "}";

Mono<Project> projectMono = graphQlClient.document(document) (1)
        .retrieve("project") (2)
        .toEntity(Project.class); (3)
1 要执行的操作。
2 要从中解码的响应映射中“数据”键下的路径。
3 解码目标类型路径处的数据。

输入文档String可以是文字,也可以是通过代码生成的请求对象生成的。您还可以在文件中定义文档并使用 文档源通过文件名来解析它们。

该路径与“数据”键相关,并使用简单的点 (“.”) 分隔符号表示嵌套字段,其中包含列表元素的可选数组索引,例如"project.name""project.releases[0].version"

FieldAccessException如果给定路径不存在,或者字段值存在null并且有错误,则可能导致解码。FieldAccessException提供对响应和字段的访问:

Mono<Project> projectMono = graphQlClient.document(document)
        .retrieve("project")
        .toEntity(Project.class)
        .onErrorResume(FieldAccessException.class, ex -> {
            ClientGraphQlResponse response = ex.getResponse();
            // ...
            ResponseField field = ex.getField();
            // ...
        });

8.2.2. 执行

Retrieve只是从响应映射中的单个路径解码的快捷方式。如需更多控制,请使用该execute方法并处理响应:

例如:

Mono<Project> projectMono = graphQlClient.document(document)
        .execute()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure... (1)
            }

            ResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure... (2)
                }
                else {
                    // Optional field set to null... (3)
                }
            }

            return field.toEntity(Project.class); (4)
        });
1 响应没有数据,只有错误
2 null存在且具有关联错误的字段
3 null由其设置的字段DataFetcher
4 解码给定路径的数据

8.2.3. 文件来源

请求的文档String可以定义在局部变量或常量中,也可以通过代码生成的请求对象生成。

您还可以在类路径下创建带有扩展名.graphql.gql"graphql-documents/"的文档文件,并通过文件名引用它们。

例如,给定一个名为projectReleases.graphqlin 的文件src/main/resources/graphql-documents,其内容为:

src/main/resources/graphql/project.graphql
query projectReleases($slug: ID!) {
    project(slug: $slug) {
        name
        releases {
            version
        }
    }
}

然后您可以:

Mono<Project> projectMono = graphQlClient.documentName("projectReleases") (1)
        .variable("slug", "spring-framework") (2)
        .retrieve()
        .toEntity(Project.class);
1 从“project.graphql”加载文档
2 提供变量值。

IntelliJ 的“JS GraphQL”插件支持带有代码完成的 GraphQL 查询文件。

您可以使用GraphQlClient Builder自定义 DocumentSource按名称加载文档。

8.3. 订阅请求

GraphQlClient可以通过支持它的传输执行订阅。目前,只有 WebSocket 传输支持 GraphQL 流,因此您需要创建一个 WebSocketGraphQlClient

8.3.1. 取回

要启动订阅流,使用retrieveSubscriptionwhich 类似于 检索单个响应但返回响应流,每个响应都解码为一些数据:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .retrieveSubscription("greeting")
        .toEntity(String.class);

订阅流可能以:

  • SubscriptionErrorException如果服务器以包含一个或多个 GraphQL 错误的显式“错误”消息结束订阅。该异常提供对从该消息解码的 GraphQL 错误的访问。

  • GraphQlTransportException例如,WebSocketDisconnectedException如果底层连接关闭或丢失,您可以使用retry运算符重新建立连接并重新开始订阅。

8.3.2. 执行

检索只是从每个响应映射中的单个路径解码的快捷方式。如需更多控制,请使用该executeSubscription方法并直接处理每个响应:

Flux<String> greetingFlux = client.document("subscription { greetings }")
        .executeSubscription()
        .map(response -> {
            if (!response.isValid()) {
                // Request failure...
            }

            ResponseField field = response.field("project");
            if (!field.hasValue()) {
                if (field.getError() != null) {
                    // Field failure...
                }
                else {
                    // Optional field set to null... (3)
                }
            }

            return field.toEntity(String.class)
        });

8.4. 拦截

您创建一个GraphQlClientInterceptor通过客户端拦截所有请求:

static class MyInterceptor implements GraphQlClientInterceptor {

    @Override
    public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
        // ...
        return chain.next(request);
    }

    @Override
    public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
        // ...
        return chain.next(request);
    }

}

创建拦截器后,通过客户端构建器注册它:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
        .interceptor(new MyInterceptor())
        .build();

9. 测试

Spring for GraphQL 为通过 HTTP、WebSocket 和 RSocket 测试 GraphQL 请求以及直接针对服务器进行测试提供了专门的支持。

要使用它,请添加spring-graphql-test到您的构建中:

Gradle
Maven
dependencies {
    // ...
    testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0'
}

9.1。GraphQlTester

GraphQlTester是一个合约,它声明了一个用于测试 GraphQL 请求的通用工作流程,该工作流程独立于底层传输。这意味着无论底层传输是什么,都使用相同的 API 测试请求,并且在构建时配置任何特定于传输的内容。

要创建GraphQlTester通过客户端执行请求的,您需要以下扩展之一:

要创建一个GraphQlTester在服务器端执行测试的,没有客户端:

每个都定义了一个Builder与传输相关的选项。所有构建器都从一个通用的基础 GraphQlTester 扩展而来,并Builder带有与所有扩展相关的选项。

9.1.1。HTTP

HttpGraphQlTester使用 WebTestClient通过 HTTP 执行 GraphQL 请求,有或没有实时服务器,具体取决于 WebTestClient配置方式。

要在没有实时服务器的情况下在 Spring WebFlux 中进行测试,请指向声明 GraphQL HTTP 端点的 Spring 配置:

ApplicationContext context = ... ;

WebTestClient client =
        WebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

要在没有实时服务器的情况下在 Spring MVC 中进行测试,请使用以下方法执行相同操作MockMvcWebTestClient

ApplicationContext context = ... ;

WebTestClient client =
        MockMvcWebTestClient.bindToApplicationContext(context)
                .configureClient()
                .baseUrl("/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

或者针对在端口上运行的实时服务器进行测试:

WebTestClient client =
        WebTestClient.bindToServer()
                .baseUrl("http://localhost:8080/graphql")
                .build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

HttpGraphQlTester创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。如果您需要更改任何特定于传输的详细信息,请mutate()在现有实例上使用HttpSocketGraphQlTester以创建具有自定义设置的新实例:

HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

HttpGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...

9.1.2. 网络套接字

WebSocketGraphQlTester通过共享的 WebSocket 连接执行 GraphQL 请求。它是使用 Spring WebFlux 中的 WebSocketClient构建的 ,您可以按如下方式创建它:

String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();

WebSocketGraphQlTester是面向连接和多路复用的。每个实例都为所有请求建立自己的单一共享连接。通常,您只希望每台服务器使用一个实例。

WebSocketGraphQlTester创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。如果您需要更改任何特定于传输的详细信息,请mutate()在现有实例上使用WebSocketGraphQlTester以创建具有自定义设置的新实例:

URI url = ... ;
WebSocketClient client = ... ;

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

// Use tester...

WebSocketGraphQlTester anotherTester = tester.mutate()
        .headers(headers -> headers.setBasicAuth("peter", "..."))
        .build();

// Use anotherTester...

WebSocketGraphQlTester提供一种stop()可用于关闭 WebSocket 连接的方法,例如在测试运行后。

9.1.3. RSocket

RSocketGraphQlTester使用RSocketRequesterspring-messaging 通过 RSocket 执行 GraphQL 请求:

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlTester client = RSocketGraphQlTester.builder()
        .clientTransport(transport)
        .build();

RSocketGraphQlTester是面向连接和多路复用的。每个实例为所有请求建立自己的单个共享会话。通常,您只希望每台服务器使用一个实例。您可以使用stop()测试仪上的方法显式关闭会话。

RSocketGraphQlTester创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。

9.1.4。GraphQlService

很多时候,在服务器端测试 GraphQL 请求就足够了,而无需使用客户端通过传输协议发送请求。要直接针对 a 进行测试 ExecutionGraphQlService,请使用ExecutionGraphQlServiceTester扩展:

GraphQlService service = ... ;
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);

ExecutionGraphQlServiceTester创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。

9.1.5。WebGraphQlHandler

GraphQlService扩展允许您在服务器端进行测试,而无需客户端。但是,在某些情况下,使用给定的模拟传输输入涉及服务器端传输处理是有用的。

WebGraphQlTester扩展允许您 在传递给请求执行WebGraphQlInterceptor之前通过链处理请求:ExecutionGraphQlService

WebGraphQlHandler handler = ... ;
WebGraphQlTester tester = WebGraphQlTester.create(handler);

此扩展的构建器允许您定义 HTTP 请求详细信息:

WebGraphQlHandler handler = ... ;

WebGraphQlTester tester = WebGraphQlTester.builder(handler)
        .headers(headers -> headers.setBasicAuth("joe", "..."))
        .build();

WebGraphQlServiceTester创建后,您可以开始 使用相同的 API执行请求,而与底层传输无关。

9.1.6。建造者

GraphQlTesterBuilder为所有扩展的构建器定义了一个具有通用配置选项的父级。它允许您配置以下内容:

  • errorFilter- 抑制预期错误的谓词,因此您可以检查响应的数据。

  • documentSource- 从类路径上的文件或其他任何地方加载文档以获取请求的策略。

  • responseTimeout- 在超时之前等待请求执行完成多长时间。

9.2. 要求

一旦你有了GraphQlTester,你就可以开始测试请求了。下面对项目执行查询并使用JsonPath从响应中提取项目发布版本:

String document = "{" +
        "  project(slug:\"spring-framework\") {" +
        "   releases {" +
        "     version" +
        "   }"+
        "  }" +
        "}";

graphQlTester.document(document)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

JsonPath 与响应的“数据”部分相关。

您还可以在类路径下创建带有扩展名.graphql.gql"graphql-test/"的文档文件,并通过文件名引用它们。

例如,给定一个名为projectReleases.graphqlin 的文件src/main/resources/graphql-test,其内容为:

query projectReleases($slug: ID!) {
    project(slug: $slug) {
        releases {
            version
        }
    }
}

然后你可以使用:

graphQlTester.documentName("projectReleases") (1)
        .variable("slug", "spring-framework") (2)
        .execute()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);
1 请参阅名为“项目”的文件中的文档。
2 设置slug变量。

IntelliJ 的“JS GraphQL”插件支持带有代码完成的 GraphQL 查询文件。

如果请求没有任何响应数据,例如突变,使用executeAndVerify 代替execute来验证响应中没有错误:

graphQlTester.query(query).executeAndVerify();

有关错误处理的更多详细信息,请参阅错误。

9.3. 订阅

要测试订阅,请调用executeSubscription而不是execute获取响应流,然后使用StepVerifierfrom Project Reactor 来检查流:

Flux<String> greetingFlux = tester.document("subscription { greetings }")
        .executeSubscription()
        .toFlux("greetings", String.class);  // decode at JSONPath

StepVerifier.create(greetingFlux)
        .expectNext("Hi")
        .expectNext("Bonjour")
        .expectNext("Hola")
        .verifyComplete();

WebSocketGraphQlTester或服务器端 GraphQlServiceWebGraphQlHandler扩展支持订阅。

9.4。错误

使用时verify(),响应中“errors”键下的任何错误都会导致断言失败。要抑制特定错误,请在之前使用错误过滤器 verify()

graphQlTester.query(query)
        .execute()
        .errors()
        .filter(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

您可以在构建器级别注册错误过滤器,以应用于所有测试:

WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
        .errorFilter(error -> ...)
        .build();

如果你想验证一个错误确实存在,并且与 相比filter,如果不存在则抛出一个断言错误,然后使用exepect

graphQlTester.query(query)
        .execute()
        .errors()
        .expect(error -> ...)
        .verify()
        .path("project.releases[*].version")
        .entityList(String.class)
        .hasSizeGreaterThan(1);

您还可以通过 a 检查所有错误Consumer,这样做也将它们标记为已过滤,因此您还可以检查响应中的数据:

graphQlTester.query(query)
        .execute()
        .errors()
        .satisfy(errors -> {
            // ...
        });

10. 样品

这个 Spring for GraphQL 存储库包含各种场景的示例应用程序。

您可以通过克隆此存储库并从 IDE 运行主应用程序类或在命令行中键入以下内容来运行它们:

$ ./gradlew :samples:{sample-directory-name}:bootRun

1. see XML Configuration