Separate transports in GraphQL auto-configurations

This commit revisits the existing GraphQL configuration properties to
better reflect which ones belong to specific transports.
This also relaxes the Web auto-configurations to only require the
`ExecutionGraphQlService` as a bean. The `GraphQlSource` is now an
optional bean dependency.

Closes gh-44495
This commit is contained in:
Brian Clozel 2025-02-28 18:12:31 +01:00
parent e886785f76
commit 83f678a8b1
7 changed files with 86 additions and 35 deletions

View File

@ -20,6 +20,7 @@ import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
/** /**
@ -31,31 +32,35 @@ import org.springframework.core.io.Resource;
@ConfigurationProperties("spring.graphql") @ConfigurationProperties("spring.graphql")
public class GraphQlProperties { public class GraphQlProperties {
/** private final Http http = new Http();
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Graphiql graphiql = new Graphiql(); private final Graphiql graphiql = new Graphiql();
private final Rsocket rsocket = new Rsocket();
private final Schema schema = new Schema(); private final Schema schema = new Schema();
private final DeprecatedSse sse = new DeprecatedSse(this.http.getSse());
private final Websocket websocket = new Websocket(); private final Websocket websocket = new Websocket();
private final Rsocket rsocket = new Rsocket(); public Http getHttp() {
return this.http;
private final Sse sse = new Sse(); }
public Graphiql getGraphiql() { public Graphiql getGraphiql() {
return this.graphiql; return this.graphiql;
} }
@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.path", since = "3.5.0")
@Deprecated(since = "3.5.0", forRemoval = true)
public String getPath() { public String getPath() {
return this.path; return getHttp().getPath();
} }
@Deprecated(since = "3.5.0", forRemoval = true)
public void setPath(String path) { public void setPath(String path) {
this.path = path; getHttp().setPath(path);
} }
public Schema getSchema() { public Schema getSchema() {
@ -70,10 +75,33 @@ public class GraphQlProperties {
return this.rsocket; return this.rsocket;
} }
public DeprecatedSse getSse() {
return this.sse;
}
public static class Http {
/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Sse sse = new Sse();
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public Sse getSse() { public Sse getSse() {
return this.sse; return this.sse;
} }
}
public static class Schema { public static class Schema {
/** /**
@ -178,7 +206,7 @@ public class GraphQlProperties {
/** /**
* Whether the endpoint that prints the schema is enabled. Schema is available * Whether the endpoint that prints the schema is enabled. Schema is available
* under spring.graphql.path + "/schema". * under spring.graphql.http.path + "/schema".
*/ */
private boolean enabled = false; private boolean enabled = false;
@ -302,4 +330,25 @@ public class GraphQlProperties {
} }
public static class DeprecatedSse {
private final Sse sse;
public DeprecatedSse(Sse sse) {
this.sse = sse;
}
@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.sse.timeout", since = "3.5.0")
@Deprecated(since = "3.5.0", forRemoval = true)
public Duration getTimeout() {
return this.sse.getTimeout();
}
@Deprecated(since = "3.5.0", forRemoval = true)
public void setTimeout(Duration timeout) {
this.sse.setTimeout(timeout);
}
}
} }

View File

@ -85,13 +85,6 @@ public class GraphQlWebFluxAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class); private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) { public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
@ -100,15 +93,23 @@ public class GraphQlWebFluxAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler) { public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
return new GraphQlSseHandler(webGraphQlHandler); return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout());
}
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
} }
@Bean @Bean
@Order(0) @Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler, public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) { GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
String path = properties.getPath(); GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path)); logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route(); RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest); builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
@ -119,7 +120,8 @@ public class GraphQlWebFluxAutoConfiguration {
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath()); GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest); builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);
} }
if (properties.getSchema().getPrinter().isEnabled()) { GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder.GET(path + "/schema", schemaHandler::handleRequest); builder.GET(path + "/schema", schemaHandler::handleRequest);
} }
@ -158,7 +160,7 @@ public class GraphQlWebFluxAutoConfiguration {
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration(); CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) { if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration); registry.addMapping(this.graphQlProperties.getHttp().getPath()).combine(configuration);
} }
} }

View File

