Configure RSocket server support in GraphQL

This commit adds the RSocket server auto-configuration for GraphQL.

See gh-30453
This commit is contained in:
Brian Clozel
2022-04-14 19:54:40 +02:00
parent 74494f1d37
commit eddb2b16ff
8 changed files with 460 additions and 10 deletions

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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()));
}
}
}