3. 请求执行

ExecutionGraphQlService 是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,如 HTTP,会将请求委托给 ExecutionGraphQlService 处理。spring-doc.cadn.net.cn

The main implementation, DefaultExecutionGraphQlService, is configured with a GraphQlSource for access to the graphql.GraphQL instance to invoke.spring-doc.cadn.net.cn

3.1. GraphQLSource

GraphQlSource 是一个契约,用于暴露 graphql.GraphQL 实例并使用该实例,还包含了一个构建该实例的 builder API。默认的 builder 可以通过 GraphQlSource.schemaResourceBuilder() 获取。spring-doc.cadn.net.cn

Boot Starter 会创建此构建器的实例,并进一步将其初始化为从可配置位置 加载模式文件暴露属性以应用于 GraphQlSource.Builder,检测 RuntimeWiringConfigurer Bean, 用于 GraphQL 指标Instrumentation Bean, 以及用于 异常解析DataFetcherExceptionResolverSubscriptionExceptionResolver Bean。 如需进一步自定义,您还可以声明一个 GraphQlSourceBuilderCustomizer Bean,例如:spring-doc.cadn.net.cn

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

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

3.1.1. 架构资源

GraphQlSource.Builder 可以配置一个或多个 Resource 实例进行解析和合并。这意味着模式文件可以从几乎任何位置加载。spring-doc.cadn.net.cn

默认情况下,BootStarters会在classpath:graphql/**位置寻找扩展名为".graphqls"或".gqls"的模式文件,通常该位置是src/main/resources/graphql。你也可以使用文件系统位置,或者任何被Spring Resource层次结构支持的位置,包括从远程位置、存储中或内存中加载模式文件。spring-doc.cadn.net.cn

使用classpath*:graphql/**/来跨多个类路径位置查找模式文件,例如跨多个模块。

3.1.2. 模式创建

默认情况下,GraphQlSource.Builder使用GraphQL Java SchemaGenerator来创建 graphql.schema.GraphQLSchema。这适用于典型用例,但如果您需要使用不同的生成器(例如用于Federation),可以注册一个schemaFactory回调:spring-doc.cadn.net.cn

GraphQlSource.Builder builder = ...

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

The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。spring-doc.cadn.net.cn

对于 Apollo Federation 的示例,请参见 federation-jvm-spring-examplespring-doc.cadn.net.cn

3.1.3. 模式遍历

您可以通过builder.schemaResources(..).typeVisitors(..)注册一个graphql.schema.GraphQLTypeVisitor,以便在模式创建之后遍历该模式,并可能应用更改到GraphQLCodeRegistry。请注意,这样的访问者不能改变模式。如果您需要对模式进行更改,请参阅模式转换spring-doc.cadn.net.cn

3.1.4. 模式转换

您可以通过builder.schemaResources(..).typeVisitorsToTransformSchema(..)注册一个graphql.schema.GraphQLTypeVisitor,以便在创建后遍历和转换模式,并对模式进行更改。请注意,这比模式遍历更昂贵,因此通常除非需要对模式进行更改,否则请优先选择遍历来避免转换。spring-doc.cadn.net.cn

3.1.5. RuntimeWiringConfigurer

您可以使用 RuntimeWiringConfigurer 进行注册:spring-doc.cadn.net.cn

GraphQL Java,服务器应用仅使用Jackson进行数据到地图的数据序列化和反序列化。 客户端输入会被解析成一个映射。服务器输出会根据字段选择集组装成一个映射。 这意味着你不能依赖于Jackson的序列化/反序列化注解。 相反,你可以使用自定义标量类型

The Boot Starter 检测类型为 RuntimeWiringConfigurer 的 bean 并将其注册在 GraphQlSource.Builder 中。这意味着大多数情况下,你的配置中会有如下内容:spring-doc.cadn.net.cn

@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>。这允许你添加任意数量的工厂,并依次调用这些工厂。spring-doc.cadn.net.cn

