跳到主要内容

客户端

DeepSeek V3 中英对照 Client

Spring for GraphQL 包含通过 HTTP、WebSocket 和 RSocket 执行 GraphQL 请求的客户端支持。

GraphQlClient

GraphQlClient 定义了独立于底层传输的 GraphQL 请求通用工作流,因此无论使用何种传输方式,执行请求的方式都保持一致。

以下传输层特定的 GraphQlClient 扩展可供使用:

每个传输方式都定义了一个 Builder,其中包含与该传输相关的选项。所有构建器都继承自一个通用的基础 GraphQlClient Builder,该构建器包含适用于所有传输的选项。

一旦 GraphQlClient 构建完成,你就可以开始发起请求

通常,GraphQL 操作以文本形式提供。或者,您可以通过 DGS Codegen 客户端 API 类,利用 DgsGraphQlClient 来包装上述任何 GraphQlClient 扩展。

HTTP 同步

HttpSyncGraphQlClient 使用 RestClient 通过阻塞式传输契约和拦截器链来执行基于 HTTP 的 GraphQL 请求。

RestClient restClient = RestClient.create("https://spring.io/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

一旦创建了 HttpSyncGraphQlClient,你就可以开始使用相同的 API 执行请求,而无需关心底层的传输方式。如果需要更改任何特定于传输的细节,可以在现有的 HttpSyncGraphQlClient 上使用 mutate() 方法来创建一个具有自定义设置的新实例:

RestClient restClient = RestClient.create("https://spring.io/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.builder(restClient)
.headers((headers) -> headers.setBasicAuth("joe", "..."))
.build();

// Perform requests with graphQlClient...

HttpSyncGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers((headers) -> headers.setBasicAuth("peter", "..."))
.build();

// Perform requests with anotherGraphQlClient...

HTTP

HttpGraphQlClient 使用 WebClient 通过非阻塞传输契约和拦截器链来执行基于 HTTP 的 GraphQL 请求。

WebClient webClient = WebClient.create("https://spring.io/graphql");
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

一旦创建了 HttpGraphQlClient,您就可以开始使用相同的 API 执行请求,而无需依赖底层传输方式。如果需要更改任何特定于传输的细节,可以在现有的 HttpGraphQlClient 上使用 mutate() 方法,以创建具有自定义设置的新实例:

WebClient webClient = WebClient.create("https://spring.io/graphql");

HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient)
.headers((headers) -> headers.setBasicAuth("joe", "..."))
.build();

// Perform requests with graphQlClient...

HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers((headers) -> headers.setBasicAuth("peter", "..."))
.build();

// Perform requests with anotherGraphQlClient...

WebSocket

WebSocketGraphQlClient 通过共享的 WebSocket 连接执行 GraphQL 请求。它基于 Spring WebFlux 中的 WebSocketClient 构建,你可以按以下方式创建它:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();

HttpGraphQlClient 不同,WebSocketGraphQlClient 是面向连接的,这意味着它需要在发起任何请求之前建立连接。当您开始发起请求时,连接会透明地建立。或者,您也可以使用客户端的 start() 方法在任何请求之前显式地建立连接。

除了面向连接的特性外,WebSocketGraphQlClient 还支持多路复用。它会为所有请求维护一个单一共享连接。如果连接断开,将在下一次请求或再次调用 start() 时重新建立连接。您也可以使用客户端的 stop() 方法,该方法会取消进行中的请求、关闭连接并拒绝新的请求。

提示

为每个服务器使用单一的 WebSocketGraphQlClient 实例,以便对该服务器的所有请求共享一个连接。每个客户端实例会建立自己的连接,这通常不是单个服务器的预期行为。

一旦创建了 WebSocketGraphQlClient,你就可以开始使用相同的 API 执行请求,这与底层传输方式无关。如果需要更改任何特定于传输的细节,可以在现有的 WebSocketGraphQlClient 上使用 mutate() 方法,以创建具有自定义设置的新实例:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.headers((headers) -> headers.setBasicAuth("joe", "..."))
.build();

// Use graphQlClient...

WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate()
.headers((headers) -> headers.setBasicAuth("peter", "..."))
.build();

// Use anotherGraphQlClient...

WebSocketGraphQlClient 支持发送周期性 ping 消息,以在没有其他消息发送或接收时保持连接活跃。你可以按如下方式启用此功能:

String url = "wss://spring.io/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.keepAlive(Duration.ofSeconds(30))
.build();

拦截器

GraphQL over WebSocket 协议除了执行请求外,还定义了一系列面向连接的消息。例如,在连接开始时,客户端发送 "connection_init",服务器则回应 "connection_ack"

对于 WebSocket 传输的特定拦截,你可以创建一个 WebSocketGraphQlClientInterceptor

static class MyInterceptor implements WebSocketGraphQlClientInterceptor {

@Override
public Mono<Object> connectionInitPayload() {
// ... the "connection_init" payload to send
}

@Override
public Mono<Void> handleConnectionAck(Map<String, Object> ackPayload) {
// ... the "connection_ack" payload received
}

}

将上述拦截器注册为任何其他 GraphQlClientInterceptor 并同样用于拦截 GraphQL 请求,但请注意,类型为 WebSocketGraphQlClientInterceptor 的拦截器最多只能有一个。

RSocket

RSocketGraphQlClient 使用 RSocketRequester 通过 RSocket 请求执行 GraphQL 请求。

URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(uri);

RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.clientTransport(transport)
.build();

HttpGraphQlClient 不同,RSocketGraphQlClient 是面向连接的,这意味着它需要在发起任何请求之前建立会话。当您开始发起请求时,会话会透明地建立。或者,您也可以使用客户端的 start() 方法在任何请求之前显式建立会话。

RSocketGraphQlClient 同样是多路复用的。它为所有请求维护一个单一的共享会话。如果会话丢失,将在下一个请求或再次调用 start() 时重新建立。你也可以使用客户端的 stop() 方法,该方法会取消进行中的请求、关闭会话并拒绝新的请求。

提示

为每个服务器使用单一的 RSocketGraphQlClient 实例,以便对该服务器的所有请求共享一个会话。每个客户端实例都会建立自己的连接,这通常不是单个服务器的预期行为。

一旦创建了 RSocketGraphQlClient,你就可以开始使用相同的 API 执行请求,这与底层传输方式无关。

Builder

GraphQlClient 定义了一个父类 BaseBuilder,其中包含了所有扩展构建器的通用配置选项。目前,它允许您配置:

  • DocumentSource 策略,用于从文件加载请求的文档

  • 已执行请求的拦截

BaseBuilder 被以下类进一步扩展:

  • SyncBuilder - 带有 SyncGraphQlInterceptor 链的阻塞执行栈。

  • Builder - 带有 GraphQlInterceptor 链的非阻塞执行栈。

请求

一旦你有了 GraphQlClient,就可以通过 retrieveexecute 方法开始执行请求。

检索

以下代码用于检索并解码查询的数据:

String document =
"""
{
project(slug:"spring-framework") {
name
releases {
version
}
}
}
""";

Project project = graphQlClient.document(document) 1
.retrieveSync("project") 2
.toEntity(Project.class); 3
  • 要执行的操作。

  • 响应映射中 "data" 键下的路径,用于解码。

  • 将指定路径的数据解码为目标类型。

输入文档是一个 String,可以是字面量,也可以通过代码生成的请求对象产生。您还可以在文件中定义文档,并使用文档源通过文件名来解析它们。

路径相对于 "data" 键,并使用简单的点号 (".") 分隔表示法来表示嵌套字段,对于列表元素可选择性地使用数组索引,例如 "project.name""project.releases[0].version"

如果给定路径不存在,或者字段值为 null 且存在错误,解码可能导致 FieldAccessExceptionFieldAccessException 提供了对响应和字段的访问权限:

try {
Project project = graphQlClient.document(document)
.retrieveSync("project")
.toEntity(Project.class);
return project;
}
catch (FieldAccessException ex) {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// return fallback value
return new Project();
}

如果字段存在但无法解码为请求的类型,则会抛出一个普通的 GraphQlClientException 异常。

执行

Retrieve 仅是从响应映射中解码单个路径的快捷方式。如需更多控制,请使用 execute 方法并处理响应:

例如:

ClientGraphQlResponse response = graphQlClient.document(document).executeSync();

if (!response.isValid()) {
// Request failure... // <1>
}

ClientResponseField field = response.field("project");
if (field.getValue() == null) {
if (field.getErrors().isEmpty()) {
// Optional field set to null... // <2>
}
else {
// Field failure... // <3>
}
}

Project project = field.toEntity(Project.class); 4
  • 响应没有数据,只有错误

  • 被其 DataFetcher 设置为 null 的字段

  • null 且有关联错误的字段

  • 解码给定路径的数据

文档来源

请求的文档是一个 String,可以在局部变量或常量中定义,也可以通过代码生成的请求对象来生成。

您也可以在类路径下的 "graphql-documents/" 目录中创建扩展名为 .graphql.gql 的文档文件,并通过文件名引用它们。

例如,给定一个位于 src/main/resources/graphql-documents 目录下名为 projectReleases.graphql 的文件,其内容如下:

query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}