@ -37,7 +37,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties; import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties; import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties.Sse;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -99,8 +98,7 @@ public class GraphQlWebMvcAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) { public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
Sse sse = properties.getSse(); return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout());
return new GraphQlSseHandler(webGraphQlHandler, sse.getTimeout());
} }
@Bean @Bean
@ -113,8 +111,9 @@ public class GraphQlWebMvcAutoConfiguration {
@Bean @Bean
@Order(0) @Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler, public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) { GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
String path = properties.getPath(); GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path)); logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route(); RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest); builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
@ -125,7 +124,8 @@ public class GraphQlWebMvcAutoConfiguration {
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath()); GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest); builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);
} }
if (properties.getSchema().getPrinter().isEnabled()) { GraphQlSource graphQlSource = graphQlSourceProvider.getIfAvailable();
if (properties.getSchema().getPrinter().isEnabled() && graphQlSource != null) {
SchemaHandler schemaHandler = new SchemaHandler(graphQlSource); SchemaHandler schemaHandler = new SchemaHandler(graphQlSource);
builder.GET(path + "/schema", schemaHandler::handleRequest); builder.GET(path + "/schema", schemaHandler::handleRequest);
} }
@ -164,7 +164,7 @@ public class GraphQlWebMvcAutoConfiguration {
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration(); CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
if (configuration != null) { if (configuration != null) {
registry.addMapping(this.graphQlProperties.getPath()).combine(configuration); registry.addMapping(this.graphQlProperties.getHttp().getPath()).combine(configuration);
} }
} }

View File

@ -81,7 +81,7 @@ class GraphQlWebMvcAutoConfigurationTests {
@Test @Test
void shouldConfigureSseTimeout() { void shouldConfigureSseTimeout() {
this.contextRunner.withPropertyValues("spring.graphql.sse.timeout=10s").run((context) -> { this.contextRunner.withPropertyValues("spring.graphql.http.sse.timeout=10s").run((context) -> {
assertThat(context).hasSingleBean(GraphQlSseHandler.class); assertThat(context).hasSingleBean(GraphQlSseHandler.class);
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class); GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
assertThat(handler).hasFieldOrPropertyWithValue("timeout", Duration.ofSeconds(10)); assertThat(handler).hasFieldOrPropertyWithValue("timeout", Duration.ofSeconds(10));

View File

@ -94,7 +94,7 @@ are detected by Spring Boot and considered as candidates for javadoc:graphql.sch
The GraphQL HTTP endpoint is at HTTP POST `/graphql` by default. The GraphQL HTTP endpoint is at HTTP POST `/graphql` by default.
It also supports the `"text/event-stream"` media type over Server Sent Events for subscriptions only. It also supports the `"text/event-stream"` media type over Server Sent Events for subscriptions only.
The path can be customized with configprop:spring.graphql.path[]. The path can be customized with configprop:spring.graphql.http.path[].
TIP: The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a `RouterFunction` bean with an javadoc:org.springframework.core.annotation.Order[format=annotation] of `0`. TIP: The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a `RouterFunction` bean with an javadoc:org.springframework.core.annotation.Order[format=annotation] of `0`.
If you define your own `RouterFunction` beans, you may want to add appropriate javadoc:org.springframework.core.annotation.Order[format=annotation] annotations to ensure that they are sorted correctly. If you define your own `RouterFunction` beans, you may want to add appropriate javadoc:org.springframework.core.annotation.Order[format=annotation] annotations to ensure that they are sorted correctly.

View File

@ -43,7 +43,7 @@ public class HttpGraphQlTesterAutoConfiguration {
@ConditionalOnBean(WebTestClient.class) @ConditionalOnBean(WebTestClient.class)
@ConditionalOnMissingBean @ConditionalOnMissingBean
public HttpGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) { public HttpGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build(); WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getHttp().getPath()).build();
return HttpGraphQlTester.create(mutatedWebTestClient); return HttpGraphQlTester.create(mutatedWebTestClient);
} }

View File

@ -171,7 +171,7 @@ class HttpGraphQlTesterContextCustomizer implements ContextCustomizer {
} }
private String findConfiguredGraphQlPath() { private String findConfiguredGraphQlPath() {
String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.path"); String configuredPath = this.applicationContext.getEnvironment().getProperty("spring.graphql.http.path");
return StringUtils.hasText(configuredPath) ? configuredPath : "/graphql"; return StringUtils.hasText(configuredPath) ? configuredPath : "/graphql";
} }