3.1.6. 默认TypeResolver

GraphQlSource.BuilderClassNameTypeResolver 注册为默认的 TypeResolver,用于那些尚未通过 RuntimeWiringConfigurer 进行此类注册的 GraphQL 接口和联合类型。在 GraphQL Java 中,TypeResolver 的作用是确定从 GraphQL 接口或联合字段的 DataFetcher 返回的值所对应的 GraphQL 对象类型。spring-doc.cadn.net.cn

ClassNameTypeResolver 尝试将值的简单类名与 GraphQL 对象类型匹配,如果失败,则会导航到其超类型(包括基类和接口)以寻找匹配。ClassNameTypeResolver 提供了一个配置名称提取函数的选择,并且可以与Class 一起使用GraphQL对象类型的名称映射来帮助覆盖更多边缘情况。spring-doc.cadn.net.cn

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

The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。spring-doc.cadn.net.cn

3.1.7. 操作缓存

GraphQL Java 在执行操作之前必须解析验证该操作。这可能会显著影响性能。为了避免重复解析和验证,应用程序可以配置一个PreparsedDocumentProvider来缓存并复用 Document 实例。GraphQL Java 文档提供了通过PreparsedDocumentProvider进行查询缓存的更多详细信息。spring-doc.cadn.net.cn

在Spring GraphQL中,您可以注册一个PreparsedDocumentProviderGraphQlSource.Builder#configureGraphQlspring-doc.cadn.net.cn

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

// Create provider
PreparsedDocumentProvider provider = ...

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

The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。spring-doc.cadn.net.cn

3.1.8. 指令

GraphQL语言支持描述GraphQL文档中“替代运行时执行和类型验证行为”的指令。这些指令类似于Java中的注解,但在GraphQL文档中声明在类型、字段、片段和操作上。spring-doc.cadn.net.cn

GraphQL Java 提供了 SchemaDirectiveWiring 合约来帮助应用程序检测和处理指令。更多细节,请参见 Schema Directives 在 GraphQL Java 文档中。spring-doc.cadn.net.cn

在 Spring GraphQL 中,您可以通过 RuntimeWiringConfigurer注册一个SchemaDirectiveWiringBoot Starter会检测此类 Bean,因此您可能会有类似如下配置:spring-doc.cadn.net.cn

@Configuration
public class GraphQlConfig {

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

}
对于指令支持的示例,请参阅 GraphQL Java 扩展验证库

3.2. 响应式DataFetcher

The default GraphQlSource builder 启用支持从DataFetcher返回MonoFlux,这些会被适配为一个CompletableFuture,其中Flux值被聚合并转换成一个 List,除非请求是 GraphQL 订阅请求,在这种情况下,返回值保持为用于流式处理 GraphQL 响应的 Reactive Streams Publisherspring-doc.cadn.net.cn

一个响应式的DataFetcher可以依赖于从传输层传播而来的Reactor上下文,例如来自WebFlux请求处理,请参阅 WebFlux 上下文spring-doc.cadn.net.cn

3.3. 上下文传播

Spring for GraphQL 提供支持,可以在从 HTTP 透明传递上下文通过 GraphQL Java,并传入 DataFetcher 和其他它调用的组件。这包括来自 Spring MVC 请求处理线程的 ThreadLocal 上下文以及来自 WebFlux 处理流水线的 Reactor Contextspring-doc.cadn.net.cn

3.3.1. WebMvc

由 GraphQL Java 调用的 DataFetcher 和其他组件可能不会始终在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步 WebGraphQlInterceptorDataFetcher 切换到不同的线程。spring-doc.cadn.net.cn

Spring for GraphQL 支持从 Servlet 容器线程向由 GraphQL Java 调用并执行的DataFetcher和其他组件传递ThreadLocal值。为此,应用程序需要为感兴趣的ThreadLocal值实现io.micrometer.context.ThreadLocalAccessorspring-doc.cadn.net.cn

