客户端
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 方法开始执行请求。
检索
以下代码用于检索并解码查询的数据:
- Sync
- Non-Blocking
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
要执行的操作。
响应映射中 "data" 键下的路径,用于解码。
将指定路径的数据解码为目标类型。
输入文档是一个 String,可以是字面量,也可以通过代码生成的请求对象产生。您还可以在文件中定义文档,并使用文档源通过文件名来解析它们。
路径相对于 "data" 键,并使用简单的点号 (".") 分隔表示法来表示嵌套字段,对于列表元素可选择性地使用数组索引,例如 "project.name" 或 "project.releases[0].version"。
如果给定路径不存在,或者字段值为 null 且存在错误,解码可能导致 FieldAccessException。FieldAccessException 提供了对响应和字段的访问权限:
- Sync
- Non-Blocking
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());
});
如果字段存在但无法解码为请求的类型,则会抛出一个普通的 GraphQlClientException 异常。
执行
Retrieve 仅是从响应映射中解码单个路径的快捷方式。如需更多控制,请使用 execute 方法并处理响应:
例如:
- Sync
- Non-Blocking
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
});
响应没有数据,只有错误
被其
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:
-
HttpGraphQlClient 配合服务器发送事件
-
WebSocketGraphQlClient 配合 WebSocket
-
RSocketGraphQlClient 配合 RSocket
检索
要启动订阅流,请使用 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。为第一个请求指定操作。
当发送多个请求时,我们需要为每个请求指定一个别名
为第二个请求指定操作。
获取完整的响应
使用配置的别名获取相关的文档部分