# 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 }); // more registrations ... } } ``` Spring 引导启动器声明一个`BatchLoaderRegistry` Bean,你可以将其注入到你的配置中,如上面所示,或注入到任何组件中,例如控制器中的顺序寄存器批处理加载功能。然后,将`BatchLoaderRegistry`注入`ExecutionGraphQlService`,从而确保每个请求的注册量`DataLoader`。 默认情况下,`DataLoader`名称是基于目标实体的类名。这允许`@SchemaMapping`方法使用泛型类型声明[Dataloader 参数](#controllers-schema-mapping-data-loader),而不需要指定名称。但是,如果有必要,可以通过`BatchLoaderRegistry`Builder 自定义名称,以及其他`DataLoader`选项。 对于许多情况,在加载相关实体时,可以使用[@batchmapping](#controllers-batch-mapping)控制器方法,这对于需要使用`BatchLoaderRegistry`和`DataLoader`直接替换的快捷方式也提供了重要的好处。S`BatchLoaderRegistry`也提供了其他好处。它支持从批装载函数和从`@BatchMapping`方法访问相同的`GraphQLContext`,并确保[上下文传播](#execution-context)到它们。这就是为什么应用程序需要使用它。可以直接执行你自己的`DataLoader`注册,但这种注册将放弃上述好处。 #### 4.5.3. 测试批装载 首先让`BatchLoaderRegistry`在`DataLoaderRegistry`上执行注册: ``` BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry(); // perform registrations... DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build(); batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext); ``` 现在你可以访问和测试个别`DataLoader`的如下所示: ``` DataLoader loader = dataLoaderRegistry.getDataLoader(Book.class.getName()); loader.load(1L); loader.loadMany(Arrays.asList(2L, 3L)); List books = loader.dispatchAndJoin(); // actual loading assertThat(books).hasSize(3); assertThat(books.get(0).getName()).isEqualTo("..."); // ... ``` ## 5. 数据整合 Spring 对于 GraphQL,你可以利用现有的 Spring 技术,遵循常见的编程模型来通过 GraphQL 公开底层数据源。 本节讨论了用于 Spring 数据的集成层,该集成层提供了一种简单的方法,将 QueryDSL 或查询 by example 存储库调整为`DataFetcher`,包括用于标记为`@GraphQlRepository`的存储库的自动检测和 GraphQL 查询注册的选项。 ### 5.1. QueryDSL Spring 对于 GraphQL 支持使用[Querydsl](http://www.querydsl.com/)来通过 Spring 数据获取数据[QueryDSL 扩展](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions)。QueryDSL 提供了一种灵活的 TypeSafe 方法,通过使用注释处理器生成元模型来表示查询谓词。 例如,将存储库声明为`QuerydslPredicateExecutor`: ``` public interface AccountRepository extends Repository, QuerydslPredicateExecutor { } ``` 然后使用它创建`DataFetcher`: ``` // For single result queries DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single(); // For multi-result queries DataFetcher> dataFetcher = QuerydslDataFetcher.builder(repository).many(); ``` 你现在可以通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)注册上面的`DataFetcher`。 `DataFetcher`从 GraphQL 请求参数构建一个 QueryDSL`Predicate`,并使用它来获取数据。 Spring 对于 JPA、MongoDB 和 LDAP,数据支持`QuerydslPredicateExecutor`。 如果存储库是`ReactiveQuerydslPredicateExecutor`,则构建器返回`DataFetcher>`或`DataFetcher>`。 Spring 数据支持 MongoDB 的这种变体。 #### 5.1.1. 构建设置 要在构建中配置 QueryDSL,请遵循[正式参考文件](https://querydsl.com/static/querydsl/latest/reference/html/ch02.html): 例如: Gradle ``` dependencies { //... annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa", 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final', 'javax.annotation:javax.annotation-api:1.3.2' } compileJava { options.annotationProcessorPath = configurations.annotationProcessor } ``` Maven ``` com.querydsl querydsl-apt ${querydsl.version} jpa provided org.hibernate.javax.persistence hibernate-jpa-2.1-api 1.0.2.Final javax.annotation javax.annotation-api 1.3.2 com.mysema.maven apt-maven-plugin ${apt-maven-plugin.version} process target/generated-sources/java com.querydsl.apt.jpa.JPAAnnotationProcessor ``` [WebMVC-HTTP](https://github.com/spring-projects/spring-graphql/tree/main/samples/webmvc-http)样本使用 queryDSL 表示`artifactRepositories`。 #### 5.1.2. 定制 `QuerydslDataFetcher`支持自定义如何将 GraphQL 参数绑定到属性上以创建 QueryDSL`Predicate`。默认情况下,对于每个可用的属性,参数被绑定为“is equaled to”。要对此进行定制,可以使用`QuerydslDataFetcher`Builder 方法来提供`QuerydslBinderCustomizer`。 存储库本身可能是`QuerydslBinderCustomizer`的实例。这是在[自动注册](#data-querydsl-registration)期间自动检测和透明地应用的。然而,当手动构建`QuerydslDataFetcher`时,你将需要使用 Builder 方法来应用它。 `QuerydslDataFetcher`支持接口和 DTO 投影来转换查询结果,然后返回这些结果进行进一步的 GraphQL 处理。 | |要了解投影是什么,请参阅[Spring Data docs](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)。
要了解如何在 GraphQL 中使用投影,请参见[选择集与投影](#data-projections)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 要在 QueryDSL 存储库中使用 Spring 数据投影,请创建一个投影接口或一个目标 DTO 类,并通过`projectAs`方法对其进行配置,以获得产生目标类型的`DataFetcher`: ``` class Account { String name, identifier, description; Person owner; } interface AccountProjection { String getName(); String getIdentifier(); } // For single result queries DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); // For multi-result queries DataFetcher> dataFetcher = QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); ``` #### 5.1.3. 自动注册 如果存储库使用`@GraphQlRepository`进行注释,那么对于尚未注册`DataFetcher`且其返回类型与存储库域类型匹配的查询,将自动对其进行注册。这包括单值查询和多值查询。 默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标 GraphQL 类型名。 自动注册检测给定存储库是否实现`QuerydslBinderCustomizer`,并通过`QuerydslDataFetcher`Builder 方法透明地应用该方法。 自动注册是通过可从`QuerydslDataFetcher`获得的内置`RuntimeWiringConfigurer`执行的。[引导启动器](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.data-query)自动检测`@GraphQlRepository`bean,并使用它们初始化`RuntimeWiringConfigurer`with。 自动注册不支持[定制](#data-querybyexample-customizations)。如果需要,你需要使用`QueryByExampleDataFetcher`通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)手动构建和注册`DataFetcher`。 ### 5.2. 示例查询 Spring 数据支持使用[示例查询](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example)来获取数据。Query by Example 是一种简单的查询技术,不需要你通过特定于存储的查询语言编写查询。 从声明`QueryByExampleExecutor`的存储库开始: ``` public interface AccountRepository extends Repository, QueryByExampleExecutor { } ``` 使用`QueryByExampleDataFetcher`将存储库转换为`DataFecher`: ``` // For single result queries DataFetcher dataFetcher = QueryByExampleDataFetcher.builder(repository).single(); // For multi-result queries DataFetcher> dataFetcher = QueryByExampleDataFetcher.builder(repository).many(); ``` 你现在可以通过[`RuntimeWiringConfigurer`]注册上面的`DataFetcher`(#execution-graphqlsource-runtimewilling-configurer)。 `DataFetcher`使用 GraphQL 参数映射来创建存储库的域类型,并将其作为示例对象来获取数据。 Spring 对于 JPA、MongoDB、NEO4J 和 Redis,数据支持`QueryByExampleDataFetcher`。 如果存储库是`ReactiveQueryByExampleExecutor`,则构建器返回`DataFetcher>`或`DataFetcher>`。 Spring 数据支持 MongoDB、NEO4J、Redis 和 R2DBC 的这种变体。 #### 5.2.1. 构建设置 Spring 对于支持它的数据存储,示例查询已经包括在 Spring 数据模块中,因此不需要额外的设置来启用它。 #### 5.2.2. 定制 `QueryByExampleDataFetcher`支持接口和 DTO 投影来转换查询结果,然后返回这些结果进行进一步的 GraphQL 处理。 | |要了解投影是什么,请参阅[Spring Data documentation](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)。
要了解投影在 GraphQL 中的作用,请参见[选择集与投影](#data-projections)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring 要通过示例存储库查询使用数据投影,可以创建投影接口或目标 DTO 类,并通过`projectAs`方法对其进行配置,以获得产生目标类型的`DataFetcher`: ``` class Account { String name, identifier, description; Person owner; } interface AccountProjection { String getName(); String getIdentifier(); } // For single result queries DataFetcher dataFetcher = QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); // For multi-result queries DataFetcher> dataFetcher = QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); ``` #### 5.2.3. 自动注册 如果存储库使用`@GraphQlRepository`进行注释,那么对于尚未注册`DataFetcher`且其返回类型与存储库域类型匹配的查询,将自动对其进行注册。这包括单值查询和多值查询。 默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标 GraphQL 类型名。 自动注册是通过可从`QueryByExampleDataFetcher`获得的内置`RuntimeWiringConfigurer`执行的。[引导启动器](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.data-query)自动检测`@GraphQlRepository`bean,并使用它们初始化`RuntimeWiringConfigurer`with。 自动注册不支持[定制](#data-querybyexample-customizations)。如果需要,你需要使用`QueryByExampleDataFetcher`通过[`RuntimeWiringConfigurer`]手动构建和注册`DataFetcher`(#execution-grapqlsource-runtimewilling-configurer)。 ### 5.3. 选择集与投影 出现的一个常见问题是,GraphQL 选择集与[Spring Data projections](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)相比如何,每个选择集起什么作用? 简短的回答是, Spring for GraphQL 不是将 GraphQL 查询直接转换为 SQL 或 JSON 查询的数据网关。相反,它允许你利用现有的 Spring 技术,并且不假定 GraphQL 模式和底层数据模型之间存在一对一的映射。这就是为什么客户机驱动的选择和数据模型的服务器端转换可以发挥互补作用的原因。 为了更好地理解,考虑 Spring 数据促进了域驱动(DDD)设计,作为管理数据层中复杂性的推荐方法。在 DDD 中,坚持总量约束是很重要的。根据定义,聚合只有在全部加载的情况下才是有效的,因为部分加载的聚合可能会对聚合功能施加限制。 在 Spring 数据中,你可以选择是希望将聚合按原样公开,还是在将其作为 GraphQL 结果返回之前将转换应用于数据模型。有时,做前者就足够了,默认情况下,[Querydsl](#data-querydsl)和[示例查询](#data-querybyexample)集成将 GraphQL 选择集转换为属性路径提示,底层 Spring 数据模块使用这些属性路径提示来限制选择。 在其他情况下,为了适应 GraphQL 模式,减少甚至转换底层数据模型是有用的。 Spring 数据通过接口和 DTO 投影来支持这一点。 接口投影定义了一组固定的属性,根据数据存储查询结果,在其中属性可以`null`,也可以不是`null`。有两种类型的接口预测,它们都决定从底层数据源加载哪些属性: * 如果不能部分实现聚合对象,但仍然希望公开属性的子集,则[闭合界面投影](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed)是有帮助的。 * [开放接口投影](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open)利用 Spring 的`@Value`注释和[SpEL](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions)表达式来应用轻量级数据转换,例如连接、计算或将静态函数应用于属性。 DTO 投影提供了更高级别的定制,因为你可以将转换代码放置在构造函数或 getter 方法中。 DTO 投影是通过查询实现的,查询中的各个属性是由投影本身确定的。DTO 投影通常用于 Full-Args 构造函数(例如 Java 记录),因此只有当所有必需的字段(或列)都是数据库查询结果的一部分时,才能构造它们。 ## 6. 带注释的控制器 Spring 对于 GraphQL 提供了一种基于注释的编程模型,其中`@Controller`组件使用注释来声明具有灵活方法签名的处理程序方法,以获取特定 GraphQL 字段的数据。例如: ``` @Controller public class GreetingController { @QueryMapping (1) public String hello() { (2) return "Hello, world!"; } } ``` |**1**|将此方法绑定到查询,即查询类型下的字段。| |-----|---------------------------------------------------------------------------| |**2**|如果未在注释上声明,则从方法名确定查询。| Spring 对于 GraphQL 使用`RuntimeWiring.Builder`将上述处理程序方法注册为用于名为“Hello”的查询的`graphql.schema.DataFetcher`。 ### 6.1. 声明 你可以将`@Controller`bean 定义为标准 Spring Bean 定义。该`@Controller`原型允许自动检测,与 Spring 对齐,用于在 Classpath 上检测`@Controller`和`@Component`类并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 GraphQL 应用程序中的数据获取组件的角色。 `AnnotatedControllerConfigurer`通过`RuntimeWiring.Builder`检测`@Controller`bean 并将其注释的处理程序方法注册为`DataFetcher`s。它是`RuntimeWiringConfigurer`的一个实现,它可以被添加到`GraphQlSource.Builder`中。 Spring 引导启动器会自动将`AnnotatedControllerConfigurer`声明为 Bean,并将所有`RuntimeWiringConfigurer`bean 添加到`GraphQlSource.Builder`中,从而支持带注释的`DataFetcher`s,请参见引导启动器文档中的[GraphQL RuntimeWiring](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.runtimewiring)部分。 ### 6.2. `@SchemaMapping` `@SchemaMapping`注释将处理程序方法映射到 GraphQL 模式中的字段,并声明该字段为该字段的`DataFetcher`。注释可以指定父类型名和字段名: ``` @Controller public class BookController { @SchemaMapping(typeName="Book", field="author") public Author getAuthor(Book book) { // ... } } ``` `@SchemaMapping`注释也可以省略这些属性,在这种情况下,字段名称默认为方法名称,而类型名称默认为注入到方法中的源/父对象的简单类名。例如,以下默认输入“book”和字段“author”: ``` @Controller public class BookController { @SchemaMapping public Author author(Book book) { // ... } } ``` 可以在类级别声明`@SchemaMapping`注释,以指定类中所有处理程序方法的默认类型名。 ``` @Controller @SchemaMapping(typeName="Book") public class BookController { // @SchemaMapping methods for fields of the "Book" type } ``` `@QueryMapping`、`@MutationMapping`和`@SubscriptionMapping`是元注释,它们本身用`@SchemaMapping`进行注释,并且将类型名称预设为`Query`、`Mutation`或`Subscription`。实际上,这些是分别针对查询、突变和订阅类型下的字段的快捷方式注释。例如: ``` @Controller public class BookController { @QueryMapping public Book bookById(@Argument Long id) { // ... } @MutationMapping public Book addBook(@Argument BookInput bookInput) { // ... } @SubscriptionMapping public Flux newPublications() { // ... } } ``` `@SchemaMapping`处理程序方法具有灵活的签名,可以从一系列方法参数和返回值中进行选择。 #### 6.2.1. 方法签名 模式映射处理程序方法可以具有以下任何一个方法参数: | Method Argument |说明| |-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| | `@Argument` |要访问将命名字段参数转换为更高级别的类型化对象。
请参见[`@Argument`](#controllers-schema-mapping-convention)。| | `@Arguments` |有关对转换为更高级别的类型化对象的所有字段参数的访问。
参见[`@Arguments`](#controllers-schema-mapping-arguments)。| | `@ProjectedPayload` Interface |通过项目接口访问字段参数。
参见[`@ProjectPayload`接口](#controllers-schema-mapping-projectedpayload-pargument)。| | Source |关于字段的源(即父/容器)实例的访问。
参见[Source](#controllers-schema-mapping-source)。| | `DataLoader` |要访问`DataLoader`中的`DataLoader`。
请参见[`DataLoader`](#controllers-schema-mapping-data-loader)。| | `@ContextValue` |对于从 localContext 访问一个值,如果它是`GraphQLContext`、
的实例,或者来自`GraphQLContext`的`DataFetchingEnvironment`的实例。| | `GraphQLContext` |用于从`DataFetchingEnvironment`访问上下文。| | `java.security.Principal` |从 Spring 安全上下文中获得的,如果可用的话。| | `@AuthenticationPrincipal` |用于从 Spring 安全上下文访问`Authentication#getPrincipal()`。| |`DataFetchingFieldSelectionSet`|用于通过`DataFetchingEnvironment`访问查询的选择集。| | `Locale`, `Optional` |从`DataFetchingEnvironment`访问`Locale`。| | `DataFetchingEnvironment` |直接访问底层`DataFetchingEnvironment`。| 模式映射处理程序方法可以返回任意值,包括反应器`Mono`和`Flux`中描述的[ractive`DataFetcher`](#execution-ractive-datafetcher)。 #### 6.2.2. `@Argument` 在 GraphQL Java 中,`DataFetchingEnvironment`提供对特定字段参数值的映射的访问。这些值可以是简单的标量值(例如 String,long)、用于更复杂输入的`Map`的值,或者是`List`的值。 使用`@Argument`注释将命名字段参数注入到处理程序方法中。方法参数可以是任何类型的更高级别的类型化对象。它是根据已命名字段参数的值创建和初始化的,或者将它们匹配到单个数据构造函数参数,或者使用默认构造函数,然后通过`org.springframework.validation.DataBinder`将键匹配到对象属性上: ``` @Controller public class BookController { @QueryMapping public Book bookById(@Argument Long id) { // ... } @MutationMapping public Book addBook(@Argument BookInput bookInput) { // ... } } ``` 默认情况下,如果方法参数名是可用的(需要`-parameters`带有 Java8+ 或来自编译器的调试信息的编译器标志),它将用于查找参数。如果需要,可以通过注释自定义名称,例如`@Argument("bookInput")`。 | |`@Argument`注释没有“required”标志,也没有
指定默认值的选项。这两个都可以在 GraphQL 模式级别指定,
由 GraphQL 引擎强制执行。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以在`Map`参数上使用`@Argument`,以获得所有参数的值。不能设置`@Argument`上的 name 属性。 #### 6.2.3. `@Arguments` 如果你想将完整的参数映射到单个目标对象上,请使用`@Arguments`注释,与`@Argument`相反,后者绑定一个特定的命名参数。 例如,`@Argument BookInput bookInput`使用参数“bookinput”的值初始化`BookInput`,而`@Arguments`使用完整的参数映射,在这种情况下,顶层参数绑定到`BookInput`属性。 #### `验证 如果在应用程序上下文中存在一个[Bean Validation](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation-beanvalidation-overview)`Validator`(或者通常,一个`LocalValidatorFactoryBean`) Bean,则`AnnotatedControllerConfigurer`将自动检测它并配置用于验证的支持。然后在方法调用之前验证用`@Valid`和`@Validated`注释的控制器参数。 Bean 验证允许你声明对类型的约束,如下例所示: ``` public class BookInput { @NotNull private String title; @NotNull @Size(max=13) private String isbn; } ``` 然后,我们可以用`@Valid`标记我们的验证参数: ``` @Controller public class BookController { @MutationMapping public Book addBook(@Argument @Valid BookInput bookInput) { // ... } } ``` 如果在验证过程中发生错误,将抛出一个`ConstraintViolationException`,并可以在以后[使用自定义`DataFetcherExceptionResolver`解决](#execution-exceptions)。 | |与 Spring MVC 不同,处理程序方法签名不支持注入`BindingResult`以对验证错误做出反应:这些将作为例外情况在全局范围内处理。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.2.5. `@ProjectPayload`接口 作为使用带有[`@Argument`](#controllers-schema-mapping-argument)的完整对象的一种替代方法,你还可以使用投影接口通过定义良好的最小接口访问 GraphQL 请求参数。当 Spring 数据在类路径上时,参数投影由[Spring Data’s Interface projections](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces)提供。 要利用这一点,请创建一个带有`@ProjectedPayload`注释的接口,并将其声明为控制器方法参数。如果参数被注释为`@Argument`,则它将应用于`DataFetchingEnvironment.getArguments()`映射中的单个参数。当声明时不带`@Argument`,投影工作于完整参数映射中的顶层参数。 例如: ``` @Controller public class BookController { @QueryMapping public Book bookById(BookIdProjection bookId) { // ... } @MutationMapping public Book addBook(@Argument BookInputProjection bookInput) { // ... } } @ProjectedPayload interface BookIdProjection { Long getId(); } @ProjectedPayload interface BookInputProjection { String getName(); @Value("#{target.author + ' ' + target.name}") String getAuthorAndName(); } ``` #### 6.2.6. 来源 在 GraphQL Java 中,`DataFetchingEnvironment`提供对字段的源(即父/容器)实例的访问。要访问它,只需声明一个预期目标类型的方法参数。 ``` @Controller public class BookController { @SchemaMapping public Author author(Book book) { // ... } } ``` 源方法参数还有助于确定映射的类型名。如果 Java 类的简单名称与 GraphQL 类型匹配,则不需要在`@SchemaMapping`注释中显式指定类型名称。 | |[`@BatchMapping`](#controllers-batch-mapping)处理程序方法可以为一个查询批装载所有作者,
给定源/父书对象的列表。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.2.7. `DataLoader` 当你注册一个实体的批处理加载函数时,如[批量装载](#execution-batching)中所解释的那样,你可以通过声明一个类型为`DataLoader`的方法参数来访问该实体的`DataLoader`,并使用它来加载该实体: ``` @Controller public class BookController { public BookController(BatchLoaderRegistry registry) { registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { // return Map }); } @SchemaMapping public CompletableFuture author(Book book, DataLoader loader) { return loader.load(book.getAuthorId()); } } ``` 默认情况下,`BatchLoaderRegistry`使用值类型的完整类名(例如,`Author`的类名)作为注册的键,因此只需声明带有泛型类型的`DataLoader`方法参数,就可以提供足够的信息来在`DataLoaderRegistry`中定位它。作为后备,`DataLoader`方法参数解析器也将尝试方法参数名称作为键,但通常不需要这样做。 请注意,对于许多加载相关实体的情况,其中`@SchemaMapping`只是将其委托给`DataLoader`,你可以使用[@batchmapping](#controllers-batch-mapping)方法来减少样板文件,如下一节所述。 ### 6.3. `@BatchMapping` [批量装载](#execution-batching)通过使用`org.dataloader.DataLoader`来延迟单个实体实例的加载,从而解决了 N+1SELECT 问题,从而可以将它们一起加载。例如: ``` @Controller public class BookController { public BookController(BatchLoaderRegistry registry) { registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { // return Map }); } @SchemaMapping public CompletableFuture author(Book book, DataLoader loader) { return loader.load(book.getAuthorId()); } } ``` 对于加载关联实体的简单情况(如上图所示),`@SchemaMapping`方法所做的不过是将其委托给`DataLoader`。这是用`@BatchMapping`方法可以避免的样板。例如: ``` @Controller public class BookController { @BatchMapping public Mono> author(List books) { // ... } } ``` 上面变成了`BatchLoaderRegistry`中的批处理加载函数,其中键是`Book`实例和加载的值它们的作者。此外,`DataFetcher`也透明地绑定到类型`author`的`author`字段,对于作者来说,它只是将其委托给`DataLoader`,给定其源/父`Book`实例。 | |要作为唯一键使用,`Book`必须实现`hashcode`和`equals`。| |---|--------------------------------------------------------------------------| 默认情况下,字段名称默认为方法名称,而类型名称默认为输入`List`元素类型的简单类名。两者都可以通过注释属性进行定制。类型名也可以从类级别`@SchemaMapping`继承。 #### 6.3.1. 方法签名 批处理映射方法支持以下参数: | Method Argument |说明| |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| | `List` |源/父对象。| |`java.security.Principal`|从 Spring 安全上下文获得(如果可用的话)。| | `@ContextValue` |要访问来自`GraphQLContext`的`BatchLoaderEnvironment`的值,请使用
,该上下文与来自`DataFetchingEnvironment`的上下文相同。| | `GraphQLContext` |要访问来自`BatchLoaderEnvironment`的上下文,请访问
,该上下文与来自`DataFetchingEnvironment`的上下文相同。| |`BatchLoaderEnvironment` |在 GraphQL Java 中可用的环境为`org.dataloader.BatchLoaderWithContext`。| 批处理映射方法可以返回: | Return Type |说明| |---------------------|--------------------------------------------------------------------------------------------------------------------------| | `Mono>` |以父对象为键,以批处理加载对象为值的映射。| | `Flux` |批装载对象的序列,其顺序必须与传递到方法中的源/父
对象的顺序相同。| |`Map`, `List`|命令式变体,例如,不需要进行远程调用。| ## 7. 安全 可以使用 HTTP URL 安全性来保护[Web](#web-transports)GraphQL 端点的路径,以确保只有经过身份验证的用户才能访问它。然而,这并不能区分在单个 URL 上的共享端点上的不同 GraphQL 请求。 要应用更细粒度的安全性,可以在获取 GraphQL 响应的特定部分所涉及的服务方法中添加 Spring 安全性注释,例如`@PreAuthorize`或`@Secured`。由于[上下文传播](#execution-context)的目的是使安全性和其他上下文在数据获取级别可用,所以这种方法应该有效。 Spring for GraphQL 存储库包含[Spring MVC](https://github.com/spring-projects/spring-graphql/tree/main/samples/webmvc-http-security)和[WebFlux](https://github.com/spring-projects/spring-graphql/tree/main/samples/webflux-security)的示例。 ## 8. 测试 用 Spring 的`WebTestClient`测试 GraphQL 请求是可能的,只需要发送和接收 JSON,但是许多 GraphQL 特定的细节使得这种方法比必要的更麻烦。 要获得完整的测试支持,你需要在构建中添加`spring-graphql-test`依赖项: Gradle ``` dependencies { // ... testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0-SNAPSHOT' } ``` Maven ``` org.springframework.graphql spring-graphql-test 1.0.0-SNAPSHOT test ``` ### 8.1. `GraphQlTester` `GraphQlTester`定义了一个工作流,用于测试 GraphQL 请求,该工作流具有以下优点: * 在响应中的“errors”键下验证没有意外错误。 * 在响应中的“数据”键下进行解码。 * 使用 JSONPath 来解码响应的不同部分。 * 测试订阅。 要创建`GraphQlTester`,只需要一个`GraphQlService`,并且不需要传输: ``` GraphQlSource graphQlSource = GraphQlSource.builder() .schemaResources(...) .runtimeWiringConfigurer(...) .build(); GraphQlService graphQlService = new ExecutionGraphQlService(graphQlSource); GraphQlTester graphQlTester = GraphQlTester.builder(graphQlService).build(); ``` ### 8.2. `WebGraphQlTester` `WebGraphQlTester`扩展`GraphQlTester`以添加特定于[网络传输](#web-transports)的工作流和配置,并且它始终验证 GraphQL HTTP 响应是 200(OK)。 要创建`WebGraphQlTester`,你需要以下输入之一: * `WebTestClient`——作为 HTTP 客户机执行请求,或者针对没有服务器的[HTTP](#web-http)处理程序,或者针对活动服务器。 * `WebGraphQlHandler`——通过[网络拦截](#web-interception)和[WebSocket](#web-websocket)处理程序都使用的[网络拦截](#web-interception)链执行请求,这实际上是在没有 Web 框架的情况下进行的测试。使用这个的一个原因是[订阅](#testing-subscriptions)。 对于没有服务器的 Spring WebFlux,你可以指向你的 Spring 配置: ``` ApplicationContext context = ... ; WebTestClient client = WebTestClient.bindToApplicationContext(context) .configureClient() .baseUrl("/graphql") .build(); WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); ``` 对于没有服务器的 Spring MVC,使用`MockMvcWebTestClient`的方法是相同的: ``` WebApplicationContext context = ... ; WebTestClient client = MockMvcWebTestClient.bindToApplicationContext(context) .configureClient() .baseUrl("/graphql") .build(); WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); ``` 对运行中的实时服务器进行测试: ``` WebTestClient client = WebTestClient.bindToServer() .baseUrl("http://localhost:8080/graphql") .build(); WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); ``` `WebGraphQlTester`支持设置 HTTP 请求标头和访问 HTTP 响应标头。这对于检查或设置与安全性相关的标题可能很有用。 ``` this.graphQlTester.queryName("{ myQuery }") .httpHeaders(headers -> headers.setBasicAuth("rob", "...")) .execute() .httpHeadersSatisfy(headers -> { // check response headers }) .path("myQuery.field1").entity(String.class).isEqualTo("value1") .path("myQuery.field2").entity(String.class).isEqualTo("value2"); ``` 你还可以在构建器级别设置默认的请求标题: ``` WebGraphQlTester tester = WebGraphQlTester.builder(client) .defaultHttpHeaders(headers -> headers.setBasicAuth("rob", "...")) .build(); ``` ### 8.3. 查询 下面是一个使用[JsonPath](https://github.com/json-path/JsonPath)提取 GraphQL 响应中所有发布版本的示例查询测试。 ``` String query = "{" + " project(slug:\"spring-framework\") {" + " releases {" + " version" + " }"+ " }" + "}"; graphQlTester.query(query) .execute() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1); ``` JSONPath 是相对于响应的“数据”部分的。 还可以在 Classpath 上的`"graphql/"`下创建扩展名为`.graphql`或`.gql`的查询文件,并通过文件名引用它们。例如,给定一个名为`projectReleases.graphql`in`src/main/resources/graphql`的文件,其内容如下: ``` query projectReleases($slug: ID!) { project(slug: $slug) { releases { version } } } ``` 你可以编写相同的测试,如下所示: ``` graphQlTester.queryName("projectReleases") (1) .variable("slug", "spring-framework") (2) .execute() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1); ``` |**1**|请参阅名为“ProjectReleases”的文件中的查询。| |-----|-------------------------------------------------------| |**2**|设置`slug`变量。| | |IntelliJ 的“JS GraphQL”插件支持带有代码补全功能的 GraphQL 查询文件。| |---|---------------------------------------------------------------------------------------| ### 8.4. 错误 当响应中的“错误”键下有错误时,验证将不会成功。 如果有必要忽略错误,请使用错误过滤器`Predicate`: ``` graphQlTester.query(query) .execute() .errors() .filter(error -> ...) .verify() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1); ``` 错误筛选器可以全局注册并应用于所有测试: ``` WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client) .errorFilter(error -> ...) .build(); ``` 或者预期会出现错误,与`filter`相反,如果响应中不存在断言错误,则抛出该断言错误: ``` graphQlTester.query(query) .execute() .errors() .expect(error -> ...) .verify() .path("project.releases[*].version") .entityList(String.class) .hasSizeGreaterThan(1); ``` 或直接检查所有错误,并将其标记为已过滤的: ``` graphQlTester.query(query) .execute() .errors() .satisfy(errors -> { // ... }); ``` 如果请求没有任何响应数据(例如,突变),请使用`executeAndVerify`而不是`execute`来验证响应中没有错误: ``` graphQlTester.query(query).executeAndVerify(); ``` ### 8.5. 订阅 `executeSubscription`方法定义了一个特定于订阅的工作流,该工作流将返回一个响应流,而不是单个响应。 要测试订阅,你可以使用`GraphQlTester`创建`GraphQlService`,它直接调用`graphql.GraphQL`,并返回一个响应流: ``` GraphQlService service = ... ; GraphQlTester graphQlTester = GraphQlTester.builder(service).build(); Flux result = graphQlTester.query("subscription { greetings }") .executeSubscription() .toFlux("greetings", String.class); // decode each response ``` 来自 Project Reactor 的对于验证流是有用的: ``` Flux result = graphQlTester.query("subscription { greetings }") .executeSubscription() .toFlux("greetings", String.class); StepVerifier.create(result) .expectNext("Hi") .expectNext("Bonjour") .expectNext("Hola") .verifyComplete(); ``` 要测试[网络拦截](#web-interception)链,你可以使用`WebGraphQlHandler`创建`WebGraphQlTester`: ``` GraphQlService service = ... ; WebGraphQlHandler handler = WebGraphQlHandler.builder(service) .interceptor((input, next) -> next.handle(input)) .build(); WebGraphQlTester graphQlTester = WebGraphQlTester.builder(handler).build(); ``` 目前, Spring for GraphQL 不支持使用 WebSocket 客户端进行测试,并且它不能用于在 WebSocket 请求上对 GraphQL 进行集成测试。 ## 9. 样本 Spring 对于 GraphQL 存储库,针对各种场景包含[示例应用程序](https://github.com/spring-projects/spring-graphql/tree/main/samples)。 你可以通过克隆此存储库并从你的 IDE 中运行主应用程序类,或者通过在命令行中键入以下内容来运行这些应用程序: ``` $ ./gradlew :samples:{sample-directory-name}:bootRun ```