4. Request Execution

ExecutionGraphQlService is the main Spring abstraction to call GraphQL Java to execute requests. Underlying transports, such as the Server Transports, delegate to ExecutionGraphQlService to handle requests.spring-doc.cn

The main implementation, DefaultExecutionGraphQlService, is configured with a GraphQlSource for access to the graphql.GraphQL instance to invoke.spring-doc.cn

4.1. GraphQLSource

GraphQlSource is a core Spring 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.spring-doc.cn

The default GraphQlSource builder, accessible via GraphQlSource.schemaResourceBuilder(), enables support for Reactive DataFetcher, Context Propagation, and Exception Resolution.spring-doc.cn

The Spring Boot starter initializes a GraphQlSource instance through the default GraphQlSource.Builder and also enables the following:spring-doc.cn

For further customizations, you can declare your own GraphQlSourceBuilderCustomizer beans; for example, for configuring your own ExecutionIdProvider:spring-doc.cn

@Configuration(proxyBeanMethods = false)
class GraphQlConfig {

    @Bean
    public GraphQlSourceBuilderCustomizer sourceBuilderCustomizer() {
        return (builder) ->
                builder.configureGraphQl(graphQlBuilder ->
                        graphQlBuilder.executionIdProvider(new CustomExecutionIdProvider()));
    }
}

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.spring-doc.cn

By default, the Spring Boot starter looks for schema files with extensions ".graphqls" or ".gqls" under the location classpath:graphql/**, which is typically src/main/resources/graphql. You can also use a file system location, or any location supported by the Spring Resource hierarchy, including a custom implementation that loads schema files from remote locations, from storage, or from memory.spring-doc.cn

Use classpath*:graphql/**/ to find schema files across multiple classpath locations, e.g. across multiple modules.

4.1.2. Schema Creation

By default, GraphQlSource.Builder uses the GraphQL Java GraphQLSchemaGenerator to create the graphql.schema.GraphQLSchema. This works for most applications, but if necessary, you can hook into the schema creation through the builder:spring-doc.cn

GraphQlSource.Builder builder = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> {
            // create GraphQLSchema
        })

The primary reason for this is to create the schema through a federation library.spring-doc.cn

The GraphQlSource section explains how to configure that with Spring Boot.spring-doc.cn

4.1.3. RuntimeWiringConfigurer

You can use RuntimeWiringConfigurer to register:spring-doc.cn

  • Custom scalar types.spring-doc.cn

  • Directives handling code.spring-doc.cn

  • TypeResolver, if you need to override the Default TypeResolver for a type.spring-doc.cn

  • DataFetcher for a field, although most applications will simply configure AnnotatedControllerConfigurer, which detects annotated, DataFetcher handler methods. The Spring Boot starter adds the AnnotatedControllerConfigurer by default.spring-doc.cn

Unlike web frameworks, GraphQL does not use Jackson annotations to drive JSON serialization/deserialization. Custom data types and their serialization must be described as Scalars.

The Spring Boot starter detects beans of type RuntimeWiringConfigurer and registers them in the GraphQlSource.Builder. That means in most cases, you’ll' have something like the following in your configuration:spring-doc.cn

@Configuration
public class GraphQlConfig {

    @Bean
    public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) {

        GraphQLScalarType scalarType = ... ;
        SchemaDirectiveWiring directiveWiring = ... ;
        DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single();

        return wiringBuilder -> wiringBuilder
                .scalar(scalarType)
                .directiveWiring(directiveWiring)
                .type("Query", builder -> builder.dataFetcher("book", dataFetcher));
    }
}

If you need to add a WiringFactory, e.g. to make registrations that take into account schema definitions, implement the alternative configure method that accepts both the RuntimeWiring.Builder and an output List<WiringFactory>. This allows you to add any number of factories that are then invoked in sequence.spring-doc.cn

4.1.4. 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.spring-doc.cn

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:spring-doc.cn

