客戶端

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

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

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

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

WebClient webClient = WebClient.create("https://springframework.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://springframework.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://springframework.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://springframework.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://: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 響應 map 中 "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());
		});

執行

獲取只是從響應 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 的檔案,內容如下:

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,它類似於單響應的 獲取,但返回一個響應流,每個響應都被解碼為一些資料:

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 的訪問。

執行

獲取只是從每個響應 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://: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 - 定義選擇集。