# Spring 用于 GraphQL 文档
## 1. 概述
Spring for GraphQL 为 Spring 构建在[GraphQL Java](https://www.graphql-java.com/)上的应用程序提供支持。这是两个团队的联合合作。我们的共同理念是少些固执己见,更多地关注全面和广泛的支持。
Spring 对于 GraphQL 来说,它是 GraphQL Java 团队的[GraphQL Java Spring](https://github.com/graphql-java/graphql-java-spring)项目的继承者。它旨在成为所有 Spring、GraphQL 应用程序的基础。
目前,该项目正处于迈向 1.0 版本的里程碑阶段,并正在寻找反馈。请使用我们的[问题追踪器](https://github.com/spring-projects/spring-graphql/issues)报告问题,讨论设计问题,或请求一个功能。
要开始,请检查[start.spring.io](https://start.spring.io)上的 Spring GraphQL 启动器和[Samples](#samples)部分。
## 2. 所需经费
Spring 对于 GraphQL,需要以下作为基线:
* JDK8
* Spring 框架 5.3
* GraphQL Java17
* Spring 用于 QueryDSL 或通过示例查询的数据 2021.1.0 或更高版本
## 3. 网络传输
Spring for GraphQL 支持 HTTP 和 Over WebSocket 上的 GraphQL 请求。
### 3.1. HTTP
`GraphQlHttpHandler`通过 HTTP 请求处理 GraphQL,并将其委托给[网络拦截](#web-interception)链以执行请求。有两种变体,一种适用于 Spring MVC,另一种适用于 Spring WebFlux。两者都异步处理请求,并具有等效的功能,但在编写 HTTP 响应时分别依赖于阻塞和非阻塞 I/O。
请求必须使用 HTTP POST,而 GraphQL 请求详细信息作为 JSON 包含在请求主体中,如提议的[HTTP 上的 GraphQL](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md)规范中所定义的那样。一旦成功解码了 JSON 主体,HTTP 响应状态总是 200(OK),来自 GraphQL 请求执行的任何错误都会出现在 GraphQL 响应的“错误”部分。
通过声明`RouterFunction` Bean 并使用 Spring MVC 或 WebFlux 中的`RouterFunctions`来创建路由,`GraphQlHttpHandler`可以作为 HTTP 端点公开。引导启动器执行此操作,请参阅[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节以获取详细信息,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。
Spring for GraphQL 存储库包含一个 Spring MVC应用程序。
### 3.2. WebSocket
`GraphQlWebSocketHandler`基于[graphql-ws](https://github.com/enisdenjo/graphql-ws)库中定义的[protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)处理 WebSocket 请求上的 GraphQL。在 WebSocket 以上使用 GraphQL 的主要原因是订阅允许发送 GraphQL 响应流,但它也可以用于具有单个响应的常规查询。处理程序将每个请求委托给[网络拦截](#web-interception)链,以进一步执行请求。
| |GraphQL over WebSocket 协议
有两个这样的协议,一个在[订阅-transport-ws](https://github.com/apollographql/subscriptions-transport-ws)库中,另一个在[graphql-ws](https://github.com/enisdenjo/graphql-ws)库中。前者不是活动的,而
是由后者继承的。阅读此[blog post](https://the-guild.dev/blog/graphql-over-websockets)查看历史。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
`GraphQlWebSocketHandler`有两种变体,一种用于 Spring MVC,另一种用于 Spring WebFlux。两者都异步处理请求,并具有等效的功能。WebFlux 处理程序还使用非阻塞 I/O 和反压来传输消息流,这很好地工作,因为在 GraphQL Java 中,订阅响应是一个反应流`Publisher`。
`graphql-ws`项目列出了许多用于客户机的[recipes](https://github.com/enisdenjo/graphql-ws#recipes)。
通过声明`SimpleUrlHandlerMapping` Bean 并使用它将处理程序映射到 URL 路径,可以将`GraphQlWebSocketHandler`公开为 WebSocket 端点。引导启动器有启用此功能的选项,有关详细信息,请参见[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)部分,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。
Spring for GraphQL 存储库包含一个 WebFlux[WebSocket sample](https://github.com/spring-projects/spring-graphql/tree/main/samples/webflux-websocket)应用程序。
### 3.3. 网络拦截
[HTTP](#web-http)和[WebSocket](#web-websocket)传输处理程序委托给公共 Web 拦截链以执行请求。该链由`WebInterceptor`组件序列组成,然后是调用 GraphQL Java 引擎的`GraphQlService`组件序列。
`WebInterceptor`是在 Spring MVC 和 WebFlux 应用程序中使用的通用契约。使用它来拦截请求、检查 HTTP 请求头或注册`graphql.ExecutionInput`的转换:
```
class MyInterceptor implements WebInterceptor {
@Override
public Mono intercept(WebInput webInput, WebInterceptorChain chain) {
webInput.configureExecutionInput((executionInput, builder) -> {
Map map = ... ;
return builder.extensions(map).build();
});
return chain.next(webInput);
}
}
```
也可以使用`WebInterceptor`来拦截响应,添加 HTTP 响应头,或转换`graphql.ExecutionResult`:
```
class MyInterceptor implements WebInterceptor {
@Override
public Mono intercept(WebInput webInput, WebInterceptorChain chain) {
return chain.next(webInput)
.map(webOutput -> {
Object data = webOutput.getData();
Object updatedData = ... ;
return webOutput.transform(builder -> builder.data(updatedData));
});
}
}
```
`WebGraphQlHandler`提供了一个构建器来初始化 Web 拦截链。在构建链后,可以使用生成的`WebGraphQlHandler`初始化 HTTP 或 WebSocket 传输处理程序。引导启动器配置所有这些,请参阅[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节以获取详细信息,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。
## 4. 请求执行
`GraphQlService`是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,例如[网络传输](#web-transports),委托给`GraphQlService`来处理请求。
主要的实现`ExecutionGraphQlService`是围绕`graphql.GraphQL`调用的一个很薄的外观。它配置了`GraphQlSource`以访问`graphql.GraphQL`实例。
### 4.1. `GraphQLSource`
`GraphQlSource`是用于访问用于执行请求的`graphql.GraphQL`实例的核心 Spring 抽象。它提供了一个 Builder API 来初始化 GraphQL Java 并构建`GraphQlSource`。
默认的`GraphQlSource`Builder 可通过`GraphQlSource.builder()`访问,支持[active`DataFetcher`](#execution-active-datafetcher)、[上下文传播](#execution-context)和[异常解决](#execution-exceptions)。
Spring boot[starter](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql)通过默认的`GraphQlSource.Builder`初始化`GraphQlSource`实例,并启用以下功能:
* 从可配置位置加载[模式文件](#execution-graphqlsource-schema-resources)。
* 公开适用于`GraphQlSource.Builder`的[properties](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/application-properties.html#appendix.application-properties.web)。
* 检测[`RuntimeWiringConfigurer`]bean。
* 检测[仪器仪表](https://www.graphql-java.com/documentation/instrumentation)bean 的[GraphQL 度量](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/actuator.html#actuator.metrics.supported.spring-graphql)。
* 检测`DataFetcherExceptionResolver`bean 的[异常解决](#execution-exceptions)。
* 检测`GraphQlSourceBuilderCustomizer`bean 的任何其他自定义。
#### 4.1.1. 模式资源
`GraphQlSource.Builder`可以配置一个或多个`Resource`实例来进行解析和合并。这意味着模式文件可以从几乎任何位置加载。
默认情况下, Spring 引导启动器[查找架构文件](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.schema)来自一个众所周知的 Classpath 位置,但是你可以通过`FileSystemResource`将其更改为文件系统上的一个位置,通过`ByteArrayResource`将字节内容更改为通过`ByteArrayResource`,或者实现一个自定义的`Resource`从远程位置或存储空间加载模式文件。
#### 4.1.2. 模式创建
默认情况下,`GraphQlSource.Builder`使用 GraphQLJava来创建。这适用于大多数应用程序,但如果有必要,你可以通过构建器连接到模式创建:
```
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
// create GraphQLSchema
})
```
这样做的主要原因是通过联合库创建模式。
#### 4.1.3. `RuntimeWiringConfigurer`
你可以使用`RuntimeWiringConfigurer`注册:
* 自定义标量类型。
* 指令处理代码。
* `TypeResolver`,如果需要覆盖类型的[default`TypeResolver`](#execution-grapqlsource-default-type-resolver)。
* 对于字段`DataFetcher`,尽管大多数应用程序只会简单地配置`AnnotatedControllerConfigurer`,其中检测带注释的`DataFetcher`处理程序方法。 Spring 引导启动器默认添加`AnnotatedControllerConfigurer`。
Spring 引导启动器检测类型`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`。这允许你添加任意数量的工厂,然后按顺序调用这些工厂。
#### 4.1.4. 默认`TypeResolver`
`GraphQlSource.Builder`将`ClassNameTypeResolver`注册为默认的`TypeResolver`,用于尚未通过[`RuntimeWiringConfigurer`]进行注册的 GraphQL 接口和联合。在 GraphQL Java 中,`TypeResolver`的目的是确定从`DataFetcher`返回的值的 GraphQL 对象类型,用于 GraphQL 接口或 Union 字段。
`ClassNameTypeResolver`尝试将该值的简单类名与 GraphQL 对象类型匹配,如果不成功,它还会导航其超级类型,包括基类和接口,以寻找匹配。`ClassNameTypeResolver`提供了一个选项,可以将名称提取函数与`Class`一起配置为 GraphQL 对象类型名称映射,这应该有助于覆盖更多的角情况。
#### 4.1.5. 操作缓存
GraphQL Java 在执行操作之前必须*解析*和*验证*。这可能会对业绩产生重大影响。为了避免重新解析和验证的需要,应用程序可以配置一个`PreparsedDocumentProvider`来缓存和重用文档实例。[GraphQL Java DOCS](https://www.graphql-java.com/documentation/execution/#query-caching)通过`PreparsedDocumentProvider`提供有关查询缓存的更多详细信息。
在 Spring GraphQL 中,你可以通过`GraphQlSource.Builder#configureGraphQl`注册一个`PreparsedDocumentProvider`:。
```
// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...
// Create provider
PreparsedDocumentProvider provider = ...
builder.schemaResources(..)
.configureRuntimeWiring(..)
.configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))
```
#### 4.1.6. 指令
GraphQL 语言支持“描述 GraphQL 文档中的替代运行时执行和类型验证行为”的指令。指令类似于 Java 中的注释,但在 GraphQL 文档中对类型、字段、片段和操作进行了声明。
GraphQL Java 提供`SchemaDirectiveWiring`契约来帮助应用程序检测和处理指令。有关更多详细信息,请参见 GraphQL Java 文档中的[模式指令](https://www.graphql-java.com/documentation/sdl-directives/)。
在 Spring GraphQL 中,可以通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)注册`SchemaDirectiveWiring`。 Spring 引导启动器会检测到这样的 bean,因此你可能会有如下内容:
```
@Configuration
public class GraphQlConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
}
}
```
| |对于指令支持的示例,请查看[GraphQL Java 的扩展验证](https://github.com/graphql-java/graphql-java-extended-validation)库。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------|
### 4.2. Reactive `DataFetcher`
默认的`GraphQlSource`Builder 使对`DataFetcher`的支持返回`Mono`或`Flux`,这将这些支持调整为`CompletableFuture`,其中`Flux`值被聚合并转换为一个列表,除非该请求是 GraphQL 订阅请求,在这种情况下,返回值仍然是用于流式 GraphQL 响应的反应流`Publisher`。
反应性`DataFetcher`可以依赖于对从传输层传播的反应器上下文的访问,例如来自 WebFlux 请求的处理,请参见[WebFlux 上下文](#execution-context-webflux)。
### 4.3. 执行上下文
Spring 对于 GraphQL 提供了支持,以通过 GraphQL 引擎从[网络传输](#web-transports)透明地传播上下文,并支持`DataFetcher`和它调用的其他组件。这既包括来自 Spring MVC 请求处理线程的`ThreadLocal`上下文,也包括来自 WebFlux 处理管道的反应器`Context`上下文。
#### 4.3.1. WebMVC
GraphQL Java 调用的`DataFetcher`和其他组件可能并不总是在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步[`WebInterceptor`]或`DataFetcher`切换到不同的线程。
Spring 对于 GraphQL 支持将`ThreadLocal`值从 Servlet 容器线程传播到`DataFetcher`线程和由 GraphQL 引擎调用的其他组件上执行。要做到这一点,应用程序需要创建`ThreadLocalAccessor`以提取感兴趣的`ThreadLocal`值:
```
public class RequestAttributesAccessor implements ThreadLocalAccessor {
private static final String KEY = RequestAttributesAccessor.class.getName();
@Override
public void extractValues(Map container) {
container.put(KEY, RequestContextHolder.getRequestAttributes());
}
@Override
public void restoreValues(Map values) {
if (values.containsKey(KEY)) {
RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
}
}
@Override
public void resetValues(Map values) {
RequestContextHolder.resetRequestAttributes();
}
}
```
可以在[Webgraphandler](#web-interception)构建器中注册`ThreadLocalAccessor`。引导启动器检测这种类型的 bean 并自动为 Spring MVC 应用程序注册它们,请参见[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节。
#### 4.3.2. WebFlux
[acceptive`DataFetcher`](#execution-acceptive-datafetcher)可以依赖于对源自 WebFlux 请求处理链的反应器上下文的访问。这包括由[网络拦截器](#web-interception)组件添加的反应堆上下文。
### 4.4. 异常解决
GraphQL Java 应用程序可以注册`DataFetcherExceptionHandler`,以决定如何在 GraphQL 响应的“错误”部分中表示来自数据层的异常。
Spring 对于 GraphQL,有一个内置的`DataFetcherExceptionHandler`,它被配置为由[`GraphQLSource`](#execution-grapqlsource)生成器使用。它使应用程序能够注册一个或多个 Spring `DataFetcherExceptionResolver`按顺序调用的组件,直到将`Exception`解析为`graphql.GraphQLError`对象的列表。
`DataFetcherExceptionResolver`是一种异步契约。对于大多数实现方式,扩展`DataFetcherExceptionResolverAdapter`并覆盖其同步解决异常的`resolveToSingleError`或`resolveToMultipleErrors`方法之一就足够了。
a`GraphQLError`可以被分配一个`graphql.ErrorClassification`。 Spring 对于 GraphQL 定义了一个`ErrorType`枚举,具有常见的、错误的分类类别:
* `BAD_REQUEST`
* `UNAUTHORIZED`
* `FORBIDDEN`
* `NOT_FOUND`
* `INTERNAL_ERROR`
应用程序可以使用它来对错误进行分类。如果错误仍未解决,则默认情况下将其标记为`INTERNAL_ERROR`。
### 4.5. 批量装载
给定一个`Book`和它的`Author`,我们可以为一本书创建一个`DataFetcher`,为它的作者创建另一个。这允许在有或没有作者的情况下选择书籍,但这意味着书籍和作者 AREN 不能一起加载,这在查询多本书时效率特别低,因为每本书的作者都是单独加载的。这就是所谓的 N+1 选择问题。
#### 4.5.1. `DataLoader`
GraphQL Java 提供了一种`DataLoader`机制,用于批量加载相关实体。你可以在[GraphQL Java DOCS](https://www.graphql-java.com/documentation/batching/)中找到完整的详细信息。以下是其工作原理的摘要:
1. 在`DataLoaderRegistry`中注册`DataLoader`的可以加载实体的项,给定唯一的键。
2. `DataFetcher`的可以访问`DataLoader`的,并使用它们通过 ID 加载实体。
3. a`DataLoader`通过返回 future 来延迟加载,因此可以在批处理中完成。
4. `DataLoader`的每请求保持一个加载实体的缓存,这可以进一步提高效率。
#### 4.5.2. `BatchLoaderRegistry`
GraphQL Java 中完整的批处理加载机制需要实现几个`BatchLoader`接口中的一个,然后用`DataLoader`中的名称将这些接口包装并注册为`DataLoader`s。
Spring GraphQL 中的 API 略有不同。对于注册,只有一个 central`BatchLoaderRegistry`公开工厂方法和构建器,以创建和注册任意数量的批处理加载函数:
```
@Configuration
public class MyConfig {
public MyConfig(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Mono