Auto-configure GraphQL WebSocket endpoint
This commit auto-configures a GraphQL WebSocket endpoint for both Spring MVC and Spring WebFlux. This is only enabled if the required libraries are on the classpath and if the `"spring.graphql.websocket.path"` property is defined. See gh-29140
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.autoconfigure.graphql;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -38,6 +39,8 @@ public class GraphQlProperties {
|
||||
|
||||
private final Schema schema = new Schema();
|
||||
|
||||
private final Websocket websocket = new Websocket();
|
||||
|
||||
public Graphiql getGraphiql() {
|
||||
return this.graphiql;
|
||||
}
|
||||
@@ -54,6 +57,10 @@ public class GraphQlProperties {
|
||||
return this.schema;
|
||||
}
|
||||
|
||||
public Websocket getWebsocket() {
|
||||
return this.websocket;
|
||||
}
|
||||
|
||||
public static class Schema {
|
||||
|
||||
/**
|
||||
@@ -143,4 +150,35 @@ public class GraphQlProperties {
|
||||
|
||||
}
|
||||
|
||||
public static class Websocket {
|
||||
|
||||
/**
|
||||
* Path of the GraphQL WebSocket subscription endpoint.
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* Time within which the initial {@code CONNECTION_INIT} type message must be
|
||||
* received.
|
||||
*/
|
||||
private Duration connectionInitTimeout = Duration.ofSeconds(60);
|
||||
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Duration getConnectionInitTimeout() {
|
||||
return this.connectionInitTimeout;
|
||||
}
|
||||
|
||||
public void setConnectionInitTimeout(Duration connectionInitTimeout) {
|
||||
this.connectionInitTimeout = connectionInitTimeout;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlCorsProperties;
|
||||
@@ -42,17 +43,22 @@ import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.web.webflux.GraphiQlHandler;
|
||||
import org.springframework.graphql.web.webflux.SchemaHandler;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.config.CorsRegistry;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.reactive.socket.server.support.WebSocketUpgradeHandlerPredicate;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.contentType;
|
||||
@@ -139,4 +145,32 @@ public class GraphQlWebFluxAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
|
||||
public static class WebSocketConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
|
||||
GraphQlProperties properties, ServerCodecConfigurer configurer) {
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, configurer,
|
||||
properties.getWebsocket().getConnectionInitTimeout());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HandlerMapping graphQlWebSocketEndpoint(GraphQlWebSocketHandler graphQlWebSocketHandler,
|
||||
GraphQlProperties properties) {
|
||||
String path = properties.getWebsocket().getPath();
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("GraphQL endpoint WebSocket " + path);
|
||||
}
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setHandlerPredicate(new WebSocketUpgradeHandlerPredicate());
|
||||
mapping.setUrlMap(Collections.singletonMap(path, graphQlWebSocketHandler));
|
||||
mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
|
||||
return mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
package org.springframework.boot.autoconfigure.graphql.servlet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.websocket.server.ServerContainer;
|
||||
|
||||
import graphql.GraphQL;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -29,10 +32,12 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
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.http.HttpMessageConverters;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -43,18 +48,25 @@ import org.springframework.graphql.execution.ThreadLocalAccessor;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
|
||||
import org.springframework.graphql.web.webmvc.GraphiQlHandler;
|
||||
import org.springframework.graphql.web.webmvc.SchemaHandler;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
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.ServerResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
|
||||
import org.springframework.web.socket.server.support.WebSocketHandlerMapping;
|
||||
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
@@ -140,4 +152,42 @@ public class GraphQlWebMvcAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ ServerContainer.class, WebSocketHandler.class })
|
||||
@ConditionalOnProperty(prefix = "spring.graphql.websocket", name = "path")
|
||||
public static class WebSocketConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlWebSocketHandler graphQlWebSocketHandler(WebGraphQlHandler webGraphQlHandler,
|
||||
GraphQlProperties properties, HttpMessageConverters converters) {
|
||||
|
||||
return new GraphQlWebSocketHandler(webGraphQlHandler, getJsonConverter(converters),
|
||||
properties.getWebsocket().getConnectionInitTimeout());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static GenericHttpMessageConverter<Object> getJsonConverter(HttpMessageConverters converters) {
|
||||
return converters.getConverters().stream()
|
||||
.filter((candidate) -> candidate.canRead(Map.class, MediaType.APPLICATION_JSON)).findFirst()
|
||||
.map((converter) -> (GenericHttpMessageConverter<Object>) converter)
|
||||
.orElseThrow(() -> new IllegalStateException("No JSON converter"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, GraphQlProperties properties) {
|
||||
String path = properties.getWebsocket().getPath();
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("GraphQL endpoint WebSocket " + path);
|
||||
}
|
||||
WebSocketHandlerMapping mapping = new WebSocketHandlerMapping();
|
||||
mapping.setWebSocketUpgradeMatch(true);
|
||||
mapping.setUrlMap(Collections.singletonMap(path,
|
||||
new WebSocketHttpRequestHandler(handler, new DefaultHandshakeHandler())));
|
||||
mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean)
|
||||
return mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,12 +33,16 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
/**
|
||||
@@ -60,6 +64,12 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||
"spring.graphql.cors.allowed-origins=https://example.com",
|
||||
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
|
||||
|
||||
@Test
|
||||
void shouldContributeDefaultBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
|
||||
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleQueryShouldWork() {
|
||||
testWithWebClient((client) -> {
|
||||
@@ -130,6 +140,12 @@ class GraphQlWebFluxAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureWebSocketBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
|
||||
}
|
||||
|
||||
private void testWithWebClient(Consumer<WebTestClient> consumer) {
|
||||
this.contextRunner.run((context) -> {
|
||||
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
|
||||
|
||||
@@ -31,13 +31,17 @@ import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
import org.springframework.graphql.web.WebGraphQlHandler;
|
||||
import org.springframework.graphql.web.WebInterceptor;
|
||||
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
|
||||
import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
@@ -65,6 +69,12 @@ class GraphQlWebMvcAutoConfigurationTests {
|
||||
"spring.graphql.cors.allowed-origins=https://example.com",
|
||||
"spring.graphql.cors.allowed-methods=POST", "spring.graphql.cors.allow-credentials=true");
|
||||
|
||||
@Test
|
||||
void shouldContributeDefaultBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlHttpHandler.class)
|
||||
.hasSingleBean(WebGraphQlHandler.class).doesNotHaveBean(GraphQlWebSocketHandler.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleQueryShouldWork() {
|
||||
testWith((mockMvc) -> {
|
||||
@@ -137,6 +147,12 @@ class GraphQlWebMvcAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConfigureWebSocketBeans() {
|
||||
this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws")
|
||||
.run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class));
|
||||
}
|
||||
|
||||
private void testWith(MockMvcConsumer mockMvcConsumer) {
|
||||
this.contextRunner.run((context) -> {
|
||||
MediaType mediaType = MediaType.APPLICATION_JSON;
|
||||
|
||||
Reference in New Issue
Block a user