GraphQlSource.Builder builder = ...
ClassNameTypeResolver classNameTypeResolver = new ClassNameTypeResolver();
classNameTypeResolver.setClassNameExtractor((klass) -> {
    // Implement Custom ClassName Extractor here
});
builder.defaultTypeResolver(classNameTypeResolver);

The GraphQlSource section explains how to configure that with Spring Boot.spring-doc.cn

4.1.5. Operation Caching

GraphQL Java must parse and validate an operation before executing it. This may impact performance significantly. To avoid the need to re-parse and validate, an application may configure a PreparsedDocumentProvider that caches and reuses Document instances. The GraphQL Java docs provide more details on query caching through a PreparsedDocumentProvider.spring-doc.cn

In Spring GraphQL you can register a PreparsedDocumentProvider through GraphQlSource.Builder#configureGraphQl: .spring-doc.cn

// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer
GraphQlSource.Builder builder = ...

// Create provider
PreparsedDocumentProvider provider = ...

builder.schemaResources(..)
        .configureRuntimeWiring(..)
        .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider))

The GraphQlSource section explains how to configure that with Spring Boot.spring-doc.cn

4.1.6. Directives

The GraphQL language supports directives that "describe alternate runtime execution and type validation behavior in a GraphQL document". Directives are similar to annotations in Java but declared on types, fields, fragments and operations in a GraphQL document.spring-doc.cn

GraphQL Java provides the SchemaDirectiveWiring contract to help applications detect and handle directives. For more details, see Schema Directives in the GraphQL Java documentation.spring-doc.cn

In Spring GraphQL you can register a SchemaDirectiveWiring through a RuntimeWiringConfigurer. The Spring Boot starter detects such beans, so you might have something like:spring-doc.cn

@Configuration
public class GraphQlConfig {

     @Bean
     public RuntimeWiringConfigurer runtimeWiringConfigurer() {
          return builder -> builder.directiveWiring(new MySchemaDirectiveWiring());
     }

}
For an example of directives support check out the Extended Validation for Graphql Java library.

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.spring-doc.cn

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.spring-doc.cn

4.3. Context Propagation

Spring for GraphQL provides support to transparently propagate context from the Server Transports, through GraphQL Java, 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.spring-doc.cn

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 WebGraphQlInterceptor or DataFetcher switches to a different thread.spring-doc.cn

Spring for GraphQL supports propagating ThreadLocal values from the Servlet container thread to the thread a DataFetcher and other components invoked by GraphQL Java to execute on. To do this, an application needs to create a ThreadLocalAccessor to extract ThreadLocal values of interest:spring-doc.cn

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 the Web Endpoints section.spring-doc.cn

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 WebGraphQlInterceptor components.spring-doc.cn

4.4. Exception Resolution

A GraphQL Java application can register a DataFetcherExceptionHandler to decide how to represent exceptions from the data layer in the "errors" section of the GraphQL response.spring-doc.cn

Spring for GraphQL has a built-in DataFetcherExceptionHandler that is configured for use by the default GraphQLSource builder. It allows applications to register one or more Spring DataFetcherExceptionResolver components that are invoked sequentially until one resolves the Exception to a (possibly empty) list of graphql.GraphQLError objects.spring-doc.cn

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.spring-doc.cn

A GraphQLError can be assigned to a category via graphql.ErrorClassification. In Spring GraphQL, you can also assign via ErrorType which has the following common classifications that applications can use to categorize errors:spring-doc.cn

If an exception remains unresolved, by default it is categorized as an INTERNAL_ERROR with a generic message that includes the category name and the executionId from DataFetchingEnvironment. The message is intentionally opaque to avoid leaking implementation details. Applications can use a DataFetcherExceptionResolver to customize error details.spring-doc.cn

Unresolved exception are logged at ERROR level along with the executionId to correlate to the error sent to the client. Resolved exceptions are logged at DEBUG level.spring-doc.cn

4.4.1. Request Exceptions

The GraphQL Java engine may run into validation or other errors when parsing the request and that in turn prevent request execution. In such cases, the response contains a "data" key with null and one or more request-level "errors" that are global, i.e. not having a field path.spring-doc.cn

DataFetcherExceptionResolver cannot handle such global errors because they are raised before execution begins and before any DataFetcher is invoked. An application can use transport level interceptors to inspect and transform errors in the ExecutionResult. See examples under WebGraphQlInterceptor.spring-doc.cn

4.4.2. Subscription Exceptions

The Publisher for a subscription request may complete with an error signal in which case the underlying transport (e.g. WebSocket) sends a final "error" type message with a list of GraphQL errors.spring-doc.cn

DataFetcherExceptionResolver cannot resolve errors from a subscription Publisher, since the data DataFetcher only creates the Publisher initially. After that, the transport subscribes to the Publisher that may then complete with an error.spring-doc.cn

An application can register a SubscriptionExceptionResolver in order to resolve exceptions from a subscription Publisher in order to resolve those to GraphQL errors to send to the client.spring-doc.cn

4.5. Batch Loading

Given a Book and its Author, we can create one DataFetcher for a book and another for its author. This allows selecting books with or without authors, but it means books and authors aren’t loaded together, which is especially inefficient when querying multiple books as the author for each book is loaded individually. This is known as the N+1 select problem.spring-doc.cn

4.5.1. DataLoader

GraphQL Java provides a DataLoader mechanism for batch loading of related entities. You can find the full details in the GraphQL Java docs. Below is a summary of how it works:spring-doc.cn

  1. Register DataLoader's in the DataLoaderRegistry that can load entities, given unique keys.spring-doc.cn

  2. DataFetcher's can access DataLoader's and use them to load entities by id.spring-doc.cn

  3. A DataLoader defers loading by returning a future so it can be done in a batch.spring-doc.cn

  4. DataLoader's maintain a per request cache of loaded entities that can further improve efficiency.spring-doc.cn

4.5.2. BatchLoaderRegistry

The complete batching loading mechanism in GraphQL Java requires implementing one of several BatchLoader interface, then wrapping and registering those as DataLoaders with a name in the DataLoaderRegistry.spring-doc.cn

The API in Spring GraphQL is slightly different. For registration, there is only one, central BatchLoaderRegistry exposing factory methods and a builder to create and register any number of batch loading functions:spring-doc.cn

@Configuration
public class MyConfig {

    public MyConfig(BatchLoaderRegistry registry) {

        registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> {
                // return Mono<Map<Long, Author>
        });

        // more registrations ...
    }

}

The Spring Boot starter declares a BatchLoaderRegistry bean that you can inject into your configuration, as shown above, or into any component such as a controller in order register batch loading functions. In turn the BatchLoaderRegistry is injected into DefaultExecutionGraphQlService where it ensures DataLoader registrations per request.spring-doc.cn

By default, the DataLoader name is based on the class name of the target entity. This allows an @SchemaMapping method to declare a DataLoader argument with a generic type, and without the need for specifying a name. The name, however, can be customized through the BatchLoaderRegistry builder, if necessary, along with other DataLoader options.spring-doc.cn

For many cases, when loading related entities, you can use @BatchMapping controller methods, which are a shortcut for and replace the need to use BatchLoaderRegistry and DataLoader directly. s BatchLoaderRegistry provides other important benefits too. It supports access to the same GraphQLContext from batch loading functions and from @BatchMapping methods, as well as ensures Context Propagation to them. This is why applications are expected to use it. It is possible to perform your own DataLoader registrations directly but such registrations would forgo the above benefits.spring-doc.cn

4.5.3. Testing Batch Loading

Start by having BatchLoaderRegistry perform registrations on a DataLoaderRegistry:spring-doc.cn

BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
// perform registrations...

DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build();
batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext);

Now you can access and test individual DataLoader's as follows:spring-doc.cn

DataLoader<Long, Book> loader = dataLoaderRegistry.getDataLoader(Book.class.getName());
loader.load(1L);
loader.loadMany(Arrays.asList(2L, 3L));
List<Book> books = loader.dispatchAndJoin(); // actual loading

assertThat(books).hasSize(3);
assertThat(books.get(0).getName()).isEqualTo("...");
// ...