6. Annotated Controllers

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

@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 for GraphQL uses RuntimeWiring.Builder to register the above handler method as a graphql.schema.DataFetcher for the query named "hello".spring-doc.cn

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

AnnotatedControllerConfigurer detects @Controller beans and registers their annotated handler methods as DataFetchers 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 DataFetchers, see the GraphQL RuntimeWiring section in the Boot starter documentation.spring-doc.cn

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

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

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

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

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

6.2.1. Method Signature

Schema mapping handler methods can have any of the following method arguments:spring-doc.cn

Method Argument Description

@Argumentspring-doc.cn

For access to a named field argument bound to a higher-level, typed Object. See @Argument.spring-doc.cn

@Argumentsspring-doc.cn

For access to all field arguments bound to a higher-level, typed Object. See @Arguments.spring-doc.cn

@Argument Map<String, Object>spring-doc.cn

For access to the raw map of arguments, where @Argument does not have a name attribute.spring-doc.cn

@Arguments Map<String, Object>spring-doc.cn

For access to the raw map of arguments.spring-doc.cn

@ProjectedPayload Interfacespring-doc.cn

For access to field arguments through a project interface. See @ProjectedPayload Interface.spring-doc.cn

"Source"spring-doc.cn

For access to the source (i.e. parent/container) instance of the field. See Source.spring-doc.cn

DataLoaderspring-doc.cn

For access to a DataLoader in the DataLoaderRegistry. See DataLoader.spring-doc.cn

@ContextValuespring-doc.cn

For access to an attribute from the main GraphQLContext in DataFetchingEnvironment.spring-doc.cn

@LocalContextValuespring-doc.cn

For access to an attribute from the local GraphQLContext in DataFetchingEnvironment.spring-doc.cn

GraphQLContextspring-doc.cn

For access to the context from the DataFetchingEnvironment.spring-doc.cn

java.security.Principalspring-doc.cn

Obtained from the Spring Security context, if available.spring-doc.cn

@AuthenticationPrincipalspring-doc.cn

For access to Authentication#getPrincipal() from the Spring Security context.spring-doc.cn

DataFetchingFieldSelectionSetspring-doc.cn

For access to the selection set for the query through the DataFetchingEnvironment.spring-doc.cn

Locale, Optional<Locale>spring-doc.cn

For access to the Locale from the DataFetchingEnvironment.spring-doc.cn

DataFetchingEnvironmentspring-doc.cn

For direct access to the underlying DataFetchingEnvironment.spring-doc.cn

Schema mapping handler methods can return:spring-doc.cn

  • A resolved value of any type.spring-doc.cn

  • Mono and Flux for asynchronous value(s). Supported for controller methods and for any DataFetcher as described in Reactive DataFetcher.spring-doc.cn

  • java.util.concurrent.Callable to have the value(s) produced asynchronously. For this to work, AnnotatedControllerConfigurer must be configured with an Executor.spring-doc.cn

6.2.2. @Argument

In GraphQL Java, DataFetchingEnvironment provides access to a map of field-specific argument values. The values can be simple scalar values (e.g. String, Long), a Map of values for more complex input, or a List of values.spring-doc.cn

Use the @Argument annotation to have an argument bound to a target object and injected into the handler method. Binding is performed by mapping argument values to a primary data constructor of the expected method parameter type, or by using a default constructor to create the object and then map argument values to its properties. This is repeated recursively, using all nested argument values and creating nested target objects accordingly. For example:spring-doc.cn

@Controller
public class BookController {

    @QueryMapping
    public Book bookById(@Argument Long id) {
        // ...
    }

    @MutationMapping
    public Book addBook(@Argument BookInput bookInput) {
        // ...
    }
}

By default, if the method parameter name is available (requires the -parameters compiler flag with Java 8+ or debugging info from the compiler), it is used to look up the argument. If needed, you can customize the name through the annotation, e.g. @Argument("bookInput").spring-doc.cn

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 GraphQL Java.

If binding fails, a BindException is raised with binding issues accumulated as field errors where the field of each error is the argument path where the issue occurred.spring-doc.cn

