用戶端
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
後,您就可以開始透過 retrieve 或 execute 方法執行請求。
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
且有錯誤,則解碼可能會導致 FieldAccessException
。FieldAccessException
提供對回應和欄位的存取權限
-
同步
-
非阻塞
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
的檔案,其內容為
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
-
HttpGraphQlClient 搭配 Server-Sent Events
-
WebSocketGraphQlClient 搭配 WebSocket
-
RSocketGraphQlClient 搭配 RSocket
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
取決於其使用方式。某些運算子 (例如 take
或 timeout
) 本身會取消 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 | - 定義選取集。 |