Search code examples
springspring-bootspring-graphql

Is it possible to expose multiple GraphQL endpoints in one spring boot application?


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:

  1. Implement another GraphQL API in the same application.
  2. This API has a different schema, and objects from this schema have naming conflicts with the current one.
  3. I need a different URL for the new API: /new-api/graphql.

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.


Solution

  • 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