Skip to content

Commit eddb2b1

Browse files
committed
Configure RSocket server support in GraphQL
This commit adds the RSocket server auto-configuration for GraphQL. See gh-30453
1 parent 74494f1 commit eddb2b1

File tree

8 files changed

+460
-10
lines changed

8 files changed

+460
-10
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class GraphQlProperties {
4141

4242
private final Websocket websocket = new Websocket();
4343

44+
private final Rsocket rsocket = new Rsocket();
45+
4446
public Graphiql getGraphiql() {
4547
return this.graphiql;
4648
}
@@ -61,6 +63,10 @@ public Websocket getWebsocket() {
6163
return this.websocket;
6264
}
6365

66+
public Rsocket getRsocket() {
67+
return this.rsocket;
68+
}
69+
6470
public static class Schema {
6571

6672
/**
@@ -204,4 +210,21 @@ public void setConnectionInitTimeout(Duration connectionInitTimeout) {
204210

205211
}
206212

213+
public static class Rsocket {
214+
215+
/**
216+
* Mapping of the RSocket message handler.
217+
*/
218+
private String mapping;
219+
220+
public String getMapping() {
221+
return this.mapping;
222+
}
223+
224+
public void setMapping(String mapping) {
225+
this.mapping = mapping;
226+
}
227+
228+
}
229+
207230
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.rsocket;
18+
19+
import java.util.List;
20+
import java.util.stream.Collectors;
21+
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import graphql.GraphQL;
24+
import io.rsocket.core.RSocketServer;
25+
import reactor.netty.http.server.HttpServer;
26+
27+
import org.springframework.beans.factory.ObjectProvider;
28+
import org.springframework.boot.autoconfigure.AutoConfiguration;
29+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
33+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
34+
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
35+
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
36+
import org.springframework.context.annotation.Bean;
37+
import org.springframework.graphql.ExecutionGraphQlService;
38+
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
39+
import org.springframework.graphql.execution.GraphQlSource;
40+
import org.springframework.graphql.server.GraphQlRSocketHandler;
41+
import org.springframework.graphql.server.RSocketGraphQlInterceptor;
42+
import org.springframework.http.codec.json.Jackson2JsonEncoder;
43+
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
44+
45+
/**
46+
* {@link EnableAutoConfiguration Auto-configuration} for enabling Spring GraphQL over
47+
* RSocket.
48+
*
49+
* @author Brian Clozel
50+
* @since 2.7.0
51+
*/
52+
@AutoConfiguration(after = { GraphQlAutoConfiguration.class, RSocketMessagingAutoConfiguration.class })
53+
@ConditionalOnClass({ GraphQL.class, GraphQlSource.class, RSocketServer.class, HttpServer.class })
54+
@ConditionalOnBean({ RSocketMessageHandler.class, AnnotatedControllerConfigurer.class })
55+
@ConditionalOnProperty(prefix = "spring.graphql.rsocket", name = "mapping")
56+
public class GraphQlRSocketAutoConfiguration {
57+
58+
@Bean
59+
@ConditionalOnMissingBean
60+
public GraphQlRSocketHandler graphQlRSocketHandler(ExecutionGraphQlService graphQlService,
61+
ObjectProvider<RSocketGraphQlInterceptor> interceptorsProvider, ObjectMapper objectMapper) {
62+
List<RSocketGraphQlInterceptor> interceptors = interceptorsProvider.orderedStream()
63+
.collect(Collectors.toList());
64+
return new GraphQlRSocketHandler(graphQlService, interceptors, new Jackson2JsonEncoder(objectMapper));
65+
}
66+
67+
@Bean
68+
@ConditionalOnMissingBean
69+
public GraphQlRSocketController graphQlRSocketController(GraphQlRSocketHandler handler) {
70+
return new GraphQlRSocketController(handler);
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.rsocket;
18+
19+
import java.util.Map;
20+
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Mono;
23+
24+
import org.springframework.graphql.server.GraphQlRSocketHandler;
25+
import org.springframework.messaging.handler.annotation.MessageMapping;
26+
import org.springframework.stereotype.Controller;
27+
28+
@Controller
29+
class GraphQlRSocketController {
30+
31+
private final GraphQlRSocketHandler handler;
32+
33+
GraphQlRSocketController(GraphQlRSocketHandler handler) {
34+
this.handler = handler;
35+
}
36+
37+
@MessageMapping("${spring.graphql.rsocket.mapping}")
38+
Mono<Map<String, Object>> handle(Map<String, Object> payload) {
39+
return this.handler.handle(payload);
40+
}
41+
42+
@MessageMapping("${spring.graphql.rsocket.mapping}")
43+
Flux<Map<String, Object>> handleSubscription(Map<String, Object> payload) {
44+
return this.handler.handleSubscription(payload);
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Auto-configuration classes for RSocket integration with GraphQL.
19+
*/
20+
package org.springframework.boot.autoconfigure.graphql.rsocket;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.graphql.rsocket;
18+
19+
import java.net.URI;
20+
import java.time.Duration;
21+
import java.util.function.Consumer;
22+
23+
import graphql.schema.idl.TypeRuntimeWiring;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.boot.autoconfigure.AutoConfigurations;
27+
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
29+
import org.springframework.boot.autoconfigure.graphql.GraphQlTestDataFetchers;
30+
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
31+
import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration;
32+
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
33+
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
34+
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
35+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
36+
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
37+
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
38+
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
39+
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
40+
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
41+
import org.springframework.boot.web.embedded.netty.NettyRouteProvider;
42+
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
43+
import org.springframework.context.annotation.Bean;
44+
import org.springframework.context.annotation.Configuration;
45+
import org.springframework.graphql.client.RSocketGraphQlClient;
46+
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
47+
import org.springframework.graphql.server.GraphQlRSocketHandler;
48+
49+
import static org.assertj.core.api.Assertions.assertThat;
50+
51+
/**
52+
* Tests for {@link GraphQlRSocketAutoConfiguration}
53+
*
54+
* @author Brian Clozel
55+
*/
56+
class GraphQlRSocketAutoConfigurationTests {
57+
58+
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
59+
.withConfiguration(
60+
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
61+
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
62+
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
63+
.withUserConfiguration(DataFetchersConfiguration.class)
64+
.withPropertyValues("spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
65+
66+
@Test
67+
void shouldContributeDefaultBeans() {
68+
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GraphQlRSocketHandler.class)
69+
.hasSingleBean(GraphQlRSocketController.class));
70+
}
71+
72+
@Test
73+
void simpleQueryShouldWorkWithTcpServer() {
74+
testWithRSocketTcp(this::assertThatSimpleQueryWorks);
75+
}
76+
77+
@Test
78+
void simpleQueryShouldWorkWithWebSocketServer() {
79+
testWithRSocketWebSocket(this::assertThatSimpleQueryWorks);
80+
}
81+
82+
private void assertThatSimpleQueryWorks(RSocketGraphQlClient client) {
83+
String document = "{ bookById(id: \"book-1\"){ id name pageCount author } }";
84+
String bookName = client.document(document).retrieve("bookById.name").toEntity(String.class)
85+
.block(Duration.ofSeconds(5));
86+
assertThat(bookName).isEqualTo("GraphQL for beginners");
87+
}
88+
89+
private void testWithRSocketTcp(Consumer<RSocketGraphQlClient> consumer) {
90+
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
91+
.withConfiguration(
92+
AutoConfigurations.of(JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
93+
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
94+
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
95+
.withUserConfiguration(DataFetchersConfiguration.class).withPropertyValues(
96+
"spring.main.web-application-type=reactive", "spring.graphql.rsocket.mapping=graphql");
97+
contextRunner.withInitializer(new RSocketPortInfoApplicationContextInitializer())
98+
.withPropertyValues("spring.rsocket.server.port=0").run((context) -> {
99+
String serverPort = context.getEnvironment().getProperty("local.rsocket.server.port");
100+
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
101+
.tcp("localhost", Integer.parseInt(serverPort)).route("graphql").build();
102+
consumer.accept(client);
103+
});
104+
}
105+
106+
private void testWithRSocketWebSocket(Consumer<RSocketGraphQlClient> consumer) {
107+
ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
108+
AnnotationConfigReactiveWebServerApplicationContext::new).withConfiguration(
109+
AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
110+
ErrorWebFluxAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
111+
JacksonAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class,
112+
RSocketMessagingAutoConfiguration.class, RSocketServerAutoConfiguration.class,
113+
GraphQlAutoConfiguration.class, GraphQlRSocketAutoConfiguration.class))
114+
.withInitializer(new ServerPortInfoApplicationContextInitializer())
115+
.withUserConfiguration(DataFetchersConfiguration.class, NettyServerConfiguration.class)
116+
.withPropertyValues("spring.main.web-application-type=reactive", "server.port=0",
117+
"spring.graphql.rsocket.mapping=graphql", "spring.rsocket.server.transport=websocket",
118+
"spring.rsocket.server.mapping-path=/rsocket");
119+
contextRunner.run((context) -> {
120+
String serverPort = context.getEnvironment().getProperty("local.server.port");
121+
RSocketGraphQlClient client = RSocketGraphQlClient.builder()
122+
.webSocket(URI.create("ws://localhost:" + serverPort + "/rsocket")).route("graphql").build();
123+
consumer.accept(client);
124+
});
125+
}
126+
127+
@Configuration(proxyBeanMethods = false)
128+
static class NettyServerConfiguration {
129+
130+
@Bean
131+
NettyReactiveWebServerFactory serverFactory(NettyRouteProvider routeProvider) {
132+
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
133+
serverFactory.addRouteProviders(routeProvider);
134+
return serverFactory;
135+
}
136+
137+
}
138+
139+
@Configuration(proxyBeanMethods = false)
140+
static class DataFetchersConfiguration {
141+
142+
@Bean
143+
RuntimeWiringConfigurer bookDataFetcher() {
144+
return (builder) -> builder.type(TypeRuntimeWiring.newTypeWiring("Query").dataFetcher("bookById",
145+
GraphQlTestDataFetchers.getBookByIdDataFetcher()));
146+
}
147+
148+
}
149+
150+
}

0 commit comments

Comments
 (0)