跳到主要内容

客户端

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,就可以通过 retrieve 或 execute 方法开始执行请求。

检索​

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

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 且存在错误,解码可能导致 FieldAccessException。FieldAccessException 提供了对响应和字段的访问权限:

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 取决于其使用方式。某些操作符(如 take 或 timeout)会自动取消 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。

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

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

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

  • 获取完整的响应

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