Browse Source
This commit adds the RSocket server auto-configuration for GraphQL. See gh-30453pull/29812/head
8 changed files with 460 additions and 10 deletions
@ -0,0 +1,73 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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())); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
|
||||
/* |
||||
* 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.docs.web.graphql.transports.rsocket; |
||||
|
||||
import java.net.URI; |
||||
import java.time.Duration; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.graphql.client.RSocketGraphQlClient; |
||||
|
||||
public class RSocketGraphQlClientExample { |
||||
|
||||
public void rsocketOverTcp() { |
||||
// tag::tcp[]
|
||||
RSocketGraphQlClient client = RSocketGraphQlClient.builder().tcp("example.spring.io", 8181).route("graphql") |
||||
.build(); |
||||
Mono<Book> book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }") |
||||
.retrieve("bookById").toEntity(Book.class); |
||||
// end::tcp[]
|
||||
book.block(Duration.ofSeconds(5)); |
||||
} |
||||
|
||||
public void rsocketOverWebSocket() { |
||||
// tag::websocket[]
|
||||
RSocketGraphQlClient client = RSocketGraphQlClient.builder() |
||||
.webSocket(URI.create("wss://example.spring.io/rsocket")).route("graphql").build(); |
||||
Mono<Book> book = client.document("{ bookById(id: \"book-1\"){ id name pageCount author } }") |
||||
.retrieve("bookById").toEntity(Book.class); |
||||
// end::websocket[]
|
||||
book.block(Duration.ofSeconds(5)); |
||||
} |
||||
|
||||
static class Book { |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
/* |
||||
* 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.docs.web.graphql.transports.rsocket |
||||
|
||||
import org.springframework.graphql.client.RSocketGraphQlClient |
||||
import java.net.URI |
||||
import java.time.Duration |
||||
|
||||
|
||||
class RSocketGraphQlClientExample { |
||||
|
||||
fun rsocketOverTcp() { |
||||
// tag::tcp[] |
||||
val client = RSocketGraphQlClient.builder() |
||||
.tcp("example.spring.io", 8181) |
||||
.route("graphql") |
||||
.build() |
||||
val book = client.document( |
||||
""" |
||||
{ |
||||
bookById(id: "book-1"){ |
||||
id |
||||
name |
||||
pageCount |
||||
author |
||||
} |
||||
} |
||||
""") |
||||
.retrieve("bookById").toEntity(Book::class.java) |
||||
// end::tcp[] |
||||
book.block(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
fun rsocketOverWebSocket() { |
||||
// tag::websocket[] |
||||
val client = RSocketGraphQlClient.builder() |
||||
.webSocket(URI.create("wss://example.spring.io/rsocket")) |
||||
.route("graphql") |
||||
.build() |
||||
val book = client.document( |
||||
""" |
||||
{ |
||||
bookById(id: "book-1"){ |
||||
id |
||||
name |
||||
pageCount |
||||
author |
||||
} |
||||
} |
||||
""") |
||||
.retrieve("bookById").toEntity(Book::class.java) |
||||
// end::websocket[] |
||||
book.block(Duration.ofSeconds(5)) |
||||
} |
||||
|
||||
internal class Book |
||||
} |
||||
Loading…
Reference in new issue