3. 请求执行
ExecutionGraphQlService 是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,如 HTTP,会将请求委托给 ExecutionGraphQlService 处理。
The main implementation, DefaultExecutionGraphQlService, is configured with a
GraphQlSource for access to the graphql.GraphQL instance to invoke.
3.1. GraphQLSource
GraphQlSource 是一个契约,用于暴露 graphql.GraphQL 实例并使用该实例,还包含了一个构建该实例的 builder API。默认的 builder 可以通过 GraphQlSource.schemaResourceBuilder() 获取。
Boot Starter 会创建此构建器的实例,并进一步将其初始化为从可配置位置 加载模式文件,
暴露属性以应用于 GraphQlSource.Builder,检测
RuntimeWiringConfigurer Bean,
用于 GraphQL 指标的 Instrumentation Bean,
以及用于 异常解析的 DataFetcherExceptionResolver 和 SubscriptionExceptionResolver Bean。
如需进一步自定义,您还可以声明一个 GraphQlSourceBuilderCustomizer Bean,例如:
@Configuration(proxyBeanMethods = false)
class GraphQlConfig {
@Bean
public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
return (builder) ->
builder.configureGraphQl(graphQlBuilder ->
graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
}
}
3.1.1. 架构资源
GraphQlSource.Builder 可以配置一个或多个 Resource 实例进行解析和合并。这意味着模式文件可以从几乎任何位置加载。
默认情况下,BootStarters会在classpath:graphql/**位置寻找扩展名为".graphqls"或".gqls"的模式文件,通常该位置是src/main/resources/graphql。你也可以使用文件系统位置,或者任何被Spring Resource层次结构支持的位置,包括从远程位置、存储中或内存中加载模式文件。
使用classpath*:graphql/**/来跨多个类路径位置查找模式文件,例如跨多个模块。 |
3.1.2. 模式创建
默认情况下,GraphQlSource.Builder使用GraphQL Java SchemaGenerator来创建
graphql.schema.GraphQLSchema。这适用于典型用例,但如果您需要使用不同的生成器(例如用于Federation),可以注册一个schemaFactory回调:
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
The GraphQlSource 部分 解释了如何使用 Spring Boot 进行配置。
对于 Apollo Federation 的示例,请参见 federation-jvm-spring-example。
3.1.3. 模式遍历
您可以通过builder.schemaResources(..).typeVisitors(..)注册一个graphql.schema.GraphQLTypeVisitor,以便在模式创建之后遍历该模式,并可能应用更改到GraphQLCodeRegistry。请注意,这样的访问者不能改变模式。如果您需要对模式进行更改,请参阅模式转换。
3.1.4. 模式转换
您可以通过builder.schemaResources(..).typeVisitorsToTransformSchema(..)注册一个graphql.schema.GraphQLTypeVisitor,以便在创建后遍历和转换模式,并对模式进行更改。请注意,这比模式遍历更昂贵,因此通常除非需要对模式进行更改,否则请优先选择遍历来避免转换。
3.1.5. RuntimeWiringConfigurer
您可以使用 RuntimeWiringConfigurer 进行注册:
-
自定义标量类型。
-
处理指令的代码。
-
TypeResolver,如果您需要为某个类型覆盖 默认的TypeResolver。 -
DataFetcher用于字段,尽管大多数应用程序将简单地配置为AnnotatedControllerConfigurer,它会检测带有注解的DataFetcher处理器方法。该 引导Starters(Boot Starter) 默认添加了AnnotatedControllerConfigurer。
| GraphQL Java,服务器应用仅使用Jackson进行数据到地图的数据序列化和反序列化。 客户端输入会被解析成一个映射。服务器输出会根据字段选择集组装成一个映射。 这意味着你不能依赖于Jackson的序列化/反序列化注解。 相反,你可以使用自定义标量类型。 |
The Boot Starter 检测类型为 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>。这允许你添加任意数量的工厂,并依次调用这些工厂。
3.1.6. 默认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 进行配置。
3.1.7. 操作缓存
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 进行配置。
3.1.8. 指令
GraphQL语言支持描述GraphQL文档中“替代运行时执行和类型验证行为”的指令。这些指令类似于Java中的注解,但在GraphQL文档中声明在类型、字段、片段和操作上。
GraphQL Java 提供了 SchemaDirectiveWiring 合约来帮助应用程序检测和处理指令。更多细节,请参见
Schema Directives 在
GraphQL Java 文档中。
在 Spring GraphQL 中,您可以通过
RuntimeWiringConfigurer注册一个SchemaDirectiveWiring。Boot Starter会检测此类 Bean,因此您可能会有类似如下配置:
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
| 对于指令支持的示例,请参阅 GraphQL Java 扩展验证库。 |
3.2. 响应式DataFetcher
The default GraphQlSource builder 启用支持从DataFetcher返回Mono或Flux,这些会被适配为一个CompletableFuture,其中Flux值被聚合并转换成一个 List,除非请求是 GraphQL 订阅请求,在这种情况下,返回值保持为用于流式处理 GraphQL 响应的 Reactive Streams Publisher。
一个响应式的DataFetcher可以依赖于从传输层传播而来的Reactor上下文,例如来自WebFlux请求处理,请参阅
WebFlux 上下文。
3.3. 上下文传播
Spring for GraphQL 提供支持,可以在从 HTTP 透明传递上下文通过 GraphQL Java,并传入 DataFetcher 和其他它调用的组件。这包括来自 Spring MVC 请求处理线程的 ThreadLocal 上下文以及来自 WebFlux 处理流水线的 Reactor Context。
3.3.1. WebMvc
由 GraphQL Java 调用的 DataFetcher 和其他组件可能不会始终在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步
WebGraphQlInterceptor 或 DataFetcher 切换到不同的线程。
Spring for GraphQL 支持从 Servlet 容器线程向由 GraphQL Java 调用并执行的DataFetcher和其他组件传递ThreadLocal值。为此,应用程序需要为感兴趣的ThreadLocal值实现io.micrometer.context.ThreadLocalAccessor:
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机制自动进行注册。
3.3.2. WebFlux
一个 响应式 DataFetcher 可以依赖源自 WebFlux 请求处理链的 Reactor 上下文。这包括由 WebGraphQlInterceptor 组件添加的 Reactor 上下文。
3.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 级别记录。
3.4.1. 请求异常
The GraphQL Java引擎在解析请求时可能会遇到验证或其他错误,这会导致请求执行被阻止。在这种情况下,响应包含一个"data"键以及一个或多个全局的"errors"(即没有字段路径),这些错误是请求级别的。
DataFetcherExceptionResolver 无法处理此类全局错误,因为这些错误在开始执行之前以及在调用任何 DataFetcher 之前就已抛出。应用程序可以使用传输级拦截器来检查和转换 ExecutionResult 中的错误。
请参阅 WebGraphQlInterceptor 下的示例。
3.4.2. 订阅异常
订阅请求中的Publisher可能以错误信号完成,在这种情况下,底层传输(例如WebSocket)会发送一个最终的"error"类型消息,并附带GraphQL错误列表。
DataFetcherExceptionResolver 无法解决来自订阅 Publisher 的错误,
因为数据 DataFetcher 只是在初始化时创建了 Publisher。之后,传输会订阅可能随后因错误而完成的 Publisher。
一个应用程序可以注册一个SubscriptionExceptionResolver,以便解析来自订阅Publisher的异常,并将这些异常转换为GraphQL错误发送给客户端。
3.5. 批量加载
给定一个Book和其Author,我们可以为一本书创建一个DataFetcher,同时为其作者创建另一个DataFetcher。这使得可以选择带有或不带作者的书籍,但这也意味着书籍和作者不会一起加载,在查询多本书籍时,每个书籍的作者需要单独加载,这称为N+1次选择问题。
3.5.1. DataLoader
GraphQL Java 提供了一个 DataLoader 机制用于批量加载相关实体。
你可以在GraphQL Java 文档中找到全部详情。下面是一个
该机制工作原理的简要总结:
-
注册
DataLoader在可以加载实体的DataLoaderRegistry中,给定唯一键。 -
DataFetcher's 可以访问DataLoader's 并使用它们通过 ID 加载实体。 -
一个
DataLoader通过返回一个未来来推迟加载,因此可以在批处理中进行。 -
DataLoader's维护一个在每次请求中加载实体的缓存,这可以进一步提高效率。
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 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 ...
}
}
The Boot Starter 声明了一个 BatchLoaderRegistry bean,您可以将其注入到您的配置中,如上所示,或者注入任何组件(例如控制器)以注册批量加载功能。反过来,BatchLoaderRegistry 被注入到 DefaultExecutionGraphQlService 中,在那里它确保每次请求有 DataLoader 的注册。
默认情况下,DataLoader名称基于目标实体的类名。
这使得一个@SchemaMapping方法能够声明一个带有泛型类型的
DataLoader参数,
无需指定名称。然而,如果需要的话,可以通过
BatchLoaderRegistry构建器自定义名称,并且还可以自定义其他DataLoaderOptions。
要将默认DataLoaderOptions全局配置为任何注册的起点,您可以重写 Boot 的 BatchLoaderRegistry 模块,并使用 DefaultBatchLoaderRegistry 接受 Supplier<DataLoaderOptions> 的构造函数。
对于许多情况,在加载相关实体时,你可以使用
@BatchMapping 控制器方法,这相当于并取代了直接使用 BatchLoaderRegistry 和 DataLoader 的需要。
BatchLoaderRegistry 还提供了其他重要的好处。它支持从批量加载函数和 @BatchMapping 方法访问相同的 GraphQLContext,并且确保 Context Propagation 到这些地方。这也是为什么应用程序被期望使用它的原因。可以直接进行自己的DataLoader 注册,但这样的注册将放弃上述所有好处。
3.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("...");
// ...