客户端
Spring for GraphQL 包含客户端支持,可通过 HTTP、WebSocket 和 RSocket 执行 GraphQL 请求。
GraphQlClient
GraphQlClient
定义了 GraphQL 请求的通用工作流,独立于底层传输方式,因此无论使用何种传输方式,执行请求的方式都是相同的。
以下是特定于传输方式的 GraphQlClient
扩展:
每个都定义了一个 Builder
,其中包含与该传输方式相关的选项。所有构建器都扩展自一个通用的基础 GraphQlClient Builder
,其中包含适用于所有传输方式的选项。
一旦构建了 GraphQlClient
,您就可以开始发送请求。
通常,请求的 GraphQL 操作以文本形式提供。另外,您可以通过 DgsGraphQlClient 使用 DGS Codegen 客户端 API 类,DgsGraphQlClient 可以封装上述任何 GraphQlClient
扩展。
HTTP 同步
HttpSyncGraphQlClient
使用 RestClient 通过阻塞式传输契约和拦截器链在 HTTP 上执行 GraphQL 请求。
RestClient restClient = RestClient.create("https://springjava.cn/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);
一旦创建了 HttpSyncGraphQlClient
,您就可以使用相同的 API 开始执行请求,而与底层传输无关。如果需要更改任何特定于传输的详细信息,请在现有的 HttpSyncGraphQlClient
上使用 mutate()
创建一个具有自定义设置的新实例
RestClient restClient = RestClient.create("https://springjava.cn/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://springjava.cn/graphql");
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
一旦创建了 HttpGraphQlClient
,您就可以使用相同的 API 开始执行请求,而与底层传输无关。如果需要更改任何特定于传输的详细信息,请在现有的 HttpGraphQlClient
上使用 mutate()
创建一个具有自定义设置的新实例
WebClient webClient = WebClient.create("https://springjava.cn/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://springjava.cn/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://springjava.cn/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://springjava.cn/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 开始执行请求,而与底层传输无关。
构建器
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)
String document =
"""
{
project(slug:"spring-framework") {
name
releases {
version
}
}
}
""";
Mono<Project> project = graphQlClient.document(document) (1)
.retrieve("project") (2)
.toEntity(Project.class); (3)
1 | 要执行的操作。 |
2 | 响应 map 中 "data" 键下的路径,用于解码。 |
3 | 将该路径下的数据解码到目标类型。 |
输入文档是一个 String
,可以是字面值,也可以通过代码生成的请求对象产生。您还可以将文档定义在文件中,并使用 Document Source 按文件名解析它们。
该路径是相对于 "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();
}
Mono<Project> projectMono = graphQlClient.document(document)
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, (ex) -> {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// return fallback value
return Mono.just(new Project());
});
执行
获取只是从响应 map 中的单个路径进行解码的快捷方式。为了获得更多控制,请使用 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)
Mono<Project> projectMono = graphQlClient.document(document)
.execute()
.map((response) -> {
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)
}
}
return field.toEntity(Project.class); (4)
});
1 | 响应没有数据,只有错误 |
2 | 由其 DataFetcher 设置为 null 的字段 |
3 | 为 null 且具有关联错误的字段 |
4 | 解码给定路径的数据 |
文档源
请求的文档是一个 String
,可以定义在局部变量或常量中,也可以通过代码生成的请求对象产生。
您还可以在 classpath 下的 "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);
1 | 从 "projectReleases.graphql" 加载文档 |
2 | 提供变量值。 |
IntelliJ 的 "JS GraphQL" 插件支持带代码补全的 GraphQL 查询文件。
您可以使用 GraphQlClient
Builder 自定义 DocumentSource
,以按名称加载文档。
订阅请求
订阅请求需要能够流式传输数据的客户端传输方式。您需要创建一个支持此功能的 GraphQlClient
:
-
HttpGraphQlClient,支持 Server-Sent Events
-
WebSocketGraphQlClient,支持 WebSocket
-
RSocketGraphQlClient,支持 RSocket
获取
要启动订阅流,请使用 retrieveSubscription
,它类似于单响应的 获取,但返回一个响应流,每个响应都被解码为一些数据:
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
的访问。
执行
获取只是从每个响应 map 中的单个路径进行解码的快捷方式。为了获得更多控制,请使用 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();
DGS Codegen
除了将操作(如 mutation、query 或 subscription)作为文本提供外,您还可以使用 DGS Codegen 库生成客户端 API 类,这些类允许您使用流式 API 定义请求。
Spring for GraphQL 提供了 DgsGraphQlClient,它可以封装任何 GraphQlClient
,并帮助使用生成的客户端 API 类准备请求。
例如,给定以下 Schema:
type Query {
books: [Book]
}
type Book {
id: ID
name: String
}
您可以按如下方式执行请求:
HttpGraphQlClient client = ... ;
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); (1)
List<Book> books = dgsClient.request(new BooksGraphQLQuery()) (2)
.projection(new BooksProjectionRoot<>().id().name()) (3)
.retrieveSync("books")
.toEntityList(Book.class);
1 | - 通过封装任何 GraphQlClient 创建 DgsGraphQlClient 。 |
2 | - 指定请求的操作。 |
3 | - 定义选择集。 |