Configure RSocket server support in GraphQL
This commit adds the RSocket server auto-configuration for GraphQL. See gh-30453
This commit is contained in:
@@ -41,6 +41,8 @@ public class GraphQlProperties {
|
||||
|
||||
private final Websocket websocket = new Websocket();
|
||||
|
||||
private final Rsocket rsocket = new Rsocket();
|
||||
|
||||
public Graphiql getGraphiql() {
|
||||
return this.graphiql;
|
||||
}
|
||||
@@ -61,6 +63,10 @@ public class GraphQlProperties {
|
||||
return this.websocket;
|
||||
}
|
||||
|
||||
public Rsocket getRsocket() {
|
||||
return this.rsocket;
|
||||
}
|
||||
|
||||
public static class Schema {
|
||||
|
||||
/**
|
||||
@@ -204,4 +210,21 @@ public class GraphQlProperties {
|
||||
|
||||
}
|
||||
|
||||
public static class Rsocket {
|
||||
|
||||
/**
|
||||
* Mapping of the RSocket message handler.
|
||||
*/
|
||||
private String mapping;
|
||||
|
||||
public String getMapping() {
|
||||
return this.mapping;
|
||||
}
|
||||
|
||||
public void setMapping(String mapping) {
|
||||
this.mapping = mapping;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.graphql.rsocket;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import graphql.GraphQL;
|
||||
import io.rsocket.core.RSocketServer;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
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.graphql.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.graphql.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||
import org.springframework.graphql.execution.GraphQlSource;
|
||||
import org.springframework.graphql.server.GraphQlRSocketHandler;
|
||||
import org.springframework.graphql.server.RSocketGraphQlInterceptor;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
|
||||
* RSocket.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 2.7.0
|
||||
*/
|
||||
@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class })
|
||||
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
|
||||
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
|
||||
@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping")
|
||||
public class GraphQlRSocketAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
|
||||
ObjectProvider<RSocketGraphQlInterceptor> interceptorsProvider, ObjectMapper objectMapper) {
|
||||
List<RSocketGraphQlInterceptor> interceptors = interceptorsProvider.orderedStream()
|
||||
.collect(Collectors.toList());
|
||||
return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
|
||||
return new GraphQlRSocketController(handler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.graphql.rsocket;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.server.GraphQlRSocketHandler;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@Controller
|
||||
class GraphQlRSocketController {
|
||||
|
||||
private final GraphQlRSocketHandler handler;
|
||||
|
||||
GraphQlRSocketController(GraphQlRSocketHandler handler) {
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
@MessageMapping("${spring.graphql.rsocket.mapping}")
|
||||
Mono<Map<String, Object>> handle(Map<String, Object> payload) {
|
||||
return this.handler.handle(payload);
|
||||
}
|
||||
|
||||
@MessageMapping("${spring.graphql.rsocket.mapping}")
|
||||
Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
|
||||
return this.handler.handleSubscription(payload);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auto-configuration classes for RSocket integration with GraphQL.
|
||||
*/
|
||||
package org.springframework.boot.autoconfigure.graphql.rsocket;
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.autoconfigure.graphql.rsocket;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import graphql.schema.idl.TypeRuntimeWiring;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
|
||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
|
||||
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
|
||||
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
|
||||
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.graphql.client.RSocketGraphQlClient;
|
||||
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
|
||||
import org.springframework.graphql.server.GraphQlRSocketHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraphQlRSocketAutoConfiguration}
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class GraphQlRSocketAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
|
||||
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
|
||||
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
|
||||
.withUserConfiguration(DataFetchersConfiguration.class)
|
||||
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
|
||||
|
||||
@Test
|
||||
void shouldContributeDefaultBeans() {
|
||||
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class)
|
||||
.hasSingleBean(GraphQlRSocketController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleQueryShouldWorkWithTcpServer() {
|
||||
testWithRSocketTcp(this::assertThatSimpleQueryWorks);
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleQueryShouldWorkWithWebSocketServer() {
|
||||
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
|
||||
}
|
||||
|
||||
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
|
||||
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
|
||||
String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class)
|
||||
.block(Duration.ofSeconds(5));
|
||||
assertThat(bookName).isEqualTo("GraphQL for beginners");
|
||||
}
|
||||
|
||||
private void testWithRSocketTcp(Consumer<RSocketGraphQlClient> consumer) {
|
||||
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
|
||||
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
|
||||
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
|
||||
.withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues(
|
||||
"spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
|
||||
contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer())
|
||||
.withPropertyValues("spring.rsocket.server.port=0").run((context) -> {
|
||||
String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port");
|
||||
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
|
||||
.tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build();
|
||||
consumer.accept(client);
|
||||
});
|
||||
}
|
||||
|
||||
private void testWithRSocketWebSocket(Consumer<RSocketGraphQlClient> consumer) {
|
||||
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
|
||||
AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration(
|
||||
AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
|
||||
ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
|
||||
JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
|
||||
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
|
||||
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
|
||||
.withInitializer(new ServerPortInfoApplicationContextInitializer())
|
||||
.withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class)
|
||||
.withPropertyValues("spring.main.web-application-type=reactive", "server.port=0",
|
||||
"spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket",
|
||||
"spring.rsocket.server.mapping-path=/rsocket");
|
||||
contextRunner.run((context) -> {
|
||||
String serverPort = context.getEnvironment().getProperty("local.server.port");
|
||||
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
|
||||
.webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build();
|
||||
consumer.accept(client);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class NettyServerConfiguration {
|
||||
|
||||
@Bean
|
||||
NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) {
|
||||
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
|
||||
serverFactory.addRouteProviders(routeProvider);
|
||||
return serverFactory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DataFetchersConfiguration {
|
||||
|
||||
@Bean
|
||||
RuntimeWiringConfigurer bookDataFetcher() {
|
||||
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
|
||||
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user