然后你可以:

Project project = graphQlClient.documentName("projectReleases") 1
.variable("slug", "spring-framework") 2
.retrieveSync("projectReleases.project")
.toEntity(Project.class);
  • 从 "projectReleases.graphql" 加载文档

  • 提供变量值

IntelliJ 的 "JS GraphQL" 插件支持 GraphQL 查询文件并提供代码补全功能。

你可以使用 GraphQlClient 构建器 来自定义按名称加载文档的 DocumentSource

订阅请求

订阅请求需要一个能够流式传输数据的客户端传输层。您需要创建一个支持此功能的 GraphQlClient

检索

要启动订阅流,请使用 retrieveSubscription,它类似于针对单个响应的 retrieve,但会返回一个响应流,每个响应都会被解码为某些数据:

Flux<String> greetingFlux = client.document("subscription { greetings }")
.retrieveSubscription("greeting")
.toEntity(String.class);

如果订阅从服务器端以“error”消息结束,Flux 可能会以 SubscriptionErrorException 终止。该异常提供对从“error”消息解码出的 GraphQL 错误的访问。

如果底层连接关闭或丢失,Flux 可能会以 GraphQlTransportException(例如 WebSocketDisconnectedException)终止。在这种情况下,您可以使用 retry 操作符来重新启动订阅。

