1. Overview
Spring GraphQL provides support for Spring applications built on GraphQL Java. It is a joint collaboration between both teams. Our shared philosophy is to be less opinionated and more focused on comprehensive and wide-ranging support.
Spring GraphQL is the successor of the GraphQL Java Spring project from the GraphQL Java team. It aims to be the foundation for all Spring, GraphQL applications.
The project is in a milestone phase towards a 1.0 release, currently, and looking for feedback. Please, use our issue tracker to report a problem, discuss a design issue, or request a feature.
To get started, please see the Boot Starter and the Samples sections.
2. Requirements
Spring GraphQL requires the following as a baseline:
-
JDK8
-
Spring Framework 5.3
-
GraphQL Java 17
-
Spring Data 2021.1.0 or later for QueryDSL or Query by Example
3. Web Transports
Spring GraphQL supports GraphQL requests over HTTP and over WebSocket.
3.1. HTTP
GraphQlHttpHandler
handles GraphQL over HTTP requests and delegates to the
Web Interception chain for request execution. There are two variants, one for
Spring MVC and one for Spring WebFlux. Both handle requests asynchronously and have
equivalent functionality, but rely on blocking vs non-blocking I/O respectively for
writing the HTTP response.
Requests must use HTTP POST with GraphQL request details included as JSON in the request body, as defined in the proposed GraphQL over HTTP specification. Once the JSON body has been successfully decoded, the HTTP response status is always 200 (OK), and any errors from GraphQL request execution appear in the "errors" section of the GraphQL response.
GraphQlHttpHandler
can be exposed as an HTTP endpoint by declaring a RouterFunction
bean and using the RouterFunctions
from Spring MVC or WebFlux to create the route. The
Boot starter does this, see Web Endpoints for details or check
GraphQlWebMvcAutoConfiguration
or GraphQlWebFluxAutoConfiguration
for example config.
The Spring GraphQL repository contains a Spring MVC HTTP sample application.
3.2. WebSocket
GraphQlWebSocketHandler
handles GraphQL over WebSocket requests based on the
protocol defined in the
graphql-ws library. The main reason to use
GraphQL over WebSocket is subscriptions which allow sending a stream of GraphQL
responses, but it can also be used for regular queries with a single response.
The handler delegates every request to the Web Interception chain for further
request execution.
GraphQL Over WebSocket Protocols
There are two such protocols, one in the subscriptions-transport-ws library and another in the graphql-ws library. The former is not active and succeeded by the latter. Read this blog post for the history. |
There are two variants of GraphQlWebSocketHandler
, one for Spring MVC and one for
Spring WebFlux. Both handle requests asynchronously and have equivalent functionality.
The WebFlux handler also uses non-blocking I/O and back pressure to stream messages,
which works well since in GraphQL Java a subscription response is a Reactive Streams
Publisher
.
The graphql-ws
project lists a number of
recipes for client use.
GraphQlWebSocketHandler
can be exposed as a WebSocket endpoint by declaring a
SimpleUrlHandlerMapping
bean and using it to map the handler to a URL path. The Boot
starter has options to enable this, see Web Endpoints for details or check
GraphQlWebMvcAutoConfiguration
or GraphQlWebFluxAutoConfiguration
for example config.
The Spring GraphQL repository contains a WebFlux WebSocket sample application.
3.3. Web Interception
HTTP and WebSocket transport handlers delegate to a common Web
interception chain for request execution. The chain consists of a sequence of
WebInterceptor
components, followed by a GraphQlService
that invokes the GraphQL
Java engine.
WebInterceptor
is as a common contract to use in both Spring MVC and WebFlux
applications. Use it to intercept requests, inspect HTTP request headers, or to register a
transformation of the graphql.ExecutionInput
:
class MyInterceptor implements WebInterceptor {
@Override
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
webInput.configureExecutionInput((executionInput, builder) -> {
Map<String, Object> map = ... ;
return builder.extensions(map).build();
});
return chain.next(webInput);
}
}
Use WebInterceptor
also to intercept responses, add HTTP response headers, or transform
the graphql.ExecutionResult
:
class MyInterceptor implements WebInterceptor {
@Override
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
return chain.next(webInput)
.map(webOutput -> {
Object data = webOutput.getData();
Object updatedData = ... ;
return webOutput.transform(builder -> builder.data(updatedData));
});
}
}
WebGraphQlHandler
provides a builder to initialize the Web interception chain. After
you build the chain, you can use the resulting WebGraphQlHandler
to initialize the HTTP
or WebSocket transport handlers. The Boot starter configures all this,
see Web Endpoints for details, or check GraphQlWebMvcAutoConfiguration
or
GraphQlWebFluxAutoConfiguration
for example config.
4. Request Execution
GraphQlService
is the main Spring GraphQL abstraction to call GraphQL Java to execute
requests. Underlying transports, such as the Web Transports, delegate to GraphQlService
to
handle requests.
The main implementation, ExecutionGraphQlService
, is a thin facade around the
invocation of graphql.GraphQL
. It is configured with a GraphQlSource
for access to
the graphql.GraphQL
instance.
4.1. GraphQLSource
GraphQlSource
is a core Spring GraphQL abstraction for access to the
graphql.GraphQL
instance to use for request execution. It provides a builder API to
initialize GraphQL Java and build a GraphQlSource
.
The default GraphQlSource
builder, accessible via GraphQlSource.builder()
, enables
support for Reactive DataFetcher
, Context Propagation, and
Exception Resolution.
4.1.1. Schema Resources
GraphQlSource.Builder
can be configured with one or more Resource
instances to be
parsed and merged together. That means schema files can be loaded from just about any
location. By default, the Spring Boot starter loads schema files
from a well-known classpath location, but you can change that to a location on the file system
via FileSystemResource
, to byte content via ByteArrayResource
, or implement a custom
Resource
that loads schema files from a remote location or storage.
4.1.2. RuntimeWiringConfigurer
You can add a RuntimeWiringConfigurer
to GraphQlSource.Builder
to register:
-
Custom scalar types.
-
Directives handling code.
-
TypeResolver
, if you need to override the DefaultTypeResolver
for a type. -
DataFetcher
for a field, although most applications will simply configureAnnotatedControllerConfigurer
, which detects annotated,DataFetcher
handler methods. The Spring Boot starter adds theAnnotatedControllerConfigurer
by default.
4.1.3. Default TypeResolver
GraphQlSource.Builder
registers ClassNameTypeResolver
as the default TypeResolver
to use for GraphQL Interfaces and Unions that don’t already have such a registration
through a RuntimeWiringConfigurer
. The purpose of
a TypeResolver
in GraphQL Java is to determine the GraphQL Object type for values
returned from the DataFetcher
for a GraphQL Interface or Union field.
ClassNameTypeResolver
tries to match the simple class name of the value to a GraphQL
Object Type and if it is not successful, it also navigates its super types including
base classes and interfaces, looking for a match. ClassNameTypeResolver
provides an
option to configure a name extracting function along with Class
to GraphQL Object type
name mappings that should help to cover more corner cases.
4.2. Reactive DataFetcher
The default GraphQlSource
builder enables support for a DataFetcher
to return Mono
or Flux
which adapts those to a CompletableFuture
where Flux
values are aggregated
and turned into a List, unless the request is a GraphQL subscription request,
in which case the return value remains a Reactive Streams Publisher
for streaming
GraphQL responses.
A reactive DataFetcher
can rely on access to Reactor context propagated from the
transport layer, such as from a WebFlux request handling, see
WebFlux Context.
4.3. Context Propagation
Spring GraphQL provides support to transparently propagate context from the Web Transports,
through the GraphQL engine, and to DataFetcher
and other components it invokes.
This includes both ThreadLocal
context from the Spring MVC request handling thread and
Reactor Context
from the WebFlux processing pipeline.
4.3.1. WebMvc
A DataFetcher
and other components invoked by GraphQL Java may not always execute on
the same thread as the Spring MVC handler, for example if an asynchronous
WebInterceptor
or DataFetcher
switches to a different thread.
Spring GraphQL supports propagating ThreadLocal
values from the Servlet container
thread to the thread a DataFetcher
and other components invoked by the GraphQL engine
execute on. To do this, an application needs to create a ThreadLocalAccessor
to extract
ThreadLocal
values of interest:
public class RequestAttributesAccessor implements ThreadLocalAccessor {
private static final String KEY = RequestAttributesAccessor.class.getName();
@Override
public void extractValues(Map<String, Object> container) {
container.put(KEY, RequestContextHolder.getRequestAttributes());
}
@Override
public void restoreValues(Map<String, Object> values) {
if (values.containsKey(KEY)) {
RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY));
}
}
@Override
public void resetValues(Map<String, Object> values) {
RequestContextHolder.resetRequestAttributes();
}
}
A ThreadLocalAccessor
can be registered in the WebGraphHandler
builder. The Boot starter detects beans of this type and automatically registers them for
Spring MVC application, see Web Endpoints.
4.3.2. WebFlux
A Reactive DataFetcher
can rely on access to Reactor context that
originates from the WebFlux request handling chain. This includes Reactor context
added by WebInterceptor components.
4.4. Exception Resolution
GraphQL Java applications can register a DataFetcherExceptionHandler
to decide how to
represent exceptions from the data layer in the "errors" section of the GraphQL response.
Spring GraphQL has a built-in DataFetcherExceptionHandler
that is configured for use
by the GraphQLSource
builder. It enables applications to register one or
more Spring DataFetcherExceptionResolver
components that are invoked sequentially
until one resolves the Exception
to a list of graphql.GraphQLError
objects.
DataFetcherExceptionResolver
is an asynchronous contract. For most implementations, it
would be sufficient to extend DataFetcherExceptionResolverAdapter
and override
one of its resolveToSingleError
or resolveToMultipleErrors
methods that
resolve exceptions synchronously.
A GraphQLError
can be assigned an graphql.ErrorClassification
. Spring GraphQL
defines an ErrorType
enum with common, error classification categories:
-
BAD_REQUEST
-
UNAUTHORIZED
-
FORBIDDEN
-
NOT_FOUND
-
INTERNAL_ERROR
Applications can use this to classify errors. If an error remains unresolved, by
default it is marked as INTERNAL_ERROR
.
4.5. Batching
Given a Book
and its Author
, we can create one DataFetcher
for books and another
for the author of a book. This means books and authors aren’t automatically loaded
together, which enables queries to select the subset of data they need. However, loading
multiple books, results in loading each author individually, and this is a performance
issue known as the N+1 select problem.
GraphQL Java provides a batching feature that allows related entities, such as the authors for all books, to be loaded together. This is how the underlying mechanism works in GraphQL Java:
-
For each request, an application can register a batch loading function as a
DataLoader
in theDataLoaderRegistry
to assist with loading instances of a given entity, such asAuthor
from a set of unique keys. -
A
DataFetcher
can access theDataLoader
for the entity and use it to load entity instances; for example the authorDataFetcher
obtains the authorId from theBook
parent object, and uses it to load theAuthor
. -
DataLoader
does not load the entity immediately but rather returns a future, and defers until it is ready to batch load all related entities as one. -
DataLoader
additionally maintains a cache of previously loaded entities that can further improve efficiency when the same entity is in multiple places of the response.
Spring GraphQL provides:
-
BatchLoaderRegistry
that accepts and stores registrations of batch loading functions; This is used inExecutionGraphQlService
to makeDataLoader
registrations per request. -
DataLoader argument for
@SchemaMapping
methods to access theDataLoader
for the field type. -
@BatchMapping data controller methods that provide a shortcut and avoid the need to use
DataLoader
directly.
The Spring Boot starter declares a BatchLoaderRegistry bean, so that applications can simply autowire the registry into their controllers in order to register batch loading functions for entities.
5. Data Integration
5.1. Querydsl
Spring GraphQL supports use of Querydsl to fetch data through the Spring Data Querydsl extension. Querydsl provides a flexible yet typesafe approach to express query predicates by generating a meta-model using annotation processors.
For example, declare a repository as QuerydslPredicateExecutor
:
public interface AccountRepository extends Repository<Account, Long>,
QuerydslPredicateExecutor<Account> {
}
Then use it to create a DataFetcher
:
// For single result queries
DataFetcher<Account> dataFetcher =
QuerydslDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QuerydslDataFetcher.builder(repository).many();
The DataFetcher
builds a Querydsl Predicate
from GraphQL request parameters, and
uses it to fetch data. Spring Data supports QuerydslPredicateExecutor
for JPA,
MongoDB, and LDAP.
If the repository is ReactiveQuerydslPredicateExecutor
, the builder returns
DataFetcher<Mono<Account>>
or DataFetcher<Flux<Account>>
. Spring Data supports this
variant for MongoDB.
5.1.1. Build Setup
To configure Querydsl in your build, follow the official reference documentation:
For example:
dependencies {
//...
annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa",
'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final',
'javax.annotation:javax.annotation-api:1.3.2'
}
compileJava {
options.annotationProcessorPath = configurations.annotationProcessor
}
<dependencies>
<!-- ... -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jpa</classifier>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<plugins>
<!-- Annotation processor configuration -->
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>${apt-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
The webmvc-http sample uses Querydsl for
artifactRepositories
.
5.1.2. Customizations
QuerydslDataFetcher
supports customizing how GraphQL arguments are bound onto properties
to create a Querydsl Predicate
. By default, arguments are bound as "is equal to" for
each available property. To customize that, you can use QuerydslDataFetcher
builder
methods to provide a QuerydslBinderCustomizer
.
A repository may itself be an instance of QuerydslBinderCustomizer
. This is auto-detected
and transparently applied during Auto Registration. However, when manually
building a QuerydslDataFetcher
you will need to use builder methods to apply it.
QuerydslDataFetcher
supports
interface and DTO projections
to transform query results before returning these for further GraphQL processing.
5.1.3. Auto Registration
If a repository is annotated with @GraphQlRepository
, it is automatically registered
for queries that do not already have a registered DataFetcher
and whose return type
matches that of the repository domain type. This includes both single value and multi-value
queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the typeName
attribute of
@GraphQlRepository
to specify the target GraphQL type name.
Auto-registration detects if a given repository implements QuerydslBinderCustomizer
and
transparently applies that through QuerydslDataFetcher
builder methods.
Auto-registration is performed through a GraphQLTypeVisitor
which can be obtained from
QuerydslDataFetcher
. The Boot starter automatically
detects @GraphQlRepository
beans and applies the GraphQLTypeVisitor
.
5.2. Query by Example
Spring Data supports the use of Query by Example to fetch data. Query by Example (QBE) is a simple querying technique that does not require you to write queries through store-specific query languages.
Start by declaring a repository that is QueryByExampleExecutor
:
public interface AccountRepository extends Repository<Account, Long>,
QueryByExampleExecutor<Account> {
}
Use QueryByExampleDataFetcher
to turn the repository into a DataFecher
:
// For single result queries
DataFetcher<Account> dataFetcher =
QueryByExampleDataFetcher.builder(repository).single();
// For multi-result queries
DataFetcher<Iterable<Account>> dataFetcher =
QueryByExampleDataFetcher.builder(repository).many();
The DataFetcher
uses the GraphQL arguments map to create the domain type of the
repository and use that as the example object to fetch data with. Spring Data supports
QueryByExampleDataFetcher
for JPA, MongoDB, Neo4j, and Redis.
If the repository is ReactiveQueryByExampleExecutor
, the builder returns
DataFetcher<Mono<Account>>
or DataFetcher<Flux<Account>>
. Spring Data supports this
variant for MongoDB, Neo4j, Redis, and R2dbc.
5.2.1. Build Setup
Query by Example is already included in the Spring Data modules for the data stores where it is supported, so no extra setup is required to enable it.
5.2.2. Customizations
QueryByExampleDataFetcher
supports
interface and DTO projections
to transform query results before returning these for further GraphQL processing.
5.2.3. Auto Registration
If a repository is annotated with @GraphQlRepository
, it is automatically registered
for queries that do not already have a registered DataFetcher
and whose return type
matches that of the repository domain type. This includes both single value and multi-value
queries.
By default, the name of the GraphQL type returned by the query must match the simple name
of the repository domain type. If needed, you can use the typeName
attribute of
@GraphQlRepository
to specify the target GraphQL type name.
Auto-registration is performed through a GraphQLTypeVisitor
which can be obtained from
QueryByExampleDataFetcher
. The Boot starter automatically
detects @GraphQlRepository
beans and applies the GraphQLTypeVisitor
.
6. Annotated Controllers
Spring GraphQL provides an annotation-based programming model where @Controller
components use annotations to declare handler methods with flexible method signatures to
fetch the data for specific GraphQL fields. For example:
@Controller
public class GreetingController {
@QueryMapping (1)
public String hello() { (2)
return "Hello, world!";
}
}
1 | Bind this method to a query, i.e. a field under the Query type. |
2 | Determine the query from the method name if not declared on the annotation. |
Spring GraphQL uses RuntimeWiring.Builder
to register the above handler method as a
graphql.schema.DataFetcher
for the query named "hello".
6.1. Declaration
You can define @Controller
beans as standard Spring bean definitions. The
@Controller
stereotype allows for auto-detection, aligned with Spring general
support for detecting @Controller
and @Component
classes on the classpath and
auto-registering bean definitions for them. It also acts as a stereotype for the annotated
class, indicating its role as a data fetching component in a GraphQL application.
AnnotatedControllerConfigurer
detects @Controller
beans and registers their
annotated handler methods as DataFetcher
s via RuntimeWiring.Builder
. It is an
implementation of RuntimeWiringConfigurer
which can be added to GraphQlSource.Builder
.
The Spring Boot starter automatically declares AnnotatedControllerConfigurer
as a bean
and adds all RuntimeWiringConfigurer
beans to GraphQlSource.Builder
and that enables
support for annotated DataFetcher
s, see RuntimeWiring.
6.2. @SchemaMapping
The @SchemaMapping
annotation maps a handler method to a field in the GraphQL schema
and declares it to be the DataFetcher
for that field. The annotation can specify the
parent type name, and the field name:
@Controller
public class BookController {
@SchemaMapping(typeName="Book", field="author")
public Author getAuthor(Book book) {
// ...
}
}
The @SchemaMapping
annotation can also leave out those attributes, in which case the
field name defaults to the method name, while the type name defaults to the simple class
name of the source/parent object injected into the method. For example, the below
defaults to type "Book" and field "author":
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
The @SchemaMapping
annotation can be declared at the class level to specify a default
type name for all handler methods in the class.
@Controller
@SchemaMapping(typeName="Book")
public class BookController {
// @SchemaMapping methods for fields of the "Book" type
}
@QueryMapping
, @MutationMapping
, and @SubscriptionMapping
are meta annotations that
are themselves annotated with @SchemaMapping
and have the typeName preset to Query
,
Mutation
, or Subscription
respectively. Effectively, these are shortcut annotations
for fields under the Query, Mutation, and Subscription types respectively. For example:
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
@SubscriptionMapping
public Flux<Book> newPublications() {
// ...
}
}
@SchemaMapping
handler methods have flexible signatures and can choose from a range of
method arguments and return values..
6.2.1. Method Signature
Schema mapping handler methods can have any of the following method arguments:
Method Argument | Description |
---|---|
|
For access to field arguments with conversion.
See |
|
For access to field arguments through a project interface.
See |
Source |
For access to the source (i.e. parent/container) instance of the field. See Source. |
|
For access to a |
|
For access to a value from the localContext, if it is an instance of |
|
For access to the context from the |
|
Obtained from Spring Security context, if available. |
|
For access to the selection set for the query through the |
|
For access to the |
|
For direct access to the underlying |
Schema mapping handler methods can return any value, including Reactor Mono
and
Flux
as described in Reactive DataFetcher
.
6.2.2. @Argument
In GraphQL Java, the DataFetchingEnvironment
provides access to field-specific argument
values. The arguments are available as simple scalar values such as String, or as a Map
of values for more complex input, or a List
of values.
Use @Argument
to access an argument for the field that maps to the handler method. You
can declare such a method parameter to be of any type.
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument Long id) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInput bookInput) {
// ...
}
}
You can explicitly specify the argument name, for example @Argument("bookInput")
, or if
it not specified, it defaults to the method parameter name, but this requires the
-parameters
compiler flag with Java 8+ or debugging information from the compiler.
The @Argument
annotation does not have a "required" flag, nor the option to specify a
default value. Both of these can be specified at the GraphQL schema level and are enforced
by the GraphQL Engine.
You can use @Argument
on a Map<String, Object>
argument, to obtain all argument
values. The name attribute on @Argument
must not be set.
6.2.3. @Argument
validation
If a Bean Validation
Validator
(or typically, a LocalValidatorFactoryBean
) bean is present in the application context,
the AnnotatedControllerConfigurer
will auto-detect it and configure support for validation.
Controller arguments annotated with @Valid
and @Validated
are then validated before method invocation.
Bean Validation lets you declare constraints on types, as the following example shows:
public class BookInput {
@NotNull
private String title;
@NotNull
@Size(max=13)
private String isbn;
}
We can then mark our argument for validation with @Valid
:
@Controller
public class BookController {
@MutationMapping
public Book addBook(@Argument @Valid BookInput bookInput) {
// ...
}
}
If an error occurs during validation, a ConstraintViolationException
is thrown and can be
later resolved with a custom DataFetcherExceptionResolver
.
Unlike Spring MVC, handler method signatures do not support the injection of |
6.2.4. @ProjectPayload
Interface
As an alternative to using complete Objects with @Argument
,
you can also use a projection interface to access GraphQL request arguments through a
well-defined, minimal interface. Argument projections are provided by
Spring Data’s Interface projections
when Spring Data is on the class path.
To make use of this, create an interface annotated with @ProjectedPayload
and declare
it as a controller method parameter. If the parameter is annotated with @Argument
,
it applies to an individual argument within the DataFetchingEnvironment.getArguments()
map. When declared without @Argument
, the projection works on top-level arguments in
the complete arguments map.
For example:
@Controller
public class BookController {
@QueryMapping
public Book bookById(BookIdProjection bookId) {
// ...
}
@MutationMapping
public Book addBook(@Argument BookInputProjection bookInput) {
// ...
}
}
@ProjectedPayload
interface BookIdProjection {
Long getId();
}
@ProjectedPayload
interface BookInputProjection {
String getName();
@Value("#{target.author + ' ' + target.name})
String getAuthorAndName();
}
6.2.5. Source
In GraphQL Java, the DataFetchingEnvironment
provides access to the source (i.e.
parent/container) instance of the field. To access this, simply declare a method parameter
of the expected target type.
@Controller
public class BookController {
@SchemaMapping
public Author author(Book book) {
// ...
}
}
The source method argument also helps to determine the type name for the mapping.
If the simple name of the Java class matches the GraphQL type, then there is no need to
explicitly specify the type name in the @SchemaMapping
annotation.
A |
6.2.6. DataLoader
When you register a batch loading function for an entity, as explained in
Batching, you can access the DataLoader
for the entity by declaring a
method argument of type DataLoader
and use it to load the entity:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
By default, BatchLoaderRegistry
uses the full class name of the value type (e.g. the
class name for Author
) for the key of the registration, and therefore simply declaring
the DataLoader
method argument with generic types provides enough information
to locate it in the DataLoaderRegistry
. As a fallback, the DataLoader
method argument
resolver will also try the method argument name as the key but typically that should not
be necessary.
For straight-forward cases where the |
6.3. @BatchMapping
Batching addresses the N+1 select problem through the use of an
org.dataloader.DataLoader
to defer the loading of individual entity instances, so they
can be loaded together. For example:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
// return Map<Long, Author>
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
For the straight-forward case of loading an associated entity, shown above, the
@SchemaMapping
method does nothing more than delegate to the DataLoader
. This is
boilerplate that can be avoided with a @BatchMapping
method. For example:
@Controller
public class BookController {
@BatchMapping
public Mono<Map<Book, Author>> author(List<Book> books) {
// ...
}
}
The above becomes a batch loading function in the BatchLoaderRegistry
where keys are Book
instances and the loaded values their authors. In addition, a
DataFetcher
is also transparently bound to the author
field of the type Book
, which
simply delegates to the DataLoader
for authors, given its source/parent Book
instance.
To be used as a unique key, |
By default, the field name defaults to the method name, while the type name defaults to
the simple class name of the input List
element type. Both can be customized through
annotation attributes. The type name can also be inherited from a class level
@SchemaMapping
.
6.3.1. Method Signature
Batch mapping methods support the following arguments:
Method Argument | Description |
---|---|
|
The source/parent objects. |
|
Obtained from Spring Security context, if available. |
|
For access to a value from the |
|
For access to the context from the |
|
The environment that is available in GraphQL Java to a
|
Batch mapping methods can return:
Return Type | Description |
---|---|
|
A map with parent objects as keys, and batch loaded objects as values. |
|
A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method. |
|
Imperative variants, e.g. without remote calls to make. |
7. Security
The path to a Web GraphQL endpoint can be secured with HTTP URL security to ensure that only authenticated users can access it. This does not, however, differentiate among different GraphQL requests on such a shared endpoint on a single URL.
To apply more fine-grained security, add Spring Security annotations such as
@PreAuthorize
or @Secured
to service methods involved in fetching specific parts of
the GraphQL response. This should work due to Context Propagation that aims to make
Security, and other context, available at the data fetching level.
The Spring GraphQL repository contains samples for Spring MVC and for WebFlux.
8. Testing
It’s possible to test GraphQL requests with Spring’s WebTestClient
, just sending and
receiving JSON, but a number of GraphQL specific details make this approach more
cumbersome than is necessary.
To get the full testing support, you’ll need to add the spring-graphql-test
dependdency
in your build:
dependencies {
// ...
testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0-M4'
}
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<version>1.0.0-M4</version>
<scope>test</scope>
</dependency>
</dependencies>
8.1. GraphQlTester
GraphQlTester
defines a workflow to test GraphQL requests with the following benefits:
-
Verify no unexpected errors under the "errors" key in the response.
-
Decode under the "data" key in the response.
-
Use JsonPath to decode different parts of the response.
-
Test subscriptions.
To create GraphQlTester
, you only need a GraphQlService
, and no transport:
GraphQlSource graphQlSource = GraphQlSource.builder()
.schemaResources(...)
.runtimeWiringConfigurer(...)
.build();
GraphQlService graphQlService = new ExecutionGraphQlService(graphQlSource);
GraphQlTester graphQlTester = GraphQlTester.builder(graphQlService).build();
8.2. WebGraphQlTester
WebGraphQlTester
extends GraphQlTester
to add a workflow and configuration specific
to Web Transports, and it always verifies GraphQL HTTP responses are 200 (OK).
To create WebGraphQlTester
, you need one of the following inputs:
-
WebTestClient
— perform requests as an HTTP client, either against HTTP handlers without a server, or against a live server. -
WebGraphQlHandler
— perform requests through the Web Interception chain used by both HTTP and WebSocket handlers, which in effect is testing without a Web framework. One reason to use this is for Subscriptions.
For Spring WebFlux without a server, you can point to your Spring configuration:
ApplicationContext context = ... ;
WebTestClient client =
WebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
WebGraphQlTester tester = WebGraphQlTester.builder(client).build();
For Spring MVC without a server, the same but using MockMvcWebTestClient
:
WebApplicationContext context = ... ;
WebTestClient client =
MockMvcWebTestClient.bindToApplicationContext(context)
.configureClient()
.baseUrl("/graphql")
.build();
WebGraphQlTester tester = WebGraphQlTester.builder(client).build();
To test against a live, running server:
WebTestClient client =
WebTestClient.bindToServer()
.baseUrl("http://localhost:8080/graphql")
.build();
WebGraphQlTester tester = WebGraphQlTester.builder(client).build();
WebGraphQlTester
supports setting HTTP request headers and access to HTTP response
headers. This may be useful to inspect or set security related headers.
this.graphQlTester.queryName("{ myQuery }")
.httpHeaders(headers -> headers.setBasicAuth("rob", "..."))
.execute()
.httpHeadersSatisfy(headers -> {
// check response headers
})
.path("myQuery.field1").entity(String.class).isEqualTo("value1")
.path("myQuery.field2").entity(String.class).isEqualTo("value2");
You can also set default request headers at the builder level:
WebGraphQlTester tester = WebGraphQlTester.builder(client)
.defaultHttpHeaders(headers -> headers.setBasicAuth("rob", "..."))
.build();
8.3. Queries
Below is an example query test using JsonPath to extract all release versions in the GraphQL response.
String query = "{" +
" project(slug:\"spring-framework\") {" +
" releases {" +
" version" +
" }"+
" }" +
"}";
graphQlTester.query(query)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
The JsonPath is relative to the "data" section of the response.
You can also create query files with extensions .graphql
or .gql
under "graphql/"
on
the classpath and refer to them by file name. For example, given a file called
projectReleases.graphql
in src/main/resources/graphql
, with content:
query projectReleases($slug: ID!) {
project(slug: $slug) {
releases {
version
}
}
}
You can write the same test as follows:
graphQlTester.queryName("projectReleases") (1)
.variable("slug", "spring-framework") (2)
.execute()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
1 | Refer to the query in the file named "projectReleases". |
2 | Set the slug variable. |
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion. |
8.4. Errors
Tests cannot use verify data, if there are errors under the "errors" key in the response
has errors. If necessary to ignore an error, use an error filter Predicate
:
graphQlTester.query(query)
.execute()
.errors()
.filter(error -> ...)
.verify()
.path("project.releases[*].version")
.entityList(String.class)
.hasSizeGreaterThan(1);
An error filter can be registered globally and apply to all tests:
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client)
.errorFilter(error -> ...)
.build();
Or inspect all errors directly and that also marks them as filtered:
graphQlTester.query(query)
.execute()
.errors()
.satisfy(errors -> {
// ...
});
If a request does not have any response data (e.g. mutation), use executeAndVerify
instead of execute
to verify there are no errors in the response:
graphQlTester.query(query).executeAndVerify();
8.5. Subscriptions
The executeSubscription
method defines a workflow specific to subscriptions which return
a stream of responses instead of a single response.
To test subscriptions, you can create GraphQlTester
with a GraphQlService
, which
calls graphql.GraphQL
directly and that returns a stream of responses:
GraphQlService service = ... ;
GraphQlTester graphQlTester = GraphQlTester.builder(service).build();
Flux<String> result = graphQlTester.query("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class); // decode each response
The StepVerifier
from Project Reactor is useful to verify a stream:
Flux<String> result = graphQlTester.query("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class);
StepVerifier.create(result)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
To test with the Web Interception chain, you can create WebGraphQlTester
with a
WebGraphQlHandler
:
GraphQlService service = ... ;
WebGraphQlHandler handler = WebGraphQlHandler.builder(service)
.interceptor((input, next) -> next.handle(input))
.build();
WebGraphQlTester graphQlTester = WebGraphQlTester.builder(handler).build();
Currently, Spring GraphQL does not support testing with a WebSocket client, and it cannot be used for integration test of GraphQL over WebSocket requests.
9. Boot Starter
This projects builds on Boot 2.6.x, but it should be compatible with the latest Boot 2.4.x. For QueryDSL-related features, Spring Data 2021.1.0 or later is required.
9.1. Project Setup
To create a project, go to start.spring.io and select starter(s) for the GraphQL transports you want to use:
Starter | Transport | Implementation |
---|---|---|
|
HTTP |
Spring MVC |
|
WebSocket |
WebSocket for Servlet apps |
|
HTTP, WebSocket |
Spring WebFlux |
In the generated project, add graphql-spring-boot-starter
manually:
dependencies {
// Spring GraphQL Boot starter
implementation 'org.springframework.experimental:graphql-spring-boot-starter:1.0.0-M4'
// ...
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' } // Spring milestones
maven { url 'https://repo.spring.io/snapshot' } // Spring snapshots
}
<dependencies>
<!-- Spring GraphQL Boot starter -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>1.0.0-M4</version>
</dependency>
<!-- ... -->
</dependencies>
<!-- For Spring project milestones or snapshot releases -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
Boot Starter Group Id
The Boot starter will move from the Spring GraphQL repository to the Spring Boot
repository, after Spring Boot 2.6 is released. The group id for the starter will then
change from |
9.2. Schema
By default, the Boot starter checks in src/main/resources/graphql
for GraphQL schema
files with extensions ".graphqls" or ".gqls". To customize this, use the following:
spring.graphql.schema.locations=classpath:graphql/
spring.graphql.schema.fileExtensions=.graphqls, .gqls
The GraphQL schema can be viewed at HTTP GET /graphql/schema. This is off by default and needs to be enabled:
spring.graphql.schema.printer.enabled=false
9.3. RuntimeWiring
The GraphQL Java RuntimeWiring.Builder
can be used to register custom scalar types,
directives, type resolvers, DataFetcher
s, and more. You can declare RuntimeWiringConfigurer
beans in your Spring config to get access to the RuntimeWiring.Builder
. The Boot
starter detects such beans and adds them to GraphQlSource.Builder.
Typically, however, applications will not implement DataFetcher
directly and will
instead create annotated controllers. The Boot
starter declares a RuntimeWiringConfigurer
called AnnotatedControllerConfigurer
that
detects @Controller
classes with annotated handler methods and registers those as
DataFetcher
s.
9.4. Querydsl Repositories
Spring Data repositories that extend QuerydslPredicateExecutor
or
ReactiveQuerydslPredicateExecutor
and are annotated with @GraphQlRepository
are
detected and considered as candidates for DataFetcher
auto registration for matching top-level queries.
9.5. Query by Example Repositories
Spring Data repositories that extend QueryByExampleExecutor
or
ReactiveQueryByExampleExecutor
and are annotated with @GraphQlRepository
are
detected and considered as candidates for DataFetcher
auto registration for matching top-level queries.
9.6. Web Endpoints
The GraphQL HTTP endpoint is at HTTP POST "/graphql" by default. The path can be customized:
spring.graphql.path=/graphql
The GraphQL WebSocket endpoint supports WebSocket handshakes at "/graphql" by default. The below shows the properties that apply for WebSocket handling:
spring.graphql.websocket.path=/graphql
# Time within which a "CONNECTION_INIT" message must be received from the client
spring.graphql.websocket.connection-init-timeout=60s
The GraphQL WebSocket endpoint is off by default. To enable it:
-
For a Servlet application, add the WebSocket starter
spring-boot-starter-websocket
. -
For a WebFlux application, set the
spring.graphql.websocket.path
application property.
Declare a WebInterceptor
bean to have it registered in the
Web Interception for GraphQL over HTTP and WebSocket
requests.
Declare a ThreadLocalAccessor
bean to assist with the propagation of ThreadLocal
values of interest in Spring MVC.
9.7. CORS
Spring MVC and Spring WebFlux support CORS (Cross-Origin Resource Sharing) requests. CORS is a critical part of the web config for GraphQL applications that are accessed from browsers using different domains.
The Boot starter supports the following CORS properties:
spring.graphql.cors.allowed-origins=https://example.org # Comma-separated list of origins to allow. '*' allows all origins.
spring.graphql.cors.allowed-origin-patterns= # Comma-separated list of origin patterns like 'https://*.example.com' to allow.
spring.graphql.cors.allowed-methods=GET,POST # Comma-separated list of methods to allow. '*' allows all methods.
spring.graphql.cors.allowed-headers= # Comma-separated list of headers to allow in a request. '*' allows all headers.
spring.graphql.cors.exposed-headers= # Comma-separated list of headers to include in a response.
spring.graphql.cors.allow-credentials= # Whether credentials are supported. When not set, credentials are not supported.
spring.graphql.cors.max-age=1800s # How long the response from a pre-flight request can be cached by clients.
For more information about the properties and their meaning, check out the GraphQlCorsProperties Javadoc. |
9.8. Exceptions
Spring GraphQL enables applications to register one or more Spring
DataFetcherExceptionResolver
components that are invoked sequentially until one
resolves the Exception to a list of graphql.GraphQLError
objects. See
Exception Resolution for details.
The Boot starter detects beans of type DataFetcherExceptionResolver
and registers them
automatically with the GraphQlSource.Builder
.
9.9. BatchLoaderRegistry
Spring GraphQL supports the GraphQL Java batch feature and provides
a BatchLoaderRegistry
to store registrations of batch loading functions. The Boot
starter declares a BatchLoaderRegistry
bean and configures the ExecutionGraphQlService
with it so that applications can simply autowire the registry into their controllers and
register batch loading functions.
For example:
@Controller
public class BookController {
public BookController(BatchLoaderRegistry registry) {
registry.forTypePair(Long.class, Author.class).registerBatchLoader((authorIds, env) -> {
// load authors
});
}
@SchemaMapping
public CompletableFuture<Author> author(Book book, DataLoader<Long, Author> loader) {
return loader.load(book.getAuthorId());
}
}
9.10. GraphiQL
The Spring Boot starter includes a GraphiQL page that is exposed at "/graphiql" by default. You can configure this as follows:
spring.graphql.graphiql.enabled=true
spring.graphql.graphiql.path=/graphiql
9.11. Metrics
When the starter spring-boot-starter-actuator
is present on the classpath, metrics for
GraphQL requests are collected. You can disable metrics collection as follows:
management.metrics.graphql.autotime.enabled=false
Metrics can be exposed with an Actuator web endpoint. The following sections assume that its exposure is enabled in your application configuration, as follows:
management.endpoints.web.exposure.include=health,metrics,info
9.11.1. Request Timer
A Request metric timer is available at /actuator/metrics/graphql.request
.
Tag | Description | Sample values |
---|---|---|
outcome |
Request outcome |
"SUCCESS", "ERROR" |
9.11.2. DataFetcher
Timer
A DataFetcher
metric timer is available at /actuator/metrics/graphql.datafetcher
.
Tag | Description | Sample values |
---|---|---|
path |
data fetcher path |
"Query.project" |
outcome |
data fetching outcome |
"SUCCESS", "ERROR" |
9.11.3. DataFetcher
Distribution Summary
A distribution summary
that counts the number of non-trivial DataFetcher
calls made per request.
This metric is useful for detecting "N+1" data fetching issues and consider batch loading;
it provides the "TOTAL"
number of data fetcher calls made over the "COUNT"
of recorded requests,
as well as the "MAX"
calls made for a single request over the considered period.
The distribution is available at /actuator/metrics/graphql.request.datafetch.count
.
More options are available for configuring distributions with application properties.
9.12. Testing
Spring GraphQL offers many ways to test your application: with or without a live server, with a Web client or without, with a Web transport or testing directly against the GraphQL Java engine. Tests rely on WebGraphQlTester, so be sure to become familiar with using it.
The Spring Boot starter will help you to configure the testing infrastructure; to start, add the following to your classpath:
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0-M4'
// Also add this, unless spring-boot-starter-webflux is also present
testImplementation 'org.springframework:spring-webflux'
// ...
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' } // Spring milestones
maven { url 'https://repo.spring.io/snapshot' } // Spring snapshots
}
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<version>1.0.0-M4</version>
<scope>test</scope>
</dependency>
<!-- Also add this, unless "spring-boot-starter-webflux" is also present -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- For Spring project milestones or snapshot releases -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
The following sections cover a range of options for testing a Spring GraphQL application.
9.12.1. GraphQL Slice Tests
Use @GraphQlTest
on a test class to create GraphQL tests focused on GraphQL request
execution, without involving a Web layer, and loading only a subset of the application
configuration.
By default, @GraphQlTest
limits scanning to the following beans:
-
@Controller
-
RuntimeWiringConfigurer
-
JsonComponent
-
Converter
-
GenericConverter
Use the controllers
attribute of @GraphQlTest
to specify a controller class, or to
list all data controllers required to perform requests in a test class. Leaving it empty,
includes all controllers.
To add collaborator and/or other components to a test class, use one of the following:
-
@MockBean
fields in the test class. -
@Import
an@Configuration
class into the test class. -
Create a
@TestConfiguration
nested class. -
Broaden the component scan via
includeFilters
on@GraphQlTest
.
To add properties, use the properties
attribute of @GraphQlTest
, or add
@EnableConfigurationProperties
on the test class.
|
@GraphQlTest(BookController.class)
public class BookControllerTests {
@Autowired
private GraphQlTester graphQlTester;
@MockBean
private BookRepository bookRepository;
@Test
void bookdByIdShouldReturnSpringBook() {
given(this.bookRepository.findById(42L)).willReturn(new Book(42L, "Spring GraphQL"));
String query = //
graphQlTester.query(query).execute()
.path("data.bookById.name").entity(String.class).isEqualTo("Spring GraphQL");
}
}
This mode is useful to test subscriptions without WebSocket.
@GraphQlTest(GreetingController.class)
public class GreetingControllerTests {
@Autowired
private GraphQlTester graphQlTester;
@Test
void subscription() {
Flux<String> result = this.graphQlTester.query("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class);
// Use StepVerifier from "reactor-test" to verify the stream...
StepVerifier.create(result)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
}
}
GraphQlService
performS the above request by calling directly the GraphQL Java engine,
which returns a Reactive Streams Publisher
.
9.12.2. Client and Mock Server Tests
You can write fuller integration tests with a Web client and a Web framework, Spring MVC or WebFlux, but without running a live server, i.e. using a mock request and response.
For GraphQL over HTTP with a mock server:
@SpringBootTest
@AutoConfigureWebGraphQlTester
public class MockWebGraphQlTests {
@Autowired
private WebGraphQlTester graphQlTester;
}
9.12.3. Live Server Tests
You can also run tests against the full application infrastructure with a live server.
Just like REST endpoints testing,
you can use a WebEnvironment.RANDOM_PORT
environment and test queries using WebGraphQlTester
.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MockMvcGraphQlTests {
@Autowired
private WebGraphQlTester graphQlTester;
}
10. Samples
This Spring GraphQL repository contains sample applications for various scenarios.
You can run those by cloning this repository and running main application classes from your IDE or by typing the following on the command line:
$ ./gradlew :samples:{sample-directory-name}:bootRun