Does Spring support exposing multiple GraphQL endpoints and having separate schemas for those endpoints?
I found nothing about it in the documentation. I know that Spring supports multiple schemas, but as far as I know, all of the schemas are merged into one. This causes naming conflicts and other issues.
Current state:
I already have a Spring GraphQL API in my application with the URL: /old-api/graphql.
To be:
Is this possible or not at all? If yes, please provide examples.
I expecting to find a simple way how to solve my problem.
Thanks in advance.
I found how solve the problem by myself. This code based that I found in GraphQlWebMvcAutoConfiguration and GraphQlAutoConfiguration (from spring-boot-autoconfigure:3.2.2)
Common config:
@Configuration
@ComponentScan(basePackages = "com.my.app")
@Import({FirstGraphQlConfig.class, SecondGraphQlConfig.class})
public class GraphQlConfig {
@Bean
public Instrumentation instrumentation() {
return new CustomLoggingInstrumentation();
}
@Bean
public AnnotatedControllerConfigurer controllerConfigurer() {
return new AnnotatedControllerConfigurer();
}
}
First config:
import graphql.execution.instrumentation.Instrumentation;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.SubscriptionExceptionResolver;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class FirstGraphQlConfig extends AbstractGraphQlConfig {
@Bean
public BatchLoaderRegistry firstBatchLoaderRegistry() {
return new DefaultBatchLoaderRegistry();
}
@Bean
public GraphQlSource firstGraphQlSource(
AnnotatedControllerConfigurer controllerConfigurer,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolvers,
ObjectProvider<SubscriptionExceptionResolver> subscriptionExceptionResolvers,
ObjectProvider<Instrumentation> instrumentations) {
return getGraphQlSource(
"graphql/first.graphqls",
controllerConfigurer,
exceptionResolvers,
subscriptionExceptionResolvers,
instrumentations);
}
@Bean
public ExecutionGraphQlService firstGraphQlService(
GraphQlSource firstGraphQlSource, BatchLoaderRegistry firstBatchLoaderRegistry) {
final var service = new DefaultExecutionGraphQlService(firstGraphQlSource);
service.addDataLoaderRegistrar(firstBatchLoaderRegistry);
return service;
}
@Bean
public RouterFunction<ServerResponse> firstRouterFunction(
ExecutionGraphQlService firstGraphQlService,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return getRouterFunction(
"/first/graphql", firstGraphQlService, interceptors);
}
}
Second config:
import graphql.execution.instrumentation.Instrumentation;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.SubscriptionExceptionResolver;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class SecondGraphQlConfig extends AbstractGraphQlConfig {
@Bean
public BatchLoaderRegistry secondLoaderRegistry() {
return new DefaultBatchLoaderRegistry();
}
@Bean
public GraphQlSource secondGraphQlSource(
AnnotatedControllerConfigurer controllerConfigurer,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolvers,
ObjectProvider<SubscriptionExceptionResolver> subscriptionExceptionResolvers,
ObjectProvider<Instrumentation> instrumentations) {
return getGraphQlSource(
"graphql/second.graphqls",
controllerConfigurer,
exceptionResolvers,
subscriptionExceptionResolvers,
instrumentations);
}
@Bean
public ExecutionGraphQlService secondGraphQlService(
GraphQlSource secondGraphQlSource, BatchLoaderRegistry secondLoaderRegistry) {
final var service = new DefaultExecutionGraphQlService(secondGraphQlSource);
service.addDataLoaderRegistrar(secondLoaderRegistry);
return service;
}
@Bean
public RouterFunction<ServerResponse> secondRouterFunction(
ExecutionGraphQlService secondGraphQlService,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return getRouterFunction("/second/graphql", secondGraphQlService, interceptors);
}
}
Abstract config:
import graphql.execution.instrumentation.Instrumentation;
import java.util.Collections;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.ExecutionGraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.ConnectionTypeDefinitionConfigurer;
import org.springframework.graphql.execution.DataFetcherExceptionResolver;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.SubscriptionExceptionResolver;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.graphql.server.WebGraphQlInterceptor;
import org.springframework.graphql.server.webmvc.GraphQlHttpHandler;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
public abstract class AbstractGraphQlConfig {
private static final MediaType[] SUPPORTED_MEDIA_TYPES =
new MediaType[] {
MediaType.APPLICATION_GRAPHQL_RESPONSE,
MediaType.APPLICATION_JSON,
MediaType.APPLICATION_GRAPHQL
};
protected RouterFunction<ServerResponse> getRouterFunction(
String path,
ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
final var webGraphQlHandler =
WebGraphQlHandler.builder(service)
.interceptors(interceptors.orderedStream().toList())
.build();
final var graphQlHttpHandler = new GraphQlHttpHandler(webGraphQlHandler);
final var graphQlPredicate =
RequestPredicates.contentType(new MediaType[] {MediaType.APPLICATION_JSON})
.and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES))
.and(RequestPredicates.path(path));
return RouterFunctions.route()
.GET(path, this::onlyAllowPost)
.POST(path, graphQlPredicate, graphQlHttpHandler::handleRequest)
.build();
}
protected GraphQlSource getGraphQlSource(
String schemaPath,
AnnotatedControllerConfigurer controllerConfigurer,
ObjectProvider<DataFetcherExceptionResolver> exceptionResolvers,
ObjectProvider<SubscriptionExceptionResolver> subscriptionExceptionResolvers,
ObjectProvider<Instrumentation> instrumentations) {
return GraphQlSource.schemaResourceBuilder()
.schemaResources(new ClassPathResource(schemaPath))
.configureTypeDefinitions(new ConnectionTypeDefinitionConfigurer())
.configureRuntimeWiring(controllerConfigurer)
.exceptionResolvers(exceptionResolvers.orderedStream().toList())
.subscriptionExceptionResolvers(subscriptionExceptionResolvers.orderedStream().toList())
.instrumentation(instrumentations.orderedStream().toList())
.build();
}
private ServerResponse onlyAllowPost(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED)
.headers(this::onlyAllowPost)
.build();
}
private void onlyAllowPost(HttpHeaders headers) {
headers.setAllow(Collections.singleton(HttpMethod.POST));
}
}
Disable auto configuration:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration, org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration