4. 请求执行
ExecutionGraphQlService 是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,如 服务器传输,将请求委托给 ExecutionGraphQlService 处理。
The main implementation, DefaultExecutionGraphQlService, is configured with a
GraphQlSource for access to the graphql.GraphQL instance to invoke.
4.1. GraphQLSource
GraphQlSource 是 Spring 框架的核心抽象,用于访问用于请求执行的
graphql.GraphQL 实例。它提供了一个构建器 API 来初始化 GraphQL Java 并构建一个
GraphQlSource。
默认的 GraphQlSource 构建器,可通过 GraphQlSource.schemaResourceBuilder() 访问,支持
响应式 DataFetcher、上下文传播 和 异常解析。
The Spring Boot Starters 初始化一个
GraphQlSource 实例并通过默认的 GraphQlSource.Builder 也启用以下功能:
-
Load 模式文件 from a configurable location.
-
暴露适用于
GraphQlSource.Builder的属性属性。 -
检测到
RuntimeWiringConfigurer个 Bean。 -
检测 指标 Bean 用于 GraphQL 指标.
-
检测到
DataFetcherExceptionResolver个用于 异常解析 的 bean。 -
检测到
SubscriptionExceptionResolver个与订阅异常解析相关的bean。
进一步自定义时,您可以声明自己所需的GraphQlSourceBuilderCustomizer bean;例如,配置您自己的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 起始模板会在位置 classpath:graphql/** 下寻找扩展名为 ".graphqls" 或 ".gqls" 的模式文件,该位置通常为 src/main/resources/graphql。你也可以使用文件系统路径,或任何由 Spring Resource 命名空间支持的位置,包括从远程位置、存储中或内存中加载模式文件的自定义实现。
使用classpath*:graphql/**/来跨多个类路径位置查找模式文件,例如跨多个模块。 |
4.1.2. 模式创建
默认情况下,GraphQlSource.Builder 使用 GraphQL Java GraphQLSchemaGenerator 创建 graphql.schema.GraphQLSchema。这适用于大多数应用程序,但如果需要,您可以通过构建器钩入模式创建过程:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
这是主要原因,通过Federation库来创建模式。
The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。
4.1.3. RuntimeWiringConfigurer
您可以使用 RuntimeWiringConfigurer 进行注册:
-
自定义标量类型。
-
处理指令的代码。
-
TypeResolver,如果您需要为某个类型覆盖 默认的TypeResolver。 -
DataFetcher对应一个字段,尽管大多数应用程序会简单地配置为AnnotatedControllerConfigurer,该值用于检测带有注解的DataFetcher处理器方法。Spring Boot 起步包默认添加了AnnotatedControllerConfigurer。
| 不像Web框架,GraphQL 不使用Jackson注解来驱动JSON序列化/反序列化。 自定义数据类型及其序列化 必须描述为Scalar。 |
The Spring BootStarters检测类型为RuntimeWiringConfigurer的bean,并将其注册到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,用于那些尚未通过 RuntimeWiringConfigurer 进行此类注册的 GraphQL 接口和联合类型。在 GraphQL Java 中,TypeResolver 的作用是确定从 GraphQL 接口或联合字段的 DataFetcher 返回的值所对应的 GraphQL 对象类型。
ClassNameTypeResolver 尝试将值的简单类名与 GraphQL 对象类型匹配,如果失败,则会导航到其超类型(包括基类和接口)以寻找匹配。ClassNameTypeResolver 提供了一个配置名称提取函数的选择,并且可以与Class 一起使用GraphQL对象类型的名称映射来帮助覆盖更多边缘情况。
GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
// Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);
The 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))
The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。
4.1.6. 指令
GraphQL语言支持描述GraphQL文档中“替代运行时执行和类型验证行为”的指令。这些指令类似于Java中的注解,但在GraphQL文档中声明在类型、字段、片段和操作上。
GraphQL Java 提供了 SchemaDirectiveWiring 合约来帮助应用程序检测和处理指令。更多细节,请参见
Schema Directives 在
GraphQL Java 文档中。
在 Spring GraphQL 中,您可以通过 SchemaDirectiveWiring 注册一个 RuntimeWiringConfigurer。Spring Boot Starter 会自动检测此类 Bean,因此您可以这样配置:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
| 对于指令支持的示例,请参阅 GraphQL Java 扩展验证库。 |
4.2. 响应式DataFetcher
The default GraphQlSource builder 启用支持从DataFetcher返回Mono或Flux,这些会被适配为一个CompletableFuture,其中Flux值被聚合并转换成一个 List,除非请求是 GraphQL 订阅请求,在这种情况下,返回值保持为用于流式处理 GraphQL 响应的 Reactive Streams Publisher。
一个响应式的DataFetcher可以依赖于从传输层传播而来的Reactor上下文,例如来自WebFlux请求处理,请参阅
WebFlux 上下文。
4.3. 上下文传播
Spring for GraphQL 提供了对从
服务器传输透明传播上下文的支持,通过 GraphQL Java,并传递到 DataFetcher 及其调用的其他组件。这包括来自 Spring MVC 请求处理线程的 ThreadLocal 上下文,以及来自 WebFlux 处理管道的 Reactor Context。
4.3.1. WebMvc
由 GraphQL Java 调用的 DataFetcher 和其他组件可能不会始终在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步
WebGraphQlInterceptor 或 DataFetcher 切换到不同的线程。
Spring for GraphQL 支持将 ThreadLocal 值从 Servlet 容器线程传播到 GraphQL Java 及其调用的其他组件用于执行的 DataFetcher 和其他线程。为此,应用程序需要创建一个 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();
}
}
可以在 WebGraphHandler 构建器中注册一个 ThreadLocalAccessor。Boot Starters会检测此类类型的 Bean,并自动为 Spring MVC 应用程序注册它们,请参阅 Web 端点 部分。
4.3.2. WebFlux
一个 响应式 DataFetcher 可以依赖源自 WebFlux 请求处理链的 Reactor 上下文。这包括由 WebGraphQlInterceptor 组件添加的 Reactor 上下文。
4.4. 异常解析
Java Spring 框架中的一个 GraphQL 应用程序可以注册一个 DataFetcherExceptionHandler,以决定如何在 GraphQL 响应的 "errors" 部分中表示数据层产生的异常。
Spring for GraphQL 拥有一个内置的 DataFetcherExceptionHandler,该组件已配置为由默认的 GraphQLSource 构建器使用。它允许应用程序注册一个或多个 Spring DataFetcherExceptionResolver 组件,这些组件将被顺序调用,直到其中一个将 Exception 解析为(可能为空的)graphql.GraphQLError 对象列表。
DataFetcherExceptionResolver 是一个异步合约。对于大多数实现而言,扩展DataFetcherExceptionResolverAdapter并重写其resolveToSingleError或resolveToMultipleErrors方法以同步解决异常就足够了。
一个GraphQLError可以通过graphql.ErrorClassification赋值给类别。
在Spring GraphQL中,你也可以通过ErrorType进行赋值,它具有以下常见的错误分类,应用程序可以使用这些分类来对错误进行分组:
-
BAD_REQUEST -
UNAUTHORIZED -
FORBIDDEN -
NOT_FOUND -
INTERNAL_ERROR
如果一个异常未被解决,默认情况下它会被归类为一个INTERNAL_ERROR
,带有包含类别名称和从DataFetchingEnvironment获取的executionId的一个通用消息。该消息故意设计得模糊不清以避免泄露实现细节。应用程序可以使用DataFetcherExceptionResolver来自定义错误详情。
未解决的异常将以 ERROR 级别记录,并附带 executionId 以与发送给客户端的错误相关联。已解决的异常将以 DEBUG 级别记录。
4.4.1. 请求异常
The GraphQL Java引擎在解析请求时可能会遇到验证或其他错误,这会导致请求执行被阻止。在这种情况下,响应包含一个"data"键以及一个或多个全局的"errors"(即没有字段路径),这些错误是请求级别的。
DataFetcherExceptionResolver 无法处理此类全局错误,因为这些错误在开始执行之前以及在调用任何 DataFetcher 之前就已抛出。应用程序可以使用传输级拦截器来检查和转换 ExecutionResult 中的错误。
请参阅 WebGraphQlInterceptor 下的示例。
4.4.2. 订阅异常
订阅请求中的Publisher可能以错误信号完成,在这种情况下,底层传输(例如WebSocket)会发送一个最终的"error"类型消息,并附带GraphQL错误列表。
DataFetcherExceptionResolver 无法解决来自订阅 Publisher 的错误,
因为数据 DataFetcher 只是在初始化时创建了 Publisher。之后,传输会订阅可能随后因错误而完成的 Publisher。
一个应用程序可以注册一个SubscriptionExceptionResolver,以便解析来自订阅Publisher的异常,并将这些异常转换为GraphQL错误发送给客户端。
4.5. 批量加载
给定一个Book和其Author,我们可以为一本书创建一个DataFetcher,同时为其作者创建另一个DataFetcher。这使得可以选择带有或不带作者的书籍,但这也意味着书籍和作者不会一起加载,在查询多本书籍时,每个书籍的作者需要单独加载,这称为N+1次选择问题。
4.5.1. DataLoader
GraphQL Java 提供了一个 DataLoader 机制用于批量加载相关实体。
你可以在GraphQL Java 文档中找到全部详情。下面是一个
该机制工作原理的简要总结:
-
注册
DataLoader在可以加载实体的DataLoaderRegistry中,给定唯一键。 -
DataFetcher's 可以访问DataLoader's 并使用它们通过 ID 加载实体。 -
一个
DataLoader通过返回一个未来来推迟加载,因此可以在批处理中进行。 -
DataLoader's维护一个在每次请求中加载实体的缓存,这可以进一步提高效率。
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 GraphQL 的 API 稍微有些不同。注册时,只有一个中心的 GraphQLSchema 暴露工厂方法和一个构建器来创建并注册任意数量的批加载函数:
@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 次注册。
默认情况下,DataLoader 名称基于目标实体的类名。
这允许 @SchemaMapping 方法声明一个
DataLoader 参数,并带有泛型类型,
而无需指定名称。不过,如有必要,可以通过
BatchLoaderRegistry 构建器自定义该名称,同时配置其他 DataLoader 选项。
对于许多情况,在加载相关实体时,您可以使用
@BatchMapping 控制器方法,它是 BatchLoaderRegistry 和 DataLoader 的快捷方式,可替代直接使用它们的需求。
s
BatchLoaderRegistry 还提供其他重要优势。它支持从批处理加载函数和 @BatchMapping 方法访问相同的 GraphQLContext,
并确保 上下文传播(Context Propagation) 到这些位置。因此,应用程序应使用它。
虽然可以直接进行自定义的 DataLoader 注册,但此类注册将放弃上述优势。
4.5.3. 测试批量加载
开始让BatchLoaderRegistry对一个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("...");
// ...