public class RequestAttributesAccessor implements ThreadLocalAccessor<RequestAttributes> {

    @Override
    public Object key() {
        return RequestAttributesAccessor.class.getName();
    }

    @Override
    public RequestAttributes getValue() {
        return RequestContextHolder.getRequestAttributes();
    }

    @Override
    public void setValue(RequestAttributes attributes) {
        RequestContextHolder.setRequestAttributes(attributes);
    }

    @Override
    public void reset() {
        RequestContextHolder.resetRequestAttributes();
    }

}

您可以在启动时通过全局的ContextRegistry 实例手动注册一个ThreadLocalAccessor,该实例可通过io.micrometer.context.ContextRegistry#getInstance()访问。您也可以通过java.util.ServiceLoader机制自动进行注册。spring-doc.cadn.net.cn

3.3.2. WebFlux

一个 响应式 DataFetcher 可以依赖源自 WebFlux 请求处理链的 Reactor 上下文。这包括由 WebGraphQlInterceptor 组件添加的 Reactor 上下文。spring-doc.cadn.net.cn

3.4. 异常解析

Java Spring 框架中的一个 GraphQL 应用程序可以注册一个 DataFetcherExceptionHandler,以决定如何在 GraphQL 响应的 "errors" 部分中表示数据层产生的异常。spring-doc.cadn.net.cn

Spring for GraphQL 拥有一个内置的 DataFetcherExceptionHandler,该组件已配置为由默认的 GraphQLSource 构建器使用。它允许应用程序注册一个或多个 Spring DataFetcherExceptionResolver 组件,这些组件将被顺序调用,直到其中一个将 Exception 解析为(可能为空的)graphql.GraphQLError 对象列表。spring-doc.cadn.net.cn

DataFetcherExceptionResolver 是一个异步合约。对于大多数实现而言,扩展DataFetcherExceptionResolverAdapter并重写其resolveToSingleErrorresolveToMultipleErrors方法以同步解决异常就足够了。spring-doc.cadn.net.cn

一个GraphQLError可以通过graphql.ErrorClassification赋值给类别。 在Spring GraphQL中,你也可以通过ErrorType进行赋值,它具有以下常见的错误分类,应用程序可以使用这些分类来对错误进行分组:spring-doc.cadn.net.cn

如果一个异常未被解决,默认情况下它会被归类为一个INTERNAL_ERROR ,带有包含类别名称和从DataFetchingEnvironment获取的executionId的一个通用消息。该消息故意设计得模糊不清以避免泄露实现细节。应用程序可以使用DataFetcherExceptionResolver来自定义错误详情。spring-doc.cadn.net.cn

未解决的异常将以 ERROR 级别记录,并附带 executionId 以与发送给客户端的错误相关联。已解决的异常将以 DEBUG 级别记录。spring-doc.cadn.net.cn

3.4.1. 请求异常

The GraphQL Java引擎在解析请求时可能会遇到验证或其他错误,这会导致请求执行被阻止。在这种情况下,响应包含一个"data"键以及一个或多个全局的"errors"(即没有字段路径),这些错误是请求级别的。spring-doc.cadn.net.cn

DataFetcherExceptionResolver 无法处理此类全局错误,因为这些错误在开始执行之前以及在调用任何 DataFetcher 之前就已抛出。应用程序可以使用传输级拦截器来检查和转换 ExecutionResult 中的错误。 请参阅 WebGraphQlInterceptor 下的示例。spring-doc.cadn.net.cn

3.4.2. 订阅异常

订阅请求中的Publisher可能以错误信号完成,在这种情况下,底层传输(例如WebSocket)会发送一个最终的"error"类型消息,并附带GraphQL错误列表。spring-doc.cadn.net.cn

DataFetcherExceptionResolver 无法解决来自订阅 Publisher 的错误, 因为数据 DataFetcher 只是在初始化时创建了 Publisher。之后,传输会订阅可能随后因错误而完成的 Publisherspring-doc.cadn.net.cn

