用戶端

Spring for GraphQL 包含用戶端支援,可透過 HTTP、WebSocket 和 RSocket 執行 GraphQL 請求。

GraphQlClient

GraphQlClient 定義了 GraphQL 請求的通用工作流程,不受底層傳輸的影響。因此,無論使用哪種傳輸方式,執行請求的方式都相同。

以下是特定傳輸方式的 GraphQlClient 擴充功能:

每個擴充功能都定義了一個 Builder,其中包含與傳輸相關的選項。所有 Builder 都從通用的基礎 GraphQlClient Builder 擴充而來,其中包含適用於所有傳輸方式的選項。

一旦建立 GraphQlClient,您就可以開始發出請求

通常,請求的 GraphQL 操作以文字形式提供。或者,您可以透過 DGS Codegen 用戶端 API 類別搭配 DgsGraphQlClient,它可以封裝上述任何 GraphQlClient 擴充功能。

HTTP 同步

HttpSyncGraphQlClient 使用 RestClient,透過阻塞傳輸協定和攔截器鏈,經由 HTTP 執行 GraphQL 請求。

RestClient restClient = RestClient.create("https://spring.dev.org.tw/graphql");
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);

建立 HttpSyncGraphQlClient 後,您就可以開始使用相同的 API 執行請求,而與底層傳輸無關。如果您需要變更任何特定於傳輸的詳細資訊,請在現有的 HttpSyncGraphQlClient 上使用 mutate() 建立具有自訂設定的新執行個體

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

建立 HttpGraphQlClient 後,您就可以開始使用相同的 API 執行請求,而與底層傳輸無關。如果您需要變更任何特定於傳輸的詳細資訊,請在現有的 HttpGraphQlClient 上使用 mutate() 建立具有自訂設定的新執行個體

WebClient webClient = WebClient.create("https://spring.dev.org.tw/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.dev.org.tw/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.dev.org.tw/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.dev.org.tw/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://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 執行請求,而與底層傳輸無關。

Builder

GraphQlClient 定義了一個父 BaseBuilder,其中包含所有擴充功能的 Builder 的通用組態選項。目前,它可讓您設定

  • DocumentSource 策略,從檔案載入請求的文件

  • 攔截已執行的請求

BaseBuilder 進一步由以下項目擴充

  • SyncBuilder - 具有 SyncGraphQlInterceptor 鏈的阻塞執行堆疊。

  • Builder - 具有 GraphQlInterceptor 鏈的非阻塞執行堆疊。

請求

取得 GraphQlClient 後,您就可以開始透過 retrieveexecute 方法執行請求。

Retrieve

以下程式碼擷取並解碼查詢的資料

  • 同步

  • 非阻塞

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,可以是文字字串,也可以是透過程式碼產生的請求物件產生。您也可以在檔案中定義文件,並使用 Document Source 依檔案名稱解析它們。

路徑相對於 "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

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 解碼給定路徑上的資料

Document Source

請求的文件是一個 String,可以定義在區域變數或常數中,也可以透過程式碼產生的請求物件產生。

您也可以在類別路徑上的 "graphql-documents/" 下建立副檔名為 .graphql.gql 的文件,並依檔案名稱參照它們。

例如,假設在 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 Builder 自訂 DocumentSource,以依名稱載入文件。

訂閱請求

訂閱請求需要能夠串流資料的用戶端傳輸。您需要建立支援此功能的 GraphQlClient

Retrieve

若要啟動訂閱串流,請使用 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 取決於其使用方式。某些運算子 (例如 taketimeout) 本身會取消 Flux。如果您使用 Subscriber 訂閱 Flux,則可以取得對 Subscription 的參照,並透過它取消。onSubscribe 運算子也提供對 Subscription 的存取權限。

Execute

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

}

建立攔截器後,透過用戶端 Builder 註冊它。例如

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 Codegen

除了以文字形式提供操作 (例如 mutation、query 或 subscription) 之外,您還可以利用 DGS Codegen 程式庫產生用戶端 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 - 定義選取集。