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 org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.core.io.Resource;
/**
@ -31,31 +32,35 @@ import org.springframework.core.io.Resource;
@ConfigurationProperties("spring.graphql")
public class GraphQlProperties {
/**
* Path at which to expose a GraphQL request HTTP endpoint.
*/
private String path = "/graphql";
private final Http http = new Http();
private final Graphiql graphiql = new Graphiql();
private final Rsocket rsocket = new Rsocket();
private final Schema schema = new Schema();
private final DeprecatedSse sse = new DeprecatedSse(this.http.getSse());
private final Websocket websocket = new Websocket();
private final Rsocket rsocket = new Rsocket();
private final Sse sse = new Sse();
public Http getHttp() {
return this.http;
}
public Graphiql getGraphiql() {
return this.graphiql;
}
@DeprecatedConfigurationProperty(replacement = "spring.graphql.http.path", since = "3.5.0")
@Deprecated(since = "3.5.0", forRemoval = true)
public String getPath() {
return this.path;
return getHttp().getPath();
}
@Deprecated(since = "3.5.0", forRemoval = true)
public void setPath(String path) {
this.path = path;
getHttp().setPath(path);
}
public Schema getSchema() {
@ -70,10 +75,33 @@ public class GraphQlProperties {
return this.rsocket;
}
public Sse getSse() {
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() {
return this.sse;
}
}
public static class Schema {
/**
@ -178,7 +206,7 @@ public class GraphQlProperties {
/**
* 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;
@ -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);
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(ExecutionGraphQlService service,
ObjectProvider<WebGraphQlInterceptor> interceptors) {
return WebGraphQlHandler.builder(service).interceptors(interceptors.orderedStream().toList()).build();
}
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {
@ -100,15 +93,23 @@ public class GraphQlWebFluxAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler) {
return new GraphQlSseHandler(webGraphQlHandler);
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
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
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
@ -119,7 +120,8 @@ public class GraphQlWebFluxAutoConfiguration {
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
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);
builder.GET(path + "/schema", schemaHandler::handleRequest);
}
@ -158,7 +160,7 @@ public class GraphQlWebFluxAutoConfiguration {
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
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.GraphQlCorsProperties;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@ -99,8 +98,7 @@ public class GraphQlWebMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlSseHandler graphQlSseHandler(WebGraphQlHandler webGraphQlHandler, GraphQlProperties properties) {
Sse sse = properties.getSse();
return new GraphQlSseHandler(webGraphQlHandler, sse.getTimeout());
return new GraphQlSseHandler(webGraphQlHandler, properties.getHttp().getSse().getTimeout());
}
@Bean
@ -113,8 +111,9 @@ public class GraphQlWebMvcAutoConfiguration {
@Bean
@Order(0)
public RouterFunction<ServerResponse> graphQlRouterFunction(GraphQlHttpHandler httpHandler,
GraphQlSseHandler sseHandler, GraphQlSource graphQlSource, GraphQlProperties properties) {
String path = properties.getPath();
GraphQlSseHandler sseHandler, ObjectProvider<GraphQlSource> graphQlSourceProvider,
GraphQlProperties properties) {
String path = properties.getHttp().getPath();
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route();
builder.route(GraphQlRequestPredicates.graphQlHttp(path), httpHandler::handleRequest);
@ -125,7 +124,8 @@ public class GraphQlWebMvcAutoConfiguration {
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
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);
builder.GET(path + "/schema", schemaHandler::handleRequest);
}
@ -164,7 +164,7 @@ public class GraphQlWebMvcAutoConfiguration {
public void addCorsMappings(CorsRegistry registry) {
CorsConfiguration configuration = this.corsProperties.toCorsConfiguration();
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
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);
GraphQlSseHandler handler = context.getBean(GraphQlSseHandler.class);
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.
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`.
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)
@ConditionalOnMissingBean
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);
}

View File

@ -171,7 +171,7 @@ class HttpGraphQlTesterContextCustomizer implements ContextCustomizer {
}
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";
}