一个应用程序可以注册一个SubscriptionExceptionResolver,以便解析来自订阅Publisher的异常,并将这些异常转换为GraphQL错误发送给客户端。spring-doc.cadn.net.cn

3.5. 批量加载

给定一个Book和其Author,我们可以为一本书创建一个DataFetcher,同时为其作者创建另一个DataFetcher。这使得可以选择带有或不带作者的书籍,但这也意味着书籍和作者不会一起加载,在查询多本书籍时,每个书籍的作者需要单独加载,这称为N+1次选择问题。spring-doc.cadn.net.cn

3.5.1. DataLoader

GraphQL Java 提供了一个 DataLoader 机制用于批量加载相关实体。
你可以在GraphQL Java 文档中找到全部详情。下面是一个
该机制工作原理的简要总结:spring-doc.cadn.net.cn

  1. 注册DataLoader在可以加载实体的DataLoaderRegistry中,给定唯一键。spring-doc.cadn.net.cn

  2. DataFetcher's 可以访问 DataLoader's 并使用它们通过 ID 加载实体。spring-doc.cadn.net.cn

  3. 一个 DataLoader 通过返回一个未来来推迟加载,因此可以在批处理中进行。spring-doc.cadn.net.cn

  4. DataLoader's维护一个在每次请求中加载实体的缓存,这可以进一步提高效率。spring-doc.cadn.net.cn

3.5.2. BatchLoaderRegistry

The complete batching loading mechanism in GraphQL Java requires implementing one of several BatchLoader interface, then wrapping and registering those as DataLoaders with a name in the DataLoaderRegistry.spring-doc.cadn.net.cn

Spring GraphQL 的 API 稍微有些不同。注册时,只有一个中心的 GraphQLSchema 暴露工厂方法和一个构建器来创建并注册任意数量的批加载函数:spring-doc.cadn.net.cn

@Configuration
public class MyConfig {

    public MyConfig(BatchLoaderRegistry registry) {

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

        // more registrations ...
    }

}

The Boot Starter 声明了一个 BatchLoaderRegistry bean,您可以将其注入到您的配置中,如上所示,或者注入任何组件(例如控制器)以注册批量加载功能。反过来,BatchLoaderRegistry 被注入到 DefaultExecutionGraphQlService 中,在那里它确保每次请求有 DataLoader 的注册。spring-doc.cadn.net.cn

默认情况下,DataLoader名称基于目标实体的类名。 这使得一个@SchemaMapping方法能够声明一个带有泛型类型的 DataLoader参数, 无需指定名称。然而,如果需要的话,可以通过 BatchLoaderRegistry构建器自定义名称,并且还可以自定义其他DataLoaderOptionsspring-doc.cadn.net.cn

要将默认DataLoaderOptions全局配置为任何注册的起点,您可以重写 Boot 的 BatchLoaderRegistry 模块,并使用 DefaultBatchLoaderRegistry 接受 Supplier<DataLoaderOptions> 的构造函数。spring-doc.cadn.net.cn

对于许多情况,在加载相关实体时,你可以使用 @BatchMapping 控制器方法,这相当于并取代了直接使用 BatchLoaderRegistryDataLoader 的需要。spring-doc.cadn.net.cn

BatchLoaderRegistry 还提供了其他重要的好处。它支持从批量加载函数和 @BatchMapping 方法访问相同的 GraphQLContext,并且确保 Context Propagation 到这些地方。这也是为什么应用程序被期望使用它的原因。可以直接进行自己的DataLoader 注册,但这样的注册将放弃上述所有好处。spring-doc.cadn.net.cn

3.5.3. 测试批量加载

开始让BatchLoaderRegistry对一个DataLoaderRegistry进行注册:spring-doc.cadn.net.cn

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

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

现在您可以访问并测试单个DataLoader,操作如下:spring-doc.cadn.net.cn

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("...");
// ...