客戶端

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.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 響應對映中 "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.