4. 请求执行

ExecutionGraphQlService 是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,如 服务器传输,将请求委托给 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

4.1. GraphQLSource

GraphQlSource 是 Spring 框架的核心抽象,用于访问用于请求执行的 graphql.GraphQL 实例。它提供了一个构建器 API 来初始化 GraphQL Java 并构建一个 GraphQlSourcespring-doc.cadn.net.cn

默认的 GraphQlSource 构建器,可通过 GraphQlSource.schemaResourceBuilder() 访问,支持 响应式 DataFetcher上下文传播异常解析spring-doc.cadn.net.cn

The Spring Boot Starters 初始化一个 GraphQlSource 实例并通过默认的 GraphQlSource.Builder 也启用以下功能:spring-doc.cadn.net.cn

进一步自定义时,您可以声明自己所需的GraphQlSourceBuilderCustomizer bean;例如,配置您自己的ExecutionIdProvider:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

默认情况下,Spring Boot 起始模板会在位置 classpath:graphql/** 下寻找扩展名为 ".graphqls" 或 ".gqls" 的模式文件,该位置通常为 src/main/resources/graphql。你也可以使用文件系统路径,或任何由 Spring Resource 命名空间支持的位置,包括从远程位置、存储中或内存中加载模式文件的自定义实现。spring-doc.cadn.net.cn

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

4.1.2. 模式创建

默认情况下,GraphQlSource.Builder 使用 GraphQL Java GraphQLSchemaGenerator 创建 graphql.schema.GraphQLSchema。这适用于大多数应用程序,但如果需要,您可以通过构建器钩入模式创建过程:spring-doc.cadn.net.cn

GraphQlSource.Builder builder = ...

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

这是主要原因,通过Federation库来创建模式。spring-doc.cadn.net.cn

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

4.1.3. RuntimeWiringConfigurer

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

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

The Spring BootStarters检测类型为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

4.1.4. 默认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

4.1.5. 操作缓存

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

4.1.6. 指令

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

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

在 Spring GraphQL 中,您可以通过 SchemaDirectiveWiring 注册一个 RuntimeWiringConfigurer。Spring Boot Starter 会自动检测此类 Bean,因此您可以这样配置:spring-doc.cadn.net.cn

@Configuration
public class GraphQlConfig {

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

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

4.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

4.3. 上下文传播

Spring for GraphQL 提供了对从 服务器传输透明传播上下文的支持,通过 GraphQL Java,并传递到 DataFetcher 及其调用的其他组件。这包括来自 Spring MVC 请求处理线程的 ThreadLocal 上下文,以及来自 WebFlux 处理管道的 Reactor Contextspring-doc.cadn.net.cn

4.3.1. WebMvc

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

Spring for GraphQL 支持将 ThreadLocal 值从 Servlet 容器线程传播到 GraphQL Java 及其调用的其他组件用于执行的 DataFetcher 和其他线程。为此,应用程序需要创建一个 ThreadLocalAccessor 以提取感兴趣的 ThreadLocal 值:spring-doc.cadn.net.cn

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();
    }

}

可以在 WebGraphHandler 构建器中注册一个 ThreadLocalAccessor。Boot Starters会检测此类类型的 Bean,并自动为 Spring MVC 应用程序注册它们,请参阅 Web 端点 部分。spring-doc.cadn.net.cn

4.3.2. WebFlux

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

4.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

4.4.1. 请求异常

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

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

4.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

4.5. 批量加载

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

4.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

4.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 ...
    }

}

Spring Boot Starters声明了一个 BatchLoaderRegistry Bean,您可以将其注入到配置中(如上所示),或注入到任何组件(如控制器)中以注册批量加载函数。反过来,BatchLoaderRegistry 会被注入到 DefaultExecutionGraphQlService 中,从而确保每个请求有 DataLoader 次注册。spring-doc.cadn.net.cn

默认情况下,DataLoader 名称基于目标实体的类名。 这允许 @SchemaMapping 方法声明一个 DataLoader 参数,并带有泛型类型, 而无需指定名称。不过,如有必要,可以通过 BatchLoaderRegistry 构建器自定义该名称,同时配置其他 DataLoader 选项。spring-doc.cadn.net.cn

对于许多情况,在加载相关实体时,您可以使用 @BatchMapping 控制器方法,它是 BatchLoaderRegistryDataLoader 的快捷方式,可替代直接使用它们的需求。 s BatchLoaderRegistry 还提供其他重要优势。它支持从批处理加载函数和 @BatchMapping 方法访问相同的 GraphQLContext, 并确保 上下文传播(Context Propagation) 到这些位置。因此,应用程序应使用它。 虽然可以直接进行自定义的 DataLoader 注册,但此类注册将放弃上述优势。spring-doc.cadn.net.cn

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