客户端

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://springframework.org.cn/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

创建 HttpSyncGraphQlClient 后,您可以使用相同的 API 开始执行请求,无论底层传输是什么。如果您需要更改任何传输特定的详细信息,请对现有 HttpSyncGraphQlClient 使用 mutate() 来创建一个具有自定义设置的新实例。

RestClient restClient = RestClient.create("https://springframework.org.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://springframework.org.cn/graphql");
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);

创建 HttpGraphQlClient 后,您可以使用相同的 API 开始执行请求,无论底层传输是什么。如果您需要更改任何传输特定的详细信息,请对现有 HttpGraphQlClient 使用 mutate() 来创建一个具有自定义设置的新实例。

WebClient webClient = WebClient.create("https://springframework.org.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://springframework.org.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://springframework.org.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://springframework.org.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://: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 后,您可以通过 retrieveexecute 方法开始执行请求。

检索

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

  • 同步

  • 非阻塞

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());
		});

如果字段存在但无法解码为请求的类型,则会抛出普通的 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)
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-documents/" 中创建扩展名为 .graphql.gql 的文档文件,并通过文件名引用它们。

例如,给定一个名为 projectReleases.graphql 的文件在 src/main/resources/graphql-documents 中,内容如下:

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

检索

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

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

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

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

要从客户端终止订阅,必须取消 Flux,然后 WebSocket 传输会向服务器发送一个“完成”消息。如何取消 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://:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

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

可选输入

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

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

import org.springframework.graphql.data.ArgumentValue;

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

}

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

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();
}
1 我们可以使用 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 代码生成

作为以文本形式提供操作(例如 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 = 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);
1 通过包装任何 GraphQlClient 来创建 DgsGraphQlClient
2 指定请求的操作。
3 定义选择集。

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);
1 通过包装任何 GraphQlClient 来创建 DgsGraphQlClient
2 指定第一个请求的操作。
3 发送多个请求时,我们需要为每个请求指定一个别名。
4 指定第二个请求的操作。
5 获取完整的响应
6 获取具有已配置别名的相关文档部分。
© . This site is unofficial and not affiliated with VMware.