要从客户端终止订阅,必须取消 Flux,随后 WebSocket 传输会向服务器发送一条"complete"消息。如何取消 Flux 取决于其使用方式。某些操作符(如 taketimeout)会自动取消 Flux。如果通过 Subscriber 订阅 Flux,可以获取 Subscription 的引用并通过它进行取消。onSubscribe 操作符同样提供了对 Subscription 的访问权限。

执行

Retrieve 仅是从每个响应映射中的单个路径进行解码的快捷方式。如需更精细的控制,请使用 executeSubscription 方法并直接处理每个响应:

Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map((response) -> {
if (!response.isValid()) {
// Request failure...
}

ClientResponseField field = response.field("project");
if (field.getValue() == null) {
if (field.getErrors().isEmpty()) {
// Optional field set to null...
}
else {
// Field failure...
}
}

return field.toEntity(String.class);
});

拦截

对于使用 GraphQlClient.SyncBuilder 创建的阻塞式传输层,你需要创建一个 SyncGraphQlClientInterceptor 来拦截通过该客户端的所有请求:

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.SyncGraphQlClientInterceptor;

public class SyncInterceptor implements SyncGraphQlClientInterceptor {

@Override
public ClientGraphQlResponse intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}
}

对于使用 GraphQlClient.Builder 创建的非阻塞传输客户端,您需要创建一个 GraphQlClientInterceptor 来拦截通过该客户端发出的所有请求:

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.graphql.client.ClientGraphQlRequest;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.GraphQlClientInterceptor;

public class MyInterceptor implements GraphQlClientInterceptor {

@Override
public Mono<ClientGraphQlResponse> intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}

@Override
public Flux<ClientGraphQlResponse> interceptSubscription(ClientGraphQlRequest request, SubscriptionChain chain) {
// ...
return chain.next(request);
}

}

创建拦截器后,通过客户端构建器进行注册。例如:

