Spring Boot starter powered by GraphQL SPQR
GraphQL SPQR Spring Boot starter aims to make it dead simple to add a GraphQL API to any Spring Boot project.
- Add
@GraphQLApi
to any Spring managed component, and you're good to go 🚀 - GraphQL endpoint available at
/graphql
by default - GraphQL Playground IDE (if enabled, see the properties below) available at
/ide
- Fully customizable in seconds by providing simple beans (any SPQR SPI can be exposed as a bean)
To use this starter in a typical Spring Boot project, add the following dependencies to your project:
<dependencies>
<dependency>
<groupId>io.leangen.graphql</groupId>
<artifactId>graphql-spqr-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
There's also a very basic sample project
All beans in Spring's application context annotated with @GraphqlApi
are considered to be operation sources (a concept similar to Controller
beans in Spring MVC).
This annotation can be used in combination with @Component/@Service/@Repository
or @Bean
annotations, e.g.
@Component
@GraphQLApi
private class UserService {
//Query/mutation/subscription methods
...
}
or
@Bean
@GraphQLApi
public userService() {
return new UserService(...);
}
To deduce which methods of each operation source class should be exposed as GraphQL queries/mutations/subscriptions,
SPQR uses the concept of a ResolverBuilder
(since each exposed method acts as a resolver function for a GraphQL operation).
To cover the basic approaches SpqrAutoConfiguration
registers a bean for each of the three built-in ResolverBuilder
implementations:
AnnotatedResolverBuilder
- exposes only the methods annotated by@GraphQLQuery
,@GraphQLMutation
or@GraphQLSubscription
PublicResolverBuilder
- exposes allpublic
methods from the operations source class (methods returningvoid
are considered mutations)BeanResolverBuilder
- exposes all getters as queries and setters as mutations (getters returningPublisher<T>
are considered subscriptions)RecordResolverBuilder
- exposes all record component accessors as queries (accessors returningPublisher<T>
are considered subscriptions)
It is also possible to implement custom resolver builders by implementing the ResolverBuilder
interface.
Resolver builders can be declared both globally and on the operation source level. If not sticking to the defaults, it is generally safer to explicitly customize on the operation source level, unless the rules are absolutely uniform across all operation sources. Customizing on both levels simultaneously will work but could prove tricky to control as your API grows.
Defaults:
- For top-level beans: only
AnnotatedResolverBuilder
is registered, - For nested beans:
AnnotatedResolverBuilder
,BeanResolverBuilder
andRecordResolverBuilder
(applied to records only) are registered
To change the default resolver builders globally, implement and register a bean of type ExtensionProvider<ResolverBuilder>
.
A simplified example of this could be:
@Bean
public ExtensionProvider<GeneratorConfiguration, ResolverBuilder> resolverBuilderExtensionProvider() {
return (config, current) -> {
List<ResolverBuilder> resolverBuilders = new ArrayList<>();
//add a custom subtype of PublicResolverBuilder that only exposes a method if it's called "greeting"
resolverBuilders.add(new PublicResolverBuilder() {
@Override
protected boolean isQuery(Method method) {
return super.isQuery(method) && method.getName().equals("greeting");
}
});
//add the default builder
resolverBuilders.add(new AnnotatedResolverBuilder());
return resolverBuilders;
};
}
This would add two resolver builders that apply to all operation sources.
The First one exposes all public methods named greeting. The second is the inbuilt AnnotatedResolverBuilder
(that exposes only the explicitly annotated methods).
A quicker way to achieve the same would be:
@Bean
public ExtensionProvider<GeneratorConfiguration, ResolverBuilder> resolverBuilderExtensionProvider() {
//prepend the custom builder to the provided list of defaults
return (config, current) -> current.prepend(new PublicResolverBuilder() {
@Override
protected boolean isQuery(Method method) {
return super.isQuery(method) && method.getName().equals("greeting");
}
});
};
To attach a resolver builder to a specific source (bean), use the @WithResolverBuilder
annotation on it.
This annotation also works both on the beans registered by @Component/@Service/@Repository
or @Bean
annotations.
As an example, we can expose the greeting
query by using:
@Component
@GraphQLApi
@WithResolverBuilder(BeanResolverBuilder.class) //exposes all getters
private class MyOperationSource {
public String getGreeting(){
return "Hello world !";
}
}
or:
@Bean
@GraphQLApi
//No explicit resolver builders declared, so AnnotatedResolverBuilder is used
public MyOperationSource() {
@GraphQLQuery(name = "greeting")
public String getGreeting() {
return "Hello world !";
}
}
It is also entirely possible to use more than one resolver builder on the same operation source e.g.
@Component
@GraphQLApi
@WithResolverBuilder(BeanResolverBuilder.class)
@WithResolverBuilder(AnnotatedResolverBuilder.class)
private class MyOperationSource {
//Exposed by BeanResolverBuilder because it's a getter
public String getGreeting(){
return "Hello world !";
}
//Exposed by AnnotatedResolverBuilder because it's annotated
@GraphQLQuery
public String personalGreeting(String name){
return "Hello " + name + " !";
}
}
This way, both queries are exposed but in different ways. The same would work on a bean registered using the @Bean
annotation.
Sometimes it is useful to have an automated strategy for generating type names, descriptions and order of fields within the type.
To do this SPQR uses TypeInfoGenerator
on a global level. When using this starter the most convenient way is to wire a single bean of that type in the application context.
@Bean
public TypeInfoGenerator testTypeInfoGenerator() {
return new TypeInfoGenerator() {
@Override
public String generateTypeName(AnnotatedType type, MessageBundle messageBundle) {
return nameGenerationMethodLocalized(type, messageBundle);
}
@Override
public String generateTypeDescription(AnnotatedType type, MessageBundle messageBundle) {
return descriptionGenerationMethodLocalized(type, messageBundle);
}
@Override
public String[] getFieldOrder(AnnotatedType type, MessageBundle messageBundle) {
return fieldOrderGenerationMethodLocalized(type, messageBundle);
}
};
}
Property | Default Value |
---|---|
graphql.spqr.base-packages | n/a |
graphql.spqr.abstract-input-type-resolution | false |
graphql.spqr.relay.enabled | false |
graphql.spqr.relay.mutation-wrapper | n/a |
graphql.spqr.relay.mutation-wrapper-description | n/a |
graphql.spqr.relay.connection-check-relaxed | false |
graphql.spqr.relay.spring-data-compatible | false |
graphql.spqr.http.enabled | true |
graphql.spqr.http.endpoint | /graphql |
graphql.spqr.http.mvc.executor | async |
graphql.spqr.ws.enabled | true |
graphql.spqr.ws.endpoint | n/a |
graphql.spqr.ws.send-time-limit | 10000 |
graphql.spqr.ws.send-buffer-size-limit | 512 * 1024 |
graphql.spqr.ws.allowed-origins | * |
graphql.spqr.ws.keep-alive.enabled | false |
graphql.spqr.ws.keep-alive.interval-millis | 10000 |
graphql.spqr.gui.enabled | true |
graphql.spqr.gui.endpoint | /gui |
graphql.spqr.gui.target-endpoint | n/a |
graphql.spqr.gui.target-ws-endpoint | n/a |
graphql.spqr.gui.page-title | GraphQL Playground |
Object in charge of doing this in SPQR is ValueMapperFactory
. Again the simplest way to make use of this when using the starter is to wire a single bean of this type into the application context.
@Bean
public ValueMapperFactory testValueMapperFactory() {
return (abstractTypes, environment) -> new ValueMapper() {
@Override
public <T> T fromInput(Object graphQLInput, Type sourceType, AnnotatedType outputType) {
return null;
}
@Override
public <T> T fromString(String json, AnnotatedType type) {
return null;
}
@Override
public String toString(Object output) {
return null;
}
};
}
NOTE: SPQR comes with JacksonValueMapper
and GsonValueMapperFactory
so in reality this should be rarely needed as these are by far the most frequently used libraries in Java.
Analogous to the rest of the configuration, single beans should be wired into the context. As this is done in functional style in SPQR it is not possible to set chains of InputConverter
and OutputConverter
, but by passing a lambda that will manipulate the chains.
Extension provider for input converters
@Bean
public ExtensionProvider<GeneratorConfiguration, InputConverter> testInputConverterExtensionProvider() {
return (config, current) -> current.prepend( //Insert before the defaults. Or return a new list to take full control.
new InputConverter() {
@Override
public Object convertInput(Object substitute, AnnotatedType type, GlobalEnvironment environment, ValueMapper valueMapper) {
return ...;
}
@Override
public boolean supports(AnnotatedType type) {
return ...;
}
@Override
public AnnotatedType getSubstituteType(AnnotatedType original) {
return ...;
}
}
);
}
Extension provider for output converters
@Bean
public ExtensionProvider<GeneratorConfiguration, OutputConverter> testOutputConverterExtensionProvider() {
//Insert a custom converter after the built-in IdAdapter (which is generally a safe position).
//Return a new list instead to take full control.
return (config, current) -> current.insertAfter(IdAdapter.class,
new OutputConverter() {
@Override
public Object convertOutput(Object original, AnnotatedType type, ResolutionEnvironment resolutionEnvironment) {
return ...;
}
@Override
public boolean supports(AnnotatedType type) {
return ...;
}
}
);
}
Again wire a single bean of type ExtensionProvider<GeneratorConfiguration, TypeMapper>
into the application context to manipulate mapper chain.
@Bean
public ExtensionProvider<GeneratorConfiguration, TypeMapper> customTypeMappers() {
//Insert a custom mapper after the built-in IdAdapter (which is generally a safe position)
return (config, current) -> current.insertAfter(IdAdapter.class,
new TypeMapper() {
@Override
public GraphQLOutputType toGraphQLType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
return new GraphQLOutputType() {
@Override
public String getName() {
return ...;
}
};
}
@Override
public GraphQLInputType toGraphQLInputType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
return new GraphQLInputType() {
@Override
public String getName() {
return ...;
}
};
}
@Override
public boolean supports(AnnotatedType type) {
return ...;
}
}
);
}
Also has a functional API, utilised by wiring a single bean of type ExtensionProvider<GeneratorConfiguration, ArgumentInjector>
.
@Bean
public ExtensionProvider<GeneratorConfiguration, ArgumentInjector> testArgumentInjectorExtensionProvider() {
return (config, current) -> current.prepend(
new ArgumentInjector() {
@Override
public Object getArgumentValue(ArgumentInjectorParams params) {
return ...;
}
@Override
public boolean supports(AnnotatedType type, Parameter parameter) {
return ...;
}
}
);
}
Wiring a single bean of type ExtensionProvider<GraphQLSchemaGenerator.ExtendedConfiguration, InputFieldBuilder>
will allow you to manipulate the input builder chain.
@Bean
public ExtensionProvider<ExtendedGeneratorConfiguration, InputFieldBuilder> testInputFieldBuilder() {
return (config, current) -> current.prepend( //Prepend your custom builder so it goes before the built-in ones
new InputFieldBuilder() {
@Override
public Set<InputField> getInputFields(InputFieldBuilderParams params) {
return ...; //Build the input fields for the given type
}
@Override
public boolean supports(AnnotatedType type) {
return ...; //Does this builder support the given type?
}
});
}
NOTE: In SPQR InputFieldBuilder
is already implemented by JacksonValueMapper
and GsonValueMapper
.