客户端

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

GraphQlClient

GraphQlClient 定义了 GraphQL 请求的通用工作流程,独立于底层传输,因此无论使用哪种传输方式,执行请求的方式都相同。

以下传输特定的 GraphQlClient 扩展可用

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

构建 GraphQlClient 后,您可以开始进行 请求

通常,请求的 GraphQL 操作以文本形式提供。或者,您可以通过 DgsGraphQlClient 使用 DGS Codegen 客户端 API 类,它可以包装上述任何 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();

拦截器

除了执行请求之外,基于 WebSocket 的 GraphQL 协议还定义了一些面向连接的消息。例如,客户端在连接开始时发送 "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://127.0.0.1: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 后,您可以通过 检索执行 方法开始执行请求。

检索

以下检索并解码查询的数据

  • 同步

  • 非阻塞

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 响应映射中“data”键下的解码路径。
3 将路径处的数据解码为目标类型。

输入文档是一个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();
}
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());
		});

执行

获取只是从响应映射中的单个路径解码的快捷方式。为了获得更多控制,请使用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,可以在本地变量或常量中定义,也可以通过代码生成的请求对象生成。

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

例如,假设在src/main/resources/graphql-documents中有一个名为projectReleases.graphql的文件,其内容为

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 构建器自定义DocumentSource,以便通过名称加载文档。

订阅请求

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

获取

要启动订阅流,请使用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取决于它的使用方式。某些操作符(如taketimeout)本身会取消Flux。如果使用Subscriber订阅Flux,则可以获取对Subscription的引用并通过它取消。onSubscribe操作符也提供对Subscription的访问。

执行

获取只是从每个响应映射中的单个路径解码的快捷方式。为了获得更多控制,请使用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://127.0.0.1:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

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

DGS 代码生成

作为提供操作(如变异、查询或订阅)作为文本的替代方案,您可以使用DGS 代码生成库生成客户端API类,让您使用流畅的API来定义请求。

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

例如,给定以下模式

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 - 定义选择集。