URI url = URI.create("wss://localhost:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.interceptor(new MyInterceptor())
.build();

可选输入

默认情况下,GraphQL 中的输入类型是可空且可选的。输入值(或其任何字段)可以设置为 null 字面量,或者完全不提供。这种区分对于带有变更的部分更新非常有用,因为底层数据可能被设置为 null,或者相应地完全不更改。

类似于控制器中的 ArgumentValue<T> 支持,我们可以在客户端将 Input 类型包装为 ArgumentValue<T>,或在类属性级别使用它。给定一个 ProjectInput 类如下:

import org.springframework.graphql.data.ArgumentValue;

public record ProjectInput(String id, ArgumentValue<String> name) {

}

我们可以使用客户端发送一个变更请求:

public void updateProject() {
ProjectInput projectInput = new ProjectInput("spring-graphql",
ArgumentValue.ofNullable("Spring for GraphQL")); 1
ClientGraphQlResponse response = this.graphQlClient.document("""
mutation updateProject($project: ProjectInput!) {
updateProject($project: $project) {
id
name
}
}
""")
.variables(Map.of("project", projectInput))
.executeSync();
}
  • 我们可以改用 ArgumentValue.omitted() 来忽略此字段

要使此功能正常工作,客户端必须使用Jackson进行JSON(反)序列化,并且必须配置org.springframework.graphql.client.json.GraphQlJacksonModule。可以在底层HTTP客户端上手动注册该模块,如下所示:

public ArgumentValueClient(HttpGraphQlClient graphQlClient) {
JsonMapper jsonMapper = JsonMapper.builder().addModule(new GraphQlJacksonModule()).build();
JacksonJsonEncoder jsonEncoder = new JacksonJsonEncoder(jsonMapper);
WebClient webClient = WebClient.builder()
.baseUrl("https://example.com/graphql")
.codecs((codecs) -> codecs.defaultCodecs().jacksonJsonEncoder(jsonEncoder))
.build();
this.graphQlClient = HttpGraphQlClient.create(webClient);
}

这个 GraphQlJacksonModule 可以通过将其注册为 Bean 的方式,在 Spring Boot 应用中全局注册:

@Configuration
public class GraphQlJsonConfiguration {

@Bean
public GraphQlJacksonModule graphQLModule() {
return new GraphQlJacksonModule();
}

}
备注

Jackson 2.x 的支持也可以通过 GraphQlJackson2Module 实现。

DGS Codegen

除了以文本形式提供操作(如变更、查询或订阅)外,您还可以使用 DGS Codegen 库来生成客户端 API 类,这些类允许您使用流畅的 API 来定义请求。

Spring for GraphQL 提供了 DgsGraphQlClient,它包装了任何 GraphQlClient,并帮助使用生成的客户端 API 类来准备请求。

例如,给定以下模式:

type Query {
books: [Book]
}

type Book {
id: ID
name: String
}

你可以按如下方式执行请求:

HttpGraphQlClient client = HttpGraphQlClient.create(WebClient.create("https://example.org/graphql"));
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); 1

List<Book> books = dgsClient.request(BookByIdGraphQLQuery.newRequest().id("42").build()) 2
.projection(new BooksProjectionRoot<>().id().name()) 3
.retrieveSync("books")
.toEntityList(Book.class);
  • 通过包装任何 GraphQlClient 来创建 DgsGraphQlClient

  • 为请求指定操作。

  • 定义选择集。

DgsGraphQlClient 还支持通过链式调用 query() 方法执行多个查询:

HttpGraphQlClient client = HttpGraphQlClient.create(WebClient.create("https://example.org/graphql"));
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); 1

ClientGraphQlResponse response = dgsClient
.request(BookByIdGraphQLQuery.newRequest().id("42").build()) 2
.queryAlias("firstBook") 3
.projection(new BooksProjectionRoot<>().id().name())
.request(BookByIdGraphQLQuery.newRequest().id("53").build()) 4
.queryAlias("secondBook")
.projection(new BooksProjectionRoot<>().id().name())
.executeSync(); 5

Book firstBook = response.field("firstBook").toEntity(Book.class); 6
Book secondBook = response.field("secondBook").toEntity(Book.class);
  • 通过包装任何 GraphQlClient 来创建 DgsGraphQlClient

  • 为第一个请求指定操作。

  • 当发送多个请求时,我们需要为每个请求指定一个别名

  • 为第二个请求指定操作。

  • 获取完整的响应

  • 使用配置的别名获取相关的文档部分