Spring for GraphQL includes client support to execute GraphQL requests over HTTP, WebSocket, and RSocket.
GraphQlClient
GraphQlClient
defines a common workflow for GraphQL requests independent of the underlying
transport, so the way you perform requests is the same no matter what transport is in use.
The following transport specific GraphQlClient
extensions are available:
Each defines a Builder
with options relevant to the transport. All builders extend
from a common, base GraphQlClient Builder
with options applicable to all transports.
Once GraphQlClient
is built you can begin to make requests.
Typically, the GraphQL operation for a request is provided as text. Alternatively, you
can use DGS Codegen client API classes through
DgsGraphQlClient, which can wrap any of the
above GraphQlClient
extensions.
HTTP Sync
HttpSyncGraphQlClient
uses
RestClient
to execute GraphQL requests over HTTP through a blocking transport contract and chain of
interceptors.
RestClient restClient = ... ;
HttpSyncGraphQlClient graphQlClient = HttpSyncGraphQlClient.create(restClient);
Once HttpSyncGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing HttpSyncGraphQlClient
to create a new instance with customized settings:
RestClient restClient = ... ;
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
uses
WebClient to execute
GraphQL requests over HTTP through a non-blocking transport contract and chain of
interceptors.
WebClient webClient = ... ;
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
Once HttpGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing HttpGraphQlClient
to create a new instance with customized settings:
WebClient webClient = ... ;
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
executes GraphQL requests over a shared WebSocket connection.
It is built using the
WebSocketClient
from Spring WebFlux and you can create it as follows:
String url = "wss://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build();
In contrast to HttpGraphQlClient
, the WebSocketGraphQlClient
is connection oriented,
which means it needs to establish a connection before making any requests. As you begin
to make requests, the connection is established transparently. Alternatively, use the
client’s start()
method to establish the connection explicitly before any requests.
In addition to being connection-oriented, WebSocketGraphQlClient
is also multiplexed.
It maintains a single, shared connection for all requests. If the connection is lost,
it is re-established on the next request or if start()
is called again. You can also
use the client’s stop()
method which cancels in-progress requests, closes the
connection, and rejects new requests.
Use a single WebSocketGraphQlClient instance for each server in order to have a
single, shared connection for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Once WebSocketGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport. If you need to change any transport specific details, use mutate()
on an
existing WebSocketGraphQlClient
to create a new instance with customized settings:
URI url = ... ;
WebSocketClient client = ... ;
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
supports sending periodic ping messages to keep the connection
active when no other messages are sent or received. You can enable that as follows:
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.keepAlive(Duration.ofSeconds(30))
.build();
Interceptor
The GraphQL over WebSocket
protocol defines a number of connection oriented messages in addition to executing
requests. For example, a client sends "connection_init"
and the server responds with
"connection_ack"
at the start of a connection.
For WebSocket transport specific interception, you can create a
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
}
}
Register the above interceptor as any other
GraphQlClientInterceptor
and use it also to intercept GraphQL requests, but note there
can be at most one interceptor of type WebSocketGraphQlClientInterceptor
.
RSocket
RSocketGraphQlClient
uses
RSocketRequester
to execute GraphQL requests over RSocket requests.
URI uri = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
.clientTransport(transport)
.build();
In contrast to HttpGraphQlClient
, the RSocketGraphQlClient
is connection oriented,
which means it needs to establish a session before making any requests. As you begin
to make requests, the session is established transparently. Alternatively, use the
client’s start()
method to establish the session explicitly before any requests.
RSocketGraphQlClient
is also multiplexed. It maintains a single, shared session for
all requests. If the session is lost, it is re-established on the next request or if
start()
is called again. You can also use the client’s stop()
method which cancels
in-progress requests, closes the session, and rejects new requests.
Use a single RSocketGraphQlClient instance for each server in order to have a
single, shared session for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Once RSocketGraphQlClient
is created, you can begin to
execute requests using the same API, independent of the underlying
transport.
Builder
GraphQlClient
defines a parent BaseBuilder
with common configuration options for the
builders of all extensions. Currently, it has lets you configure:
-
DocumentSource
strategy to load the document for a request from a file -
Interception of executed requests
BaseBuilder
is further extended by the following:
-
SyncBuilder
- blocking execution stack with a chain ofSyncGraphQlInterceptor
's. -
Builder
- non-blocking execution stack with chain ofGraphQlInterceptor
's.
Use a single WebSocketGraphQlClient instance for each server in order to have a
single, shared connection for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Use a single RSocketGraphQlClient instance for each server in order to have a
single, shared session for all requests to that server. Each client instance
establishes its own connection and that is typically not the intent for a single server.
|
Requests
Once you have a GraphQlClient
, you can begin to perform requests via
retrieve or execute
methods.
Retrieve
The below retrieves and decodes the data for a query:
-
Sync
-
Non-Blocking
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> projectMono = graphQlClient.document(document) (1)
.retrieve("project") (2)
.toEntity(Project.class); (3)
1 | The operation to perform. |
2 | The path under the "data" key in the response map to decode from. |
3 | Decode the data at the path to the target type. |
The input document is a String
that could be a literal or produced through a code
generated request object. You can also define documents in files and use a
Document Source to resole them by file name.
The path is relative to the "data" key and uses a simple dot (".") separated notation
for nested fields with optional array indices for list elements, e.g. "project.name"
or "project.releases[0].version"
.
Decoding can result in FieldAccessException
if the given path is not present, or the
field value is null
and has an error. FieldAccessException
provides access to the
response and the field:
-
Sync
-
Non-Blocking
try {
Project project = graphQlClient.document(document)
.retrieveSync("project")
.toEntity(Project.class);
}
catch (FieldAccessException ex) {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// ...
}
Mono<Project> projectMono = graphQlClient.document(document)
.retrieve("project")
.toEntity(Project.class)
.onErrorResume(FieldAccessException.class, ex -> {
ClientGraphQlResponse response = ex.getResponse();
// ...
ClientResponseField field = ex.getField();
// ...
});
Execute
Retrieve is only a shortcut to decode from a single path in the
response map. For more control, use the execute
method and handle the response:
For example:
-
Sync
-
Non-Blocking
ClientGraphQlResponse response = graphQlClient.document(document).executeSync();
if (!response.isValid()) {
// Request failure... (1)
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure... (2)
}
else {
// Optional field set to null... (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.hasValue()) {
if (field.getError() != null) {
// Field failure... (2)
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(Project.class); (4)
});
1 | The response does not have data, only errors |
2 | Field that is null and has an associated error |
3 | Field that was set to null by its DataFetcher |
4 | Decode the data at the given path |
Document Source
The document for a request is a String
that may be defined in a local variable or
constant, or it may be produced through a code generated request object.
You can also create document files with extensions .graphql
or .gql
under
"graphql-documents/"
on the classpath and refer to them by file name.
For example, given a file called projectReleases.graphql
in
src/main/resources/graphql-documents
, with content:
query projectReleases($slug: ID!) {
project(slug: $slug) {
name
releases {
version
}
}
}
You can then:
Project project = graphQlClient.documentName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.retrieveSync()
.toEntity(Project.class);
1 | Load the document from "projectReleases.graphql" |
2 | Provide variable values. |
This approach also works for loading fragments for your queries.
Fragments are reusable field selection sets that avoid repetition in a request document.
For example, we can use a …releases
fragment in multiple queries:
query frameworkReleases {
project(slug: "spring-framework") {
name
...releases
}
}
query graphqlReleases {
project(slug: "spring-graphql") {
name
...releases
}
}
This fragment can be defined in a separate file for reuse:
fragment releases on Project {
releases {
version
}
}
You can then send this fragment along the query document:
Project project = graphQlClient.documentName("projectReleases") (1)
.fragmentName("releases") (2)
.retrieveSync()
.toEntity(Project.class);
1 | Load the document from "projectReleases.graphql" |
2 | Load the fragment from "releases.graphql" and append it to the document |
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
You can use the GraphQlClient
Builder to customize the
DocumentSource
for loading documents by names.
1 | The operation to perform. |
2 | The path under the "data" key in the response map to decode from. |
3 | Decode the data at the path to the target type. |
1 | The response does not have data, only errors |
2 | Field that is null and has an associated error |
3 | Field that was set to null by its DataFetcher |
4 | Decode the data at the given path |
1 | Load the document from "projectReleases.graphql" |
2 | Provide variable values. |
1 | Load the document from "projectReleases.graphql" |
2 | Load the fragment from "releases.graphql" and append it to the document |
Subscription Requests
Subscription requests require a client transport that is capable of streaming data.
You will need to create a GraphQlClient
that support this:
-
HttpGraphQlClient with Server-Sent Events
-
WebSocketGraphQlClient with WebSocket
-
RSocketGraphQlClient with RSocket
Retrieve
To start a subscription stream, use retrieveSubscription
which is similar to
retrieve for a single response but returning a stream of
responses, each decoded to some data:
Flux<String> greetingFlux = client.document("subscription { greetings }")
.retrieveSubscription("greeting")
.toEntity(String.class);
The Flux
may terminate with SubscriptionErrorException
if the subscription ends from
the server side with an "error" message. The exception provides access to GraphQL errors
decoded from the "error" message.
The Flux
may termiate with GraphQlTransportException
such as
WebSocketDisconnectedException
if the underlying connection is closed or lost. In that
case you can use the retry
operator to restart the subscription.
To end the subscription from the client side, the Flux
must be cancelled, and in turn
the WebSocket transport sends a "complete" message to the server. How to cancel the
Flux
depends on how it is used. Some operators such as take
or timeout
themselves
cancel the Flux
. If you subscribe to the Flux
with a Subscriber
, you can get a
reference to the Subscription
and cancel through it. The onSubscribe
operator also
provides access to the Subscription
.
Execute
Retrieve is only a shortcut to decode from a single path in each
response map. For more control, use the executeSubscription
method and handle each
response directly:
Flux<String> greetingFlux = client.document("subscription { greetings }")
.executeSubscription()
.map(response -> {
if (!response.isValid()) {
// Request failure...
}
ClientResponseField field = response.field("project");
if (!field.hasValue()) {
if (field.getError() != null) {
// Field failure...
}
else {
// Optional field set to null... (3)
}
}
return field.toEntity(String.class)
});
Interception
For blocking transports created with the GraphQlClient.SyncBuilder
, you create a
SyncGraphQlClientInterceptor
to intercept all requests through the client:
static class MyInterceptor implements SyncGraphQlClientInterceptor {
@Override
public ClientGraphQlResponse intercept(ClientGraphQlRequest request, Chain chain) {
// ...
return chain.next(request);
}
}
For non-blocking transports created with GraphQlClient.Builder
, you create a
GraphQlClientInterceptor
to intercept all requests through the client:
static 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);
}
}
Once the interceptor is created, register it through the client builder. For example:
URI url = ... ;
WebSocketClient client = ... ;
WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client)
.interceptor(new MyInterceptor())
.build();
DGS Codegen
As an alternative to providing the operation such as a mutation, query, or subscription as text, you can use the DGS Codegen library to generate client API classes that let you use a fluent API to define the request.
Spring for GraphQL provides DgsGraphQlClient
that wraps any GraphQlClient
and helps to prepare the request with generated client
API classes.
For example, given the following schema:
type Query {
books: [Book]
}
type Book {
id: ID
name: String
}
You can perform a request as follows:
HttpGraphQlClient client = ... ;
DgsGraphQlClient dgsClient = DgsGraphQlClient.create(client); (1)
List<Book> books = dgsClient.request(new BooksGraphQLQuery()) (2)
.projection(new BooksProjectionRoot<>().id().name()) (3)
.retrieveSync()
.toEntityList(Book.class);
1 | - Create DgsGraphQlClient by wrapping any GraphQlClient . |
2 | - Specify the operation for the request. |
3 | - Define the selection set. |
1 | - Create DgsGraphQlClient by wrapping any GraphQlClient . |
2 | - Specify the operation for the request. |
3 | - Define the selection set. |