You can use @Argument with a Map<String, Object> argument, to obtain the raw map of all argument values. The name attribute on @Argument must not be set.spring-doc.cn

6.2.3. @Arguments

Use the @Arguments annotation, if you want to bind the full arguments map onto a single target Object, in contrast to @Argument, which binds a specific, named argument.spring-doc.cn

For example, @Argument BookInput bookInput uses the value of the argument "bookInput" to initialize BookInput, while @Arguments uses the full arguments map and in that case, top-level arguments are bound to BookInput properties.spring-doc.cn

You can use @Arguments with a Map<String, Object> argument, to obtain the raw map of all argument values.spring-doc.cn

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

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

For example:spring-doc.cn

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

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

A @BatchMapping handler method can batch load all authors for a query, given a list of source/parent books objects.spring-doc.cn

6.2.6. DataLoader

When you register a batch loading function for an entity, as explained in Batch Loading, you can access the DataLoader for the entity by declaring a method argument of type DataLoader and use it to load the entity:spring-doc.cn

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

Note that for many cases with loading related entities, where the @SchemaMapping simply delegates to a DataLoader, you can reduce boilerplate by using a @BatchMapping method as described in the next section.spring-doc.cn

6.2.7. Validation

When a javax.validation.Validator bean is found, AnnotatedControllerConfigurer enables support for Bean Validation on annotated controller methods. Typically, the bean is of type LocalValidatorFactoryBean.spring-doc.cn

Bean validation lets you declare constraints on types:spring-doc.cn

public class BookInput {

    @NotNull
    private String title;

    @NotNull
    @Size(max=13)
    private String isbn;
}

You can then annotate a controller method parameter with @Valid to validate it before method invocation:spring-doc.cn

@Controller
public class BookController {

    @MutationMapping
    public Book addBook(@Argument @Valid BookInput bookInput) {
        // ...
    }
}

If an error occurs during validation, a ConstraintViolationException is raised. You can use the Exception Resolution chain to decide how to present that to clients by turning it into an error to include in the GraphQL response.spring-doc.cn

In addition to @Valid, you can also use Spring’s @Validated that allows specifying validation groups.

Bean validation is useful for @Argument, @Arguments, and @ProjectedPayload method parameters, but applies more generally to any method parameter.spring-doc.cn

Validation and Kotlin Coroutines

Hibernate Validator is not compatible with Kotlin Coroutine methods and fails when introspecting their method parameters. Please see spring-projects/spring-graphql#344 (comment) for links to relevant issues and a suggested workaround.spring-doc.cn

6.3. @BatchMapping

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

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

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

To be used as a unique key, Book must implement hashcode and equals.spring-doc.cn

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

6.3.1. Method Signature

Batch mapping methods support the following arguments:spring-doc.cn

Method Argument Description

List<K>spring-doc.cn

The source/parent objects.spring-doc.cn

java.security.Principalspring-doc.cn

Obtained from Spring Security context, if available.spring-doc.cn

@ContextValuespring-doc.cn

For access to a value from the GraphQLContext of BatchLoaderEnvironment, which is the same context as the one from the DataFetchingEnvironment.spring-doc.cn

GraphQLContextspring-doc.cn

For access to the context from the BatchLoaderEnvironment, which is the same context as the one from the DataFetchingEnvironment.spring-doc.cn

BatchLoaderEnvironmentspring-doc.cn

The environment that is available in GraphQL Java to a org.dataloader.BatchLoaderWithContext.spring-doc.cn

Batch mapping methods can return:spring-doc.cn

Return Type Description

Mono<Map<K,V>>spring-doc.cn

A map with parent objects as keys, and batch loaded objects as values.spring-doc.cn

Flux<V>spring-doc.cn

A sequence of batch loaded objects that must be in the same order as the source/parent objects passed into the method.spring-doc.cn

Map<K,V>, Collection<V>spring-doc.cn

Imperative variants, e.g. without remote calls to make.spring-doc.cn

Callable<Map<K,V>>, Callable<Collection<V>>spring-doc.cn

Imperative variants to be invoked asynchronously. For this to work, AnnotatedControllerConfigurer must be configured with an Executor.spring-doc.cn