Create spring-boot-rsocket module

This commit is contained in:
Andy Wilkinson
2025-03-21 13:23:29 +00:00
committed by Phillip Webb
parent 5715b90af9
commit 0d5a141a41
58 changed files with 147 additions and 181 deletions

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
/**
* Callback interface that can be used to customize a {@link RSocketMessageHandler}.
*
* @author Aarti Gupta
* @author Madhura Bhave
* @since 4.0.0
*/
@FunctionalInterface
public interface RSocketMessageHandlerCustomizer {
/**
* Customize the {@link RSocketMessageHandler}.
* @param messageHandler the message handler to customize
*/
void customize(RSocketMessageHandler messageHandler);
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import io.rsocket.transport.netty.server.TcpServerTransport;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring RSocket support in Spring
* Messaging.
*
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, TcpServerTransport.class })
public class RSocketMessagingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RSocketMessageHandler messageHandler(RSocketStrategies rSocketStrategies,
ObjectProvider<RSocketMessageHandlerCustomizer> customizers) {
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
messageHandler.setRSocketStrategies(rSocketStrategies);
customizers.orderedStream().forEach((customizer) -> customizer.customize(messageHandler));
return messageHandler;
}
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import java.net.InetAddress;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.web.server.Ssl;
import org.springframework.util.unit.DataSize;
/**
* {@link ConfigurationProperties Properties} for RSocket support.
*
* @author Brian Clozel
* @author Chris Bono
* @since 4.0.0
*/
@ConfigurationProperties("spring.rsocket")
public class RSocketProperties {
@NestedConfigurationProperty
private final Server server = new Server();
public Server getServer() {
return this.server;
}
public static class Server {
/**
* Server port.
*/
private Integer port;
/**
* Network address to which the server should bind.
*/
private InetAddress address;
/**
* RSocket transport protocol.
*/
private RSocketServer.Transport transport = RSocketServer.Transport.TCP;
/**
* Path under which RSocket handles requests (only works with websocket
* transport).
*/
private String mappingPath;
/**
* Maximum transmission unit. Frames larger than the specified value are
* fragmented.
*/
private DataSize fragmentSize;
@NestedConfigurationProperty
private Ssl ssl;
private final Spec spec = new Spec();
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.port = port;
}
public InetAddress getAddress() {
return this.address;
}
public void setAddress(InetAddress address) {
this.address = address;
}
public RSocketServer.Transport getTransport() {
return this.transport;
}
public void setTransport(RSocketServer.Transport transport) {
this.transport = transport;
}
public String getMappingPath() {
return this.mappingPath;
}
public void setMappingPath(String mappingPath) {
this.mappingPath = mappingPath;
}
public DataSize getFragmentSize() {
return this.fragmentSize;
}
public void setFragmentSize(DataSize fragmentSize) {
this.fragmentSize = fragmentSize;
}
public Ssl getSsl() {
return this.ssl;
}
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
public Spec getSpec() {
return this.spec;
}
public static class Spec {
/**
* Sub-protocols to use in websocket handshake signature.
*/
private String protocols;
/**
* Maximum allowable frame payload length.
*/
private DataSize maxFramePayloadLength = DataSize.ofBytes(65536);
/**
* Whether to proxy websocket ping frames or respond to them.
*/
private boolean handlePing;
/**
* Whether the websocket compression extension is enabled.
*/
private boolean compress;
public String getProtocols() {
return this.protocols;
}
public void setProtocols(String protocols) {
this.protocols = protocols;
}
public DataSize getMaxFramePayloadLength() {
return this.maxFramePayloadLength;
}
public void setMaxFramePayloadLength(DataSize maxFramePayloadLength) {
this.maxFramePayloadLength = maxFramePayloadLength;
}
public boolean isHandlePing() {
return this.handlePing;
}
public void setHandlePing(boolean handlePing) {
this.handlePing = handlePing;
}
public boolean isCompress() {
return this.compress;
}
public void setCompress(boolean compress) {
this.compress = compress;
}
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import io.rsocket.transport.netty.server.TcpServerTransport;
import reactor.netty.http.server.HttpServer;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.rsocket.RSocketConnectorConfigurer;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketRequester.Builder;
import org.springframework.messaging.rsocket.RSocketStrategies;
/**
* {@link EnableAutoConfiguration Auto-configuration} for
* {@link org.springframework.messaging.rsocket.RSocketRequester}. This auto-configuration
* creates {@link org.springframework.messaging.rsocket.RSocketRequester.Builder}
* prototype beans, as the builders are stateful and should not be reused to build
* requester instances with different configurations.
*
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
@ConditionalOnClass({ RSocketRequester.class, io.rsocket.RSocket.class, HttpServer.class, TcpServerTransport.class })
public class RSocketRequesterAutoConfiguration {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@ConditionalOnMissingBean
public RSocketRequester.Builder rSocketRequesterBuilder(RSocketStrategies strategies,
ObjectProvider<RSocketConnectorConfigurer> connectorConfigurers) {
Builder builder = RSocketRequester.builder().rsocketStrategies(strategies);
connectorConfigurers.orderedStream().forEach(builder::rsocketConnector);
return builder;
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import java.util.function.Consumer;
import io.rsocket.core.RSocketServer;
import io.rsocket.frame.decoder.PayloadDecoder;
import io.rsocket.transport.netty.server.TcpServerTransport;
import reactor.netty.http.server.HttpServer;
import reactor.netty.http.server.WebsocketServerSpec.Builder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.reactor.netty.autoconfigure.ReactorNettyConfigurations;
import org.springframework.boot.rsocket.autoconfigure.RSocketProperties.Server.Spec;
import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
import org.springframework.boot.rsocket.netty.NettyRSocketServerFactory;
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.util.unit.DataSize;
/**
* {@link EnableAutoConfiguration Auto-configuration} for RSocket servers. In the case of
* {@link org.springframework.boot.WebApplicationType#REACTIVE}, the RSocket server is
* added as a WebSocket endpoint on the existing
* {@link org.springframework.boot.reactor.netty.NettyWebServer}. If a specific server
* port is configured, a new standalone RSocket server is created.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 4.0.0
*/
@AutoConfiguration(after = RSocketStrategiesAutoConfiguration.class)
@ConditionalOnClass({ RSocketServer.class, RSocketStrategies.class, HttpServer.class, TcpServerTransport.class })
@ConditionalOnBean(RSocketMessageHandler.class)
@EnableConfigurationProperties(RSocketProperties.class)
public class RSocketServerAutoConfiguration {
@Conditional(OnRSocketWebServerCondition.class)
@Configuration(proxyBeanMethods = false)
static class WebFluxServerConfiguration {
@Bean
@ConditionalOnMissingBean
RSocketWebSocketNettyRouteProvider rSocketWebsocketRouteProvider(RSocketProperties properties,
RSocketMessageHandler messageHandler, ObjectProvider<RSocketServerCustomizer> customizers) {
return new RSocketWebSocketNettyRouteProvider(properties.getServer().getMappingPath(),
messageHandler.responder(), customizeWebsocketServerSpec(properties.getServer().getSpec()),
customizers.orderedStream());
}
private Consumer<Builder> customizeWebsocketServerSpec(Spec spec) {
return (builder) -> {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(spec.getProtocols()).to(builder::protocols);
map.from(spec.getMaxFramePayloadLength()).asInt(DataSize::toBytes).to(builder::maxFramePayloadLength);
map.from(spec.isHandlePing()).to(builder::handlePing);
map.from(spec.isCompress()).to(builder::compress);
};
}
}
@ConditionalOnProperty("spring.rsocket.server.port")
@ConditionalOnClass(ReactorResourceFactory.class)
@Configuration(proxyBeanMethods = false)
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
static class EmbeddedServerConfiguration {
@Bean
@ConditionalOnMissingBean
RSocketServerFactory rSocketServerFactory(RSocketProperties properties, ReactorResourceFactory resourceFactory,
ObjectProvider<RSocketServerCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {
NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
factory.setResourceFactory(resourceFactory);
factory.setTransport(properties.getServer().getTransport());
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties.getServer().getAddress()).to(factory::setAddress);
map.from(properties.getServer().getPort()).to(factory::setPort);
map.from(properties.getServer().getFragmentSize()).to(factory::setFragmentSize);
map.from(properties.getServer().getSsl()).to(factory::setSsl);
factory.setSslBundles(sslBundles.getIfAvailable());
factory.setRSocketServerCustomizers(customizers.orderedStream().toList());
return factory;
}
@Bean
@ConditionalOnMissingBean
RSocketServerBootstrap rSocketServerBootstrap(RSocketServerFactory rSocketServerFactory,
RSocketMessageHandler rSocketMessageHandler) {
return new RSocketServerBootstrap(rSocketServerFactory, rSocketMessageHandler.responder());
}
@Bean
RSocketServerCustomizer frameDecoderRSocketServerCustomizer(RSocketMessageHandler rSocketMessageHandler) {
return (server) -> {
if (rSocketMessageHandler.getRSocketStrategies()
.dataBufferFactory() instanceof NettyDataBufferFactory) {
server.payloadDecoder(PayloadDecoder.ZERO_COPY);
}
};
}
}
static class OnRSocketWebServerCondition extends AllNestedConditions {
OnRSocketWebServerCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
static class IsReactiveWebApplication {
}
@ConditionalOnProperty(name = "spring.rsocket.server.port", matchIfMissing = true)
static class HasNoPortConfigured {
}
@ConditionalOnProperty("spring.rsocket.server.mapping-path")
static class HasMappingPathConfigured {
}
@ConditionalOnProperty(name = "spring.rsocket.server.transport", havingValue = "websocket")
static class HasWebsocketTransportConfigured {
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import io.netty.buffer.PooledByteBufAllocator;
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.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.util.ClassUtils;
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link RSocketStrategies}.
*
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration")
@ConditionalOnClass({ io.rsocket.RSocket.class, RSocketStrategies.class, PooledByteBufAllocator.class })
public class RSocketStrategiesAutoConfiguration {
private static final String PATHPATTERN_ROUTEMATCHER_CLASS = "org.springframework.web.util.pattern.PathPatternRouteMatcher";
@Bean
@ConditionalOnMissingBean
public RSocketStrategies rSocketStrategies(ObjectProvider<RSocketStrategiesCustomizer> customizers) {
RSocketStrategies.Builder builder = RSocketStrategies.builder();
if (ClassUtils.isPresent(PATHPATTERN_ROUTEMATCHER_CLASS, null)) {
builder.routeMatcher(new PathPatternRouteMatcher());
}
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ObjectMapper.class, CBORFactory.class })
@SuppressWarnings({ "removal", "deprecation" })
protected static class JacksonCborStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_CBOR };
@Bean
@Order(0)
@ConditionalOnBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
public RSocketStrategiesCustomizer jacksonCborRSocketStrategyCustomizer(
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder builder) {
return (strategy) -> {
ObjectMapper objectMapper = builder.createXmlMapper(false).factory(new CBORFactory()).build();
strategy.decoder(
new org.springframework.http.codec.cbor.Jackson2CborDecoder(objectMapper, SUPPORTED_TYPES));
strategy.encoder(
new org.springframework.http.codec.cbor.Jackson2CborEncoder(objectMapper, SUPPORTED_TYPES));
};
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class)
@SuppressWarnings({ "removal", "deprecation" })
protected static class JacksonJsonStrategyConfiguration {
private static final MediaType[] SUPPORTED_TYPES = { MediaType.APPLICATION_JSON,
new MediaType("application", "*+json") };
@Bean
@Order(1)
@ConditionalOnBean(ObjectMapper.class)
public RSocketStrategiesCustomizer jacksonJsonRSocketStrategyCustomizer(ObjectMapper objectMapper) {
return (strategy) -> {
strategy.decoder(
new org.springframework.http.codec.json.Jackson2JsonDecoder(objectMapper, SUPPORTED_TYPES));
strategy.encoder(
new org.springframework.http.codec.json.Jackson2JsonEncoder(objectMapper, SUPPORTED_TYPES));
};
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
import io.rsocket.SocketAcceptor;
import io.rsocket.core.RSocketServer;
import io.rsocket.transport.ServerTransport;
import io.rsocket.transport.netty.server.WebsocketRouteTransport;
import reactor.netty.http.server.HttpServerRoutes;
import reactor.netty.http.server.WebsocketServerSpec;
import reactor.netty.http.server.WebsocketServerSpec.Builder;
import org.springframework.boot.reactor.netty.NettyRouteProvider;
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
/**
* {@link NettyRouteProvider} that configures an RSocket Websocket endpoint.
*
* @author Brian Clozel
* @author Leo Li
*/
class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider {
private final String mappingPath;
private final SocketAcceptor socketAcceptor;
private final List<RSocketServerCustomizer> customizers;
private final Consumer<Builder> serverSpecCustomizer;
RSocketWebSocketNettyRouteProvider(String mappingPath, SocketAcceptor socketAcceptor,
Consumer<Builder> serverSpecCustomizer, Stream<RSocketServerCustomizer> customizers) {
this.mappingPath = mappingPath;
this.socketAcceptor = socketAcceptor;
this.serverSpecCustomizer = serverSpecCustomizer;
this.customizers = customizers.toList();
}
@Override
public HttpServerRoutes apply(HttpServerRoutes httpServerRoutes) {
RSocketServer server = RSocketServer.create(this.socketAcceptor);
this.customizers.forEach((customizer) -> customizer.customize(server));
ServerTransport.ConnectionAcceptor connectionAcceptor = server.asConnectionAcceptor();
return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor),
createWebsocketServerSpec());
}
private WebsocketServerSpec createWebsocketServerSpec() {
WebsocketServerSpec.Builder builder = WebsocketServerSpec.builder();
this.serverSpecCustomizer.accept(builder);
return builder.build();
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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 for RSocket.
*/
package org.springframework.boot.rsocket.autoconfigure;

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2025 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.rsocket.context;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
/**
* {@link ApplicationContextInitializer} that sets {@link Environment} properties for the
* ports that {@link RSocketServer} servers are actually listening on. The property
* {@literal "local.rsocket.server.port"} can be injected directly into tests using
* {@link Value @Value} or obtained through the {@link Environment}.
* <p>
* Properties are automatically propagated up to any parent context.
*
* @author Verónica Vásquez
* @author Eddú Meléndez
* @since 2.2.0
*/
public class RSocketPortInfoApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(new Listener(applicationContext));
}
private static class Listener implements ApplicationListener<RSocketServerInitializedEvent> {
private static final String PROPERTY_NAME = "local.rsocket.server.port";
private static final String PROPERTY_SOURCE_NAME = "server.ports";
private final ConfigurableApplicationContext applicationContext;
Listener(ConfigurableApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void onApplicationEvent(RSocketServerInitializedEvent event) {
if (event.getServer().address() != null) {
setPortProperty(this.applicationContext, event.getServer().address().getPort());
}
}
private void setPortProperty(ApplicationContext context, int port) {
if (context instanceof ConfigurableApplicationContext configurableContext) {
setPortProperty(configurableContext.getEnvironment(), port);
}
if (context.getParent() != null) {
setPortProperty(context.getParent(), port);
}
}
private void setPortProperty(ConfigurableEnvironment environment, int port) {
MutablePropertySources sources = environment.getPropertySources();
PropertySource<?> source = sources.get(PROPERTY_SOURCE_NAME);
if (source == null) {
source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>());
sources.addFirst(source);
}
setPortProperty(port, source);
}
@SuppressWarnings("unchecked")
private void setPortProperty(int port, PropertySource<?> source) {
((Map<String, Object>) source.getSource()).put(PROPERTY_NAME, port);
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2025 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.rsocket.context;
import io.rsocket.SocketAcceptor;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.SmartLifecycle;
import org.springframework.util.Assert;
/**
* Bootstrap an {@link RSocketServer} and start it with the application context.
*
* @author Brian Clozel
* @since 2.2.0
*/
public class RSocketServerBootstrap implements ApplicationEventPublisherAware, SmartLifecycle {
private final RSocketServer server;
private ApplicationEventPublisher eventPublisher;
public RSocketServerBootstrap(RSocketServerFactory serverFactory, SocketAcceptor socketAcceptor) {
Assert.notNull(serverFactory, "'serverFactory' must not be null");
this.server = serverFactory.create(socketAcceptor);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
@Override
public void start() {
this.server.start();
this.eventPublisher.publishEvent(new RSocketServerInitializedEvent(this.server));
}
@Override
public void stop() {
this.server.stop();
}
@Override
public boolean isRunning() {
RSocketServer server = this.server;
if (server != null) {
return server.address() != null;
}
return false;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2025 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.rsocket.context;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.context.ApplicationEvent;
/**
* Event to be published after the application context is refreshed and the
* {@link RSocketServer} is ready. Useful for obtaining the local port of a running
* server.
*
* @author Brian Clozel
* @since 2.2.0
*/
public class RSocketServerInitializedEvent extends ApplicationEvent {
public RSocketServerInitializedEvent(RSocketServer server) {
super(server);
}
/**
* Access the {@link RSocketServer}.
* @return the embedded RSocket server
*/
public RSocketServer getServer() {
return getSource();
}
/**
* Access the source of the event (an {@link RSocketServer}).
* @return the embedded web server
*/
@Override
public RSocketServer getSource() {
return (RSocketServer) super.getSource();
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2012-2025 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.
*/
/**
* RSocket integrations with Spring Framework's
* {@link org.springframework.context.ApplicationContext ApplicationContext}.
*/
package org.springframework.boot.rsocket.context;

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.rsocket.messaging;
import org.springframework.messaging.rsocket.RSocketStrategies;
/**
* Callback interface that can be used to customize codecs configuration for an RSocket
* client and/or server with {@link RSocketStrategies}.
*
* @author Brian Clozel
* @since 2.2.0
*/
@FunctionalInterface
public interface RSocketStrategiesCustomizer {
/**
* Callback to customize a {@link RSocketStrategies#builder()} instance.
* @param strategies rSocket codec strategies to customize
*/
void customize(RSocketStrategies.Builder strategies);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Support for RSocket-based messaging.
*/
package org.springframework.boot.rsocket.messaging;

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2012-2025 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.rsocket.netty;
import java.net.InetSocketAddress;
import java.time.Duration;
import io.rsocket.transport.netty.server.CloseableChannel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.rsocket.server.RSocketServerException;
import org.springframework.util.Assert;
/**
* {@link RSocketServer} that is based on a Reactor Netty server. Usually this class
* should be created using the {@link NettyRSocketServerFactory} and not directly.
*
* @author Brian Clozel
* @since 2.2.0
*/
public class NettyRSocketServer implements RSocketServer {
private static final Log logger = LogFactory.getLog(NettyRSocketServer.class);
private final Mono<CloseableChannel> starter;
private final Duration lifecycleTimeout;
private CloseableChannel channel;
public NettyRSocketServer(Mono<CloseableChannel> starter, Duration lifecycleTimeout) {
Assert.notNull(starter, "'starter' must not be null");
this.starter = starter;
this.lifecycleTimeout = lifecycleTimeout;
}
@Override
public InetSocketAddress address() {
if (this.channel != null) {
return this.channel.address();
}
return null;
}
@Override
public void start() throws RSocketServerException {
this.channel = block(this.starter, this.lifecycleTimeout);
logger.info("Netty RSocket started on port " + address().getPort());
startDaemonAwaitThread(this.channel);
}
private void startDaemonAwaitThread(CloseableChannel channel) {
Thread awaitThread = new Thread(() -> channel.onClose().block(), "rsocket");
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
}
@Override
public void stop() throws RSocketServerException {
if (this.channel != null) {
this.channel.dispose();
this.channel = null;
}
}
private <T> T block(Mono<T> mono, Duration timeout) {
return (timeout != null) ? mono.block(timeout) : mono.block();
}
}

View File

@@ -0,0 +1,307 @@
/*
* Copyright 2012-2025 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.rsocket.netty;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import io.netty.handler.ssl.ClientAuth;
import io.rsocket.SocketAcceptor;
import io.rsocket.transport.ServerTransport;
import io.rsocket.transport.netty.server.CloseableChannel;
import io.rsocket.transport.netty.server.TcpServerTransport;
import io.rsocket.transport.netty.server.WebsocketServerTransport;
import reactor.core.publisher.Mono;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.server.HttpServer;
import reactor.netty.tcp.AbstractProtocolSslContextSpec;
import reactor.netty.tcp.SslProvider;
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
import reactor.netty.tcp.SslProvider.SslContextSpec;
import reactor.netty.tcp.TcpServer;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.rsocket.server.ConfigurableRSocketServerFactory;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.Ssl.ServerNameSslBundle;
import org.springframework.boot.web.server.WebServerSslBundle;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataSize;
/**
* {@link RSocketServerFactory} that can be used to create {@link RSocketServer}s backed
* by Netty.
*
* @author Brian Clozel
* @author Chris Bono
* @author Scott Frederick
* @since 2.2.0
*/
public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory {
private int port = 9898;
private DataSize fragmentSize;
private InetAddress address;
private RSocketServer.Transport transport = RSocketServer.Transport.TCP;
private ReactorResourceFactory resourceFactory;
private Duration lifecycleTimeout;
private List<RSocketServerCustomizer> rSocketServerCustomizers = new ArrayList<>();
private Ssl ssl;
private SslBundles sslBundles;
@Override
public void setPort(int port) {
this.port = port;
}
@Override
public void setFragmentSize(DataSize fragmentSize) {
this.fragmentSize = fragmentSize;
}
@Override
public void setAddress(InetAddress address) {
this.address = address;
}
@Override
public void setTransport(RSocketServer.Transport transport) {
this.transport = transport;
}
@Override
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
@Override
public void setSslBundles(SslBundles sslBundles) {
this.sslBundles = sslBundles;
}
/**
* Set the {@link ReactorResourceFactory} to get the shared resources from.
* @param resourceFactory the server resources
*/
public void setResourceFactory(ReactorResourceFactory resourceFactory) {
this.resourceFactory = resourceFactory;
}
/**
* Set {@link RSocketServerCustomizer}s that should be called to configure the
* {@link io.rsocket.core.RSocketServer} while building the server. Calling this
* method will replace any existing customizers.
* @param rSocketServerCustomizers customizers to apply before the server starts
* @since 2.2.7
*/
public void setRSocketServerCustomizers(Collection<? extends RSocketServerCustomizer> rSocketServerCustomizers) {
Assert.notNull(rSocketServerCustomizers, "'rSocketServerCustomizers' must not be null");
this.rSocketServerCustomizers = new ArrayList<>(rSocketServerCustomizers);
}
/**
* Add {@link RSocketServerCustomizer}s that should be called to configure the
* {@link io.rsocket.core.RSocketServer}.
* @param rSocketServerCustomizers customizers to apply before the server starts
* @since 2.2.7
*/
public void addRSocketServerCustomizers(RSocketServerCustomizer... rSocketServerCustomizers) {
Assert.notNull(rSocketServerCustomizers, "'rSocketServerCustomizers' must not be null");
this.rSocketServerCustomizers.addAll(Arrays.asList(rSocketServerCustomizers));
}
/**
* Set the maximum amount of time that should be waited when starting or stopping the
* server.
* @param lifecycleTimeout the lifecycle timeout
*/
public void setLifecycleTimeout(Duration lifecycleTimeout) {
this.lifecycleTimeout = lifecycleTimeout;
}
@Override
public NettyRSocketServer create(SocketAcceptor socketAcceptor) {
ServerTransport<CloseableChannel> transport = createTransport();
io.rsocket.core.RSocketServer server = io.rsocket.core.RSocketServer.create(socketAcceptor);
configureServer(server);
Mono<CloseableChannel> starter = server.bind(transport);
return new NettyRSocketServer(starter, this.lifecycleTimeout);
}
private void configureServer(io.rsocket.core.RSocketServer server) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.fragmentSize).asInt(DataSize::toBytes).to(server::fragment);
this.rSocketServerCustomizers.forEach((customizer) -> customizer.customize(server));
}
private ServerTransport<CloseableChannel> createTransport() {
if (this.transport == RSocketServer.Transport.WEBSOCKET) {
return createWebSocketTransport();
}
return createTcpTransport();
}
private ServerTransport<CloseableChannel> createWebSocketTransport() {
HttpServer httpServer = HttpServer.create();
if (this.resourceFactory != null) {
httpServer = httpServer.runOn(this.resourceFactory.getLoopResources());
}
if (Ssl.isEnabled(this.ssl)) {
httpServer = customizeSslConfiguration(httpServer);
}
return WebsocketServerTransport.create(httpServer.bindAddress(this::getListenAddress));
}
private HttpServer customizeSslConfiguration(HttpServer httpServer) {
return new HttpServerSslCustomizer(this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles())
.apply(httpServer);
}
private ServerTransport<CloseableChannel> createTcpTransport() {
TcpServer tcpServer = TcpServer.create();
if (this.resourceFactory != null) {
tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources());
}
if (Ssl.isEnabled(this.ssl)) {
tcpServer = new TcpServerSslCustomizer(this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles())
.apply(tcpServer);
}
return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress));
}
private SslBundle getSslBundle() {
return WebServerSslBundle.get(this.ssl, this.sslBundles);
}
protected final Map<String, SslBundle> getServerNameSslBundles() {
return this.ssl.getServerNameBundles()
.stream()
.collect(Collectors.toMap(Ssl.ServerNameSslBundle::serverName, this::getBundle));
}
private SslBundle getBundle(ServerNameSslBundle serverNameSslBundle) {
return this.sslBundles.getBundle(serverNameSslBundle.bundle());
}
private InetSocketAddress getListenAddress() {
if (this.address != null) {
return new InetSocketAddress(this.address.getHostAddress(), this.port);
}
return new InetSocketAddress(this.port);
}
private abstract static class SslCustomizer {
private final ClientAuth clientAuth;
protected SslCustomizer(ClientAuth clientAuth) {
this.clientAuth = clientAuth;
}
protected final AbstractProtocolSslContextSpec<?> createSslContextSpec(SslBundle sslBundle) {
AbstractProtocolSslContextSpec<?> sslContextSpec = Http11SslContextSpec
.forServer(sslBundle.getManagers().getKeyManagerFactory());
return sslContextSpec.configure((builder) -> {
builder.trustManager(sslBundle.getManagers().getTrustManagerFactory());
SslOptions options = sslBundle.getOptions();
builder.protocols(options.getEnabledProtocols());
builder.ciphers(SslOptions.asSet(options.getCiphers()));
builder.clientAuth(this.clientAuth);
});
}
}
private static final class TcpServerSslCustomizer extends SslCustomizer {
private final SslBundle sslBundle;
private TcpServerSslCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle,
Map<String, SslBundle> serverNameSslBundles) {
super(Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
this.sslBundle = sslBundle;
}
private TcpServer apply(TcpServer server) {
GenericSslContextSpec<?> sslContextSpec = createSslContextSpec(this.sslBundle);
return server.secure((spec) -> spec.sslContext(sslContextSpec));
}
}
private static final class HttpServerSslCustomizer extends SslCustomizer {
private final SslProvider sslProvider;
private final Map<String, SslProvider> serverNameSslProviders;
private HttpServerSslCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle,
Map<String, SslBundle> serverNameSslBundles) {
super(Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE));
this.sslProvider = createSslProvider(sslBundle);
this.serverNameSslProviders = createServerNameSslProviders(serverNameSslBundles);
}
private HttpServer apply(HttpServer server) {
return server.secure(this::applySecurity);
}
private void applySecurity(SslContextSpec spec) {
spec.sslContext(this.sslProvider.getSslContext()).setSniAsyncMappings((serverName, promise) -> {
SslProvider provider = (serverName != null) ? this.serverNameSslProviders.get(serverName)
: this.sslProvider;
return promise.setSuccess(provider);
});
}
private Map<String, SslProvider> createServerNameSslProviders(Map<String, SslBundle> serverNameSslBundles) {
Map<String, SslProvider> serverNameSslProviders = new HashMap<>();
serverNameSslBundles.forEach(
(serverName, sslBundle) -> serverNameSslProviders.put(serverName, createSslProvider(sslBundle)));
return serverNameSslProviders;
}
private SslProvider createSslProvider(SslBundle sslBundle) {
return SslProvider.builder().sslContext((GenericSslContextSpec<?>) createSslContextSpec(sslBundle)).build();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Reactor Netty based RSocket server implementation.
*/
package org.springframework.boot.rsocket.netty;

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2025 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.rsocket.server;
import java.net.InetAddress;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.server.Ssl;
import org.springframework.util.unit.DataSize;
/**
* A configurable {@link RSocketServerFactory}.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 2.2.0
*/
public interface ConfigurableRSocketServerFactory {
/**
* Set the port that the server should listen on. If not specified port '9898' will be
* used.
* @param port the port to set
*/
void setPort(int port);
/**
* Specify the maximum transmission unit. Frames larger than the specified
* {@code fragmentSize} are fragmented.
* @param fragmentSize the fragment size
* @since 2.4.0
*/
void setFragmentSize(DataSize fragmentSize);
/**
* Set the specific network address that the server should bind to.
* @param address the address to set (defaults to {@code null})
*/
void setAddress(InetAddress address);
/**
* Set the transport that the RSocket server should use.
* @param transport the transport protocol to use
*/
void setTransport(RSocketServer.Transport transport);
/**
* Sets the SSL configuration that will be applied to the server's default connector.
* @param ssl the SSL configuration
*/
void setSsl(Ssl ssl);
/**
* Sets an SSL bundle that can be used to get SSL configuration.
* @param sslBundles the SSL bundles
* @since 3.1.0
*/
void setSslBundles(SslBundles sslBundles);
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2025 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.rsocket.server;
import java.net.InetSocketAddress;
/**
* Simple interface that represents a fully configured RSocket server. Allows the server
* to be {@link #start() started} and {@link #stop() stopped}.
*
* @author Brian Clozel
* @since 2.2.0
*/
public interface RSocketServer {
/**
* Starts the RSocket server. Calling this method on an already started server has no
* effect.
* @throws RSocketServerException if the server cannot be started
*/
void start() throws RSocketServerException;
/**
* Stops the RSocket server. Calling this method on an already stopped server has no
* effect.
* @throws RSocketServerException if the server cannot be stopped
*/
void stop() throws RSocketServerException;
/**
* Return the address this server is listening on.
* @return the address
*/
InetSocketAddress address();
/**
* Choice of transport protocol for the RSocket server.
*/
enum Transport {
/**
* TCP transport protocol.
*/
TCP,
/**
* WebSocket transport protocol.
*/
WEBSOCKET
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.rsocket.server;
import io.rsocket.core.RSocketServer;
/**
* Callback interface that can be used to customize a {@link RSocketServer}.
*
* @author Brian Clozel
* @since 2.3.0
* @see RSocketServer
*/
@FunctionalInterface
public interface RSocketServerCustomizer {
/**
* Callback to customize a {@link RSocketServer} instance.
* @param rSocketServer the RSocket server to customize
*/
void customize(RSocketServer rSocketServer);
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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.rsocket.server;
/**
* Exceptions thrown by an RSocket server.
*
* @author Brian Clozel
* @since 2.2.0
*/
public class RSocketServerException extends RuntimeException {
public RSocketServerException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.rsocket.server;
import io.rsocket.SocketAcceptor;
/**
* Factory interface that can be used to create a reactive {@link RSocketServer}.
*
* @author Brian Clozel
* @since 2.2.0
*/
@FunctionalInterface
public interface RSocketServerFactory {
/**
* Gets a new fully configured but paused {@link RSocketServer} instance. Clients
* should not be able to connect to the returned server until
* {@link RSocketServer#start()} is called (which happens when the
* {@code ApplicationContext} has been fully refreshed).
* @param socketAcceptor the socket acceptor
* @return a fully configured and started {@link RSocketServer}
* @see RSocketServer#stop()
*/
RSocketServer create(SocketAcceptor socketAcceptor);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Support for RSocket servers.
*/
package org.springframework.boot.rsocket.server;

View File

@@ -0,0 +1,3 @@
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer

View File

@@ -0,0 +1,4 @@
org.springframework.boot.rsocket.autoconfigure.RSocketMessagingAutoConfiguration
org.springframework.boot.rsocket.autoconfigure.RSocketRequesterAutoConfiguration
org.springframework.boot.rsocket.autoconfigure.RSocketServerAutoConfiguration
org.springframework.boot.rsocket.autoconfigure.RSocketStrategiesAutoConfiguration

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RSocketMessagingAutoConfiguration}.
*
* @author Brian Clozel
* @author Madhura Bhave
*/
class RSocketMessagingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RSocketMessagingAutoConfiguration.class))
.withUserConfiguration(BaseConfiguration.class);
@Test
void shouldCreateDefaultBeans() {
this.contextRunner.run((context) -> assertThat(context).getBeans(RSocketMessageHandler.class).hasSize(1));
}
@Test
void shouldFailOnMissingStrategies() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(RSocketMessagingAutoConfiguration.class))
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure().getMessage()).contains("No qualifying bean of type "
+ "'org.springframework.messaging.rsocket.RSocketStrategies' available");
});
}
@Test
void shouldUseCustomSocketAcceptor() {
this.contextRunner.withUserConfiguration(CustomMessageHandler.class)
.run((context) -> assertThat(context).getBeanNames(RSocketMessageHandler.class)
.containsOnly("customMessageHandler"));
}
@Test
void shouldApplyMessageHandlerCustomizers() {
this.contextRunner.withUserConfiguration(CustomizerConfiguration.class).run((context) -> {
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
assertThat(handler.getDefaultDataMimeType()).isEqualTo(MimeType.valueOf("application/json"));
});
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@Bean
RSocketStrategies rSocketStrategies() {
return RSocketStrategies.builder()
.encoder(CharSequenceEncoder.textPlainOnly())
.decoder(StringDecoder.allMimeTypes())
.build();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomMessageHandler {
@Bean
RSocketMessageHandler customMessageHandler() {
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
RSocketStrategies strategies = RSocketStrategies.builder()
.encoder(CharSequenceEncoder.textPlainOnly())
.decoder(StringDecoder.allMimeTypes())
.build();
messageHandler.setRSocketStrategies(strategies);
return messageHandler;
}
}
@Configuration(proxyBeanMethods = false)
static class CustomizerConfiguration {
@Bean
RSocketMessageHandlerCustomizer customizer() {
return (messageHandler) -> messageHandler.setDefaultDataMimeType(MimeType.valueOf("application/json"));
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import org.junit.jupiter.api.Test;
import reactor.netty.http.server.WebsocketServerSpec;
import org.springframework.boot.rsocket.autoconfigure.RSocketProperties.Server.Spec;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RSocketProperties}.
*
* @author Stephane Nicoll
*/
class RSocketPropertiesTests {
@Test
void defaultServerSpecValuesAreConsistent() {
WebsocketServerSpec spec = WebsocketServerSpec.builder().build();
Spec properties = new RSocketProperties().getServer().getSpec();
assertThat(properties.getProtocols()).isEqualTo(spec.protocols());
assertThat(properties.getMaxFramePayloadLength().toBytes()).isEqualTo(spec.maxFramePayloadLength());
assertThat(properties.isHandlePing()).isEqualTo(spec.handlePing());
assertThat(properties.isCompress()).isEqualTo(spec.compress());
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.rsocket.RSocketConnectorConfigurer;
import org.springframework.messaging.rsocket.RSocketRequester;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RSocketRequesterAutoConfiguration}
*
* @author Brian Clozel
* @author Nguyen Bao Sach
*/
class RSocketRequesterAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class, RSocketRequesterAutoConfiguration.class));
@Test
void shouldCreateBuilder() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RSocketRequester.Builder.class));
}
@Test
void shouldGetPrototypeScopedBean() {
this.contextRunner.run((context) -> {
RSocketRequester.Builder first = context.getBean(RSocketRequester.Builder.class);
RSocketRequester.Builder second = context.getBean(RSocketRequester.Builder.class);
assertThat(first).isNotEqualTo(second);
});
}
@Test
void shouldNotCreateBuilderIfAlreadyPresent() {
this.contextRunner.withUserConfiguration(CustomRSocketRequesterBuilder.class).run((context) -> {
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
assertThat(builder).isInstanceOf(MyRSocketRequesterBuilder.class);
});
}
@Test
void shouldCreateBuilderWithAvailableRSocketConnectorConfigurers() {
RSocketConnectorConfigurer first = mock(RSocketConnectorConfigurer.class);
RSocketConnectorConfigurer second = mock(RSocketConnectorConfigurer.class);
this.contextRunner.withBean("first", RSocketConnectorConfigurer.class, () -> first)
.withBean("second", RSocketConnectorConfigurer.class, () -> second)
.run((context) -> {
assertThat(context).getBeans(RSocketConnectorConfigurer.class).hasSize(2);
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
assertThat(builder).extracting("rsocketConnectorConfigurers", as(InstanceOfAssertFactories.LIST))
.containsExactly(first, second);
});
}
@Configuration(proxyBeanMethods = false)
static class CustomRSocketRequesterBuilder {
@Bean
MyRSocketRequesterBuilder myRSocketRequesterBuilder() {
return mock(MyRSocketRequesterBuilder.class);
}
}
interface MyRSocketRequesterBuilder extends RSocketRequester.Builder {
}
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer;
import org.springframework.boot.rsocket.context.RSocketServerBootstrap;
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.boot.ssl.NoSuchSslBundleException;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RSocketServerAutoConfiguration}.
*
* @author Brian Clozel
* @author Verónica Vásquez
* @author Scott Frederick
*/
class RSocketServerAutoConfigurationTests {
@Test
void shouldNotCreateBeansByDefault() {
contextRunner().run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
.doesNotHaveBean(RSocketServerFactory.class)
.doesNotHaveBean(RSocketServerBootstrap.class));
}
@Test
void shouldNotCreateDefaultBeansForReactiveWebAppWithoutMapping() {
reactiveWebContextRunner()
.run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
.doesNotHaveBean(RSocketServerFactory.class)
.doesNotHaveBean(RSocketServerBootstrap.class));
}
@Test
void shouldNotCreateDefaultBeansForReactiveWebAppWithWrongTransport() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.transport=tcp", "spring.rsocket.server.mapping-path=/rsocket")
.run((context) -> assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class)
.doesNotHaveBean(RSocketServerFactory.class)
.doesNotHaveBean(RSocketServerBootstrap.class));
}
@Test
void shouldCreateDefaultBeansForReactiveWebApp() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket")
.run((context) -> assertThat(context).hasSingleBean(RSocketWebSocketNettyRouteProvider.class));
}
@Test
void shouldCreateDefaultBeansForRSocketServerWhenPortIsSet() {
reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0")
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
.hasSingleBean(RSocketServerBootstrap.class)
.hasSingleBean(RSocketServerCustomizer.class));
}
@Test
void shouldSetLocalServerPortWhenRSocketServerPortIsSet() {
reactiveWebContextRunner().withPropertyValues("spring.rsocket.server.port=0")
.withInitializer(new RSocketPortInfoApplicationContextInitializer())
.run((context) -> {
assertThat(context).hasSingleBean(RSocketServerFactory.class)
.hasSingleBean(RSocketServerBootstrap.class)
.hasSingleBean(RSocketServerCustomizer.class);
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isNotNull();
});
}
@Test
void shouldSetFragmentWhenRSocketServerFragmentSizeIsSet() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=12KB")
.run((context) -> {
assertThat(context).hasSingleBean(RSocketServerFactory.class);
RSocketServerFactory factory = context.getBean(RSocketServerFactory.class);
assertThat(factory).hasFieldOrPropertyWithValue("fragmentSize", DataSize.ofKilobytes(12));
});
}
@Test
void shouldFailToSetFragmentWhenRSocketServerFragmentSizeIsBelow64() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.fragment-size=60B")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure())
.hasMessageContaining("The smallest allowed mtu size is 64 bytes, provided: 60");
});
}
@Test
@WithPackageResources("test.jks")
void shouldUseSslWhenRocketServerSslIsConfigured() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.ssl.keyStore=classpath:test.jks",
"spring.rsocket.server.ssl.keyPassword=password", "spring.rsocket.server.port=0")
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
.hasSingleBean(RSocketServerBootstrap.class)
.hasSingleBean(RSocketServerCustomizer.class)
.getBean(RSocketServerFactory.class)
.hasFieldOrPropertyWithValue("ssl.keyStore", "classpath:test.jks")
.hasFieldOrPropertyWithValue("ssl.keyPassword", "password"));
}
@Test
@Disabled
@WithPackageResources("test.jks")
void shouldUseSslWhenRocketServerSslIsConfiguredWithSslBundle() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle",
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.key.password=password")
.run((context) -> assertThat(context).hasSingleBean(RSocketServerFactory.class)
.hasSingleBean(RSocketServerBootstrap.class)
.hasSingleBean(RSocketServerCustomizer.class)
.getBean(RSocketServerFactory.class)
.hasFieldOrPropertyWithValue("sslBundle.details.keyStore", "classpath:test.jks")
.hasFieldOrPropertyWithValue("sslBundle.details.keyPassword", "password"));
}
@Test
void shouldFailWhenSslIsConfiguredWithMissingBundle() {
reactiveWebContextRunner()
.withPropertyValues("spring.rsocket.server.port=0", "spring.rsocket.server.ssl.bundle=test-bundle")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(NoSuchSslBundleException.class)
.withFailMessage("SSL bundle name 'test-bundle' is not valid");
});
}
@Test
void shouldUseCustomServerBootstrap() {
contextRunner().withUserConfiguration(CustomServerBootstrapConfig.class)
.run((context) -> assertThat(context).getBeanNames(RSocketServerBootstrap.class)
.containsExactly("customServerBootstrap"));
}
@Test
void shouldUseCustomNettyRouteProvider() {
reactiveWebContextRunner().withUserConfiguration(CustomNettyRouteProviderConfig.class)
.withPropertyValues("spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket")
.run((context) -> assertThat(context).getBeanNames(RSocketWebSocketNettyRouteProvider.class)
.containsExactly("customNettyRouteProvider"));
}
@Test
void whenSpringWebIsNotPresentThenEmbeddedServerConfigurationBacksOff() {
contextRunner().withClassLoader(new FilteredClassLoader(ReactorResourceFactory.class))
.withPropertyValues("spring.rsocket.server.port=0")
.run((context) -> assertThat(context).doesNotHaveBean(RSocketServerFactory.class));
}
private ApplicationContextRunner contextRunner() {
return new ApplicationContextRunner().withUserConfiguration(BaseConfiguration.class)
.withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class));
}
private ReactiveWebApplicationContextRunner reactiveWebContextRunner() {
return new ReactiveWebApplicationContextRunner().withUserConfiguration(BaseConfiguration.class)
.withConfiguration(AutoConfigurations.of(RSocketServerAutoConfiguration.class, SslAutoConfiguration.class));
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@Bean
RSocketMessageHandler messageHandler() {
RSocketMessageHandler messageHandler = new RSocketMessageHandler();
messageHandler.setRSocketStrategies(RSocketStrategies.builder()
.encoder(CharSequenceEncoder.textPlainOnly())
.decoder(StringDecoder.allMimeTypes())
.build());
return messageHandler;
}
}
@Configuration(proxyBeanMethods = false)
static class CustomServerBootstrapConfig {
@Bean
RSocketServerBootstrap customServerBootstrap() {
return mock(RSocketServerBootstrap.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomNettyRouteProviderConfig {
@Bean
RSocketWebSocketNettyRouteProvider customNettyRouteProvider() {
return mock(RSocketWebSocketNettyRouteProvider.class);
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.web.util.pattern.PathPatternRouteMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RSocketStrategiesAutoConfiguration}
*
* @author Brian Clozel
*/
class RSocketStrategiesAutoConfigurationTests {
@SuppressWarnings("removal")
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class))
.withBean(org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class)
.withBean(ObjectMapper.class);
@Test
@SuppressWarnings("removal")
void shouldCreateDefaultBeans() {
this.contextRunner.run((context) -> {
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
assertThat(strategies.decoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborDecoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonDecoder.class);
assertThat(strategies.encoders())
.hasAtLeastOneElementOfType(org.springframework.http.codec.cbor.Jackson2CborEncoder.class)
.hasAtLeastOneElementOfType(org.springframework.http.codec.json.Jackson2JsonEncoder.class);
assertThat(strategies.routeMatcher()).isInstanceOf(PathPatternRouteMatcher.class);
});
}
@Test
void shouldUseCustomStrategies() {
this.contextRunner.withUserConfiguration(UserStrategies.class).run((context) -> {
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
assertThat(context.getBeanNamesForType(RSocketStrategies.class)).contains("customRSocketStrategies");
});
}
@Test
void shouldUseStrategiesCustomizer() {
this.contextRunner.withUserConfiguration(StrategiesCustomizer.class).run((context) -> {
assertThat(context).getBeans(RSocketStrategies.class).hasSize(1);
RSocketStrategies strategies = context.getBean(RSocketStrategies.class);
assertThat(strategies.decoders()).hasAtLeastOneElementOfType(CustomDecoder.class);
assertThat(strategies.encoders()).hasAtLeastOneElementOfType(CustomEncoder.class);
});
}
@Configuration(proxyBeanMethods = false)
static class UserStrategies {
@Bean
RSocketStrategies customRSocketStrategies() {
return RSocketStrategies.builder()
.encoder(CharSequenceEncoder.textPlainOnly())
.decoder(StringDecoder.textPlainOnly())
.build();
}
}
@Configuration(proxyBeanMethods = false)
static class StrategiesCustomizer {
@Bean
RSocketStrategiesCustomizer myCustomizer() {
return (strategies) -> strategies.encoder(mock(CustomEncoder.class)).decoder(mock(CustomDecoder.class));
}
}
interface CustomEncoder extends Encoder<String> {
}
interface CustomDecoder extends Decoder<String> {
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2012-2025 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.rsocket.autoconfigure;
import java.net.URI;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link RSocketWebSocketNettyRouteProvider}.
*
* @author Brian Clozel
*/
class RSocketWebSocketNettyRouteProviderTests {
@Test
void webEndpointsShouldWork() {
new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class,
RSocketStrategiesAutoConfiguration.class, RSocketServerAutoConfiguration.class,
RSocketMessagingAutoConfiguration.class, RSocketRequesterAutoConfiguration.class))
.withUserConfiguration(WebConfiguration.class)
.withPropertyValues("spring.rsocket.server.transport=websocket",
"spring.rsocket.server.mapping-path=/rsocket")
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run((context) -> {
ReactiveWebServerApplicationContext serverContext = (ReactiveWebServerApplicationContext) context
.getSourceApplicationContext();
RSocketRequester requester = createRSocketRequester(context, serverContext.getWebServer());
TestProtocol rsocketResponse = requester.route("websocket")
.data(new TestProtocol("rsocket"))
.retrieveMono(TestProtocol.class)
.block(Duration.ofSeconds(3));
assertThat(rsocketResponse.getName()).isEqualTo("rsocket");
WebTestClient client = createWebTestClient(serverContext.getWebServer());
client.get()
.uri("/protocol")
.exchange()
.expectStatus()
.isOk()
.expectBody()
.jsonPath("name")
.isEqualTo("http");
});
}
private WebTestClient createWebTestClient(WebServer server) {
return WebTestClient.bindToServer()
.baseUrl("http://localhost:" + server.getPort())
.responseTimeout(Duration.ofMinutes(5))
.build();
}
private RSocketRequester createRSocketRequester(ApplicationContext context, WebServer server) {
int port = server.getPort();
RSocketRequester.Builder builder = context.getBean(RSocketRequester.Builder.class);
return builder.dataMimeType(MediaType.APPLICATION_CBOR)
.websocket(URI.create("ws://localhost:" + port + "/rsocket"));
}
@EnableWebFlux
@Configuration(proxyBeanMethods = false)
static class WebConfiguration {
@Bean
HttpHandler httpHandler(ApplicationContext context) {
return WebHttpHandlerBuilder.applicationContext(context).build();
}
@Bean
WebController webController() {
return new WebController();
}
@Bean
NettyReactiveWebServerFactory customServerFactory(RSocketWebSocketNettyRouteProvider routeProvider) {
NettyReactiveWebServerFactory serverFactory = new NettyReactiveWebServerFactory(0);
serverFactory.addRouteProviders(routeProvider);
return serverFactory;
}
@Bean
ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
@SuppressWarnings({ "removal", "deprecation" })
org.springframework.http.converter.json.Jackson2ObjectMapperBuilder objectMapperBuilder() {
return new org.springframework.http.converter.json.Jackson2ObjectMapperBuilder();
}
}
@Controller
static class WebController {
@GetMapping(path = "/protocol", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
TestProtocol testWebEndpoint() {
return new TestProtocol("http");
}
@MessageMapping("websocket")
TestProtocol testRSocketEndpoint() {
return new TestProtocol("rsocket");
}
}
public static class TestProtocol {
private String name;
TestProtocol() {
}
TestProtocol(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2025 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.rsocket.context;
import java.net.InetSocketAddress;
import org.junit.jupiter.api.Test;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RSocketPortInfoApplicationContextInitializer}.
*
* @author Spencer Gibb
* @author Andy Wilkinson
*/
class RSocketPortInfoApplicationContextInitializerTests {
@Test
void whenServerHasAddressThenInitializerSetsPortProperty() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) {
context.getBean(RSocketPortInfoApplicationContextInitializer.class).initialize(context);
RSocketServer server = mock(RSocketServer.class);
given(server.address()).willReturn(new InetSocketAddress(65535));
context.publishEvent(new RSocketServerInitializedEvent(server));
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isEqualTo("65535");
}
}
@Test
void whenServerHasNoAddressThenInitializerDoesNotSetPortProperty() {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) {
context.getBean(RSocketPortInfoApplicationContextInitializer.class).initialize(context);
RSocketServer server = mock(RSocketServer.class);
context.publishEvent(new RSocketServerInitializedEvent(server));
then(server).should().address();
assertThat(context.getEnvironment().getProperty("local.rsocket.server.port")).isNull();
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
RSocketPortInfoApplicationContextInitializer rSocketPortInfoApplicationContextInitializer() {
return new RSocketPortInfoApplicationContextInitializer();
}
}
}

View File

@@ -0,0 +1,435 @@
/*
* Copyright 2012-2025 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.rsocket.netty;
import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.Callable;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.client.WebsocketClientTransport;
import io.rsocket.util.DefaultPayload;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import reactor.core.publisher.Mono;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
import reactor.netty.tcp.TcpClient;
import reactor.test.StepVerifier;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.rsocket.server.RSocketServer.Transport;
import org.springframework.boot.rsocket.server.RSocketServerCustomizer;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundleKey;
import org.springframework.boot.ssl.jks.JksSslStoreBundle;
import org.springframework.boot.ssl.jks.JksSslStoreDetails;
import org.springframework.boot.ssl.pem.PemSslStoreBundle;
import org.springframework.boot.ssl.pem.PemSslStoreDetails;
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.web.server.Ssl;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.client.ReactorResourceFactory;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketStrategies;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.will;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NettyRSocketServerFactory}
*
* @author Brian Clozel
* @author Leo Li
* @author Chris Bono
* @author Scott Frederick
*/
class NettyRSocketServerFactoryTests {
private NettyRSocketServer server;
private RSocketRequester requester;
@AfterEach
void tearDown() {
if (this.requester != null) {
this.requester.rsocketClient().dispose();
}
if (this.server != null) {
try {
this.server.stop();
}
catch (Exception ex) {
// Ignore
}
}
}
private NettyRSocketServerFactory getFactory() {
NettyRSocketServerFactory factory = new NettyRSocketServerFactory();
factory.setPort(0);
return factory;
}
@Test
void specificPort() {
NettyRSocketServerFactory factory = getFactory();
int specificPort = doWithRetry(() -> {
factory.setPort(0);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
return this.server.address().getPort();
});
this.requester = createRSocketTcpClient();
assertThat(this.server.address().getPort()).isEqualTo(specificPort);
checkEchoRequest();
}
@Test
void websocketTransport() {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(RSocketServer.Transport.WEBSOCKET);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = createRSocketWebSocketClient();
checkEchoRequest();
}
@Test
void websocketTransportWithReactorResource() {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(RSocketServer.Transport.WEBSOCKET);
ReactorResourceFactory resourceFactory = new ReactorResourceFactory();
resourceFactory.afterPropertiesSet();
factory.setResourceFactory(resourceFactory);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = createRSocketWebSocketClient();
checkEchoRequest();
}
@Test
void serverCustomizers() {
NettyRSocketServerFactory factory = getFactory();
RSocketServerCustomizer[] customizers = new RSocketServerCustomizer[2];
for (int i = 0; i < customizers.length; i++) {
customizers[i] = mock(RSocketServerCustomizer.class);
will((invocation) -> invocation.getArgument(0)).given(customizers[i])
.customize(any(io.rsocket.core.RSocketServer.class));
}
factory.setRSocketServerCustomizers(Arrays.asList(customizers));
this.server = factory.create(new EchoRequestResponseAcceptor());
InOrder ordered = inOrder((Object[]) customizers);
for (RSocketServerCustomizer customizer : customizers) {
ordered.verify(customizer).customize(any(io.rsocket.core.RSocketServer.class));
}
}
@Test
@WithPackageResources("test.jks")
void tcpTransportBasicSslFromClassPath() {
testBasicSslWithKeyStore("classpath:test.jks", "password", Transport.TCP);
}
@Test
@WithPackageResources("test.jks")
void tcpTransportBasicSslFromFileSystem(@ResourcePath("test.jks") String keyStore) {
testBasicSslWithKeyStore(keyStore, "password", Transport.TCP);
}
@Test
@WithPackageResources("test.jks")
void websocketTransportBasicSslFromClassPath() {
testBasicSslWithKeyStore("classpath:test.jks", "password", Transport.WEBSOCKET);
}
@Test
@WithPackageResources("test.jks")
void websocketTransportBasicSslFromFileSystem(@ResourcePath("test.jks") String keyStore) {
testBasicSslWithKeyStore(keyStore, "password", Transport.WEBSOCKET);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void tcpTransportBasicSslCertificateFromClassPath() {
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
Transport.TCP);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void tcpTransportBasicSslCertificateFromFileSystem(@ResourcePath("test-cert.pem") String testCert,
@ResourcePath("test-key.pem") String testKey) {
testBasicSslWithPemCertificate(testCert, testKey, testCert, Transport.TCP);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void websocketTransportBasicSslCertificateFromClassPath() {
testBasicSslWithPemCertificate("classpath:test-cert.pem", "classpath:test-key.pem", "classpath:test-cert.pem",
Transport.WEBSOCKET);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void websocketTransportBasicSslCertificateFromFileSystem(@ResourcePath("test-cert.pem") String testCert,
@ResourcePath("test-key.pem") String testKey) {
testBasicSslWithPemCertificate(testCert, testKey, testCert, Transport.WEBSOCKET);
}
@Test
@WithPackageResources("test.jks")
void tcpTransportBasicSslFromClassPathWithBundle() {
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.TCP);
}
@Test
@WithPackageResources("test.jks")
void tcpTransportBasicSslFromFileSystemWithBundle(@ResourcePath("test.jks") String keyStore) {
testBasicSslWithKeyStoreFromBundle(keyStore, "password", Transport.TCP);
}
@Test
@WithPackageResources("test.jks")
void websocketTransportBasicSslFromClassPathWithBundle() {
testBasicSslWithKeyStoreFromBundle("classpath:test.jks", "password", Transport.WEBSOCKET);
}
@Test
@WithPackageResources("test.jks")
void websocketTransportBasicSslFromFileSystemWithBundle(@ResourcePath("test.jks") String keyStore) {
testBasicSslWithKeyStoreFromBundle(keyStore, "password", Transport.WEBSOCKET);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void tcpTransportBasicSslCertificateFromClassPathWithBundle() {
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
"classpath:test-cert.pem", Transport.TCP);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void tcpTransportBasicSslCertificateFromFileSystemWithBundle(@ResourcePath("test-cert.pem") String testCert,
@ResourcePath("test-key.pem") String testKey) {
testBasicSslWithPemCertificateFromBundle(testCert, testKey, testCert, Transport.TCP);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void websocketTransportBasicSslCertificateFromClassPathWithBundle() {
testBasicSslWithPemCertificateFromBundle("classpath:test-cert.pem", "classpath:test-key.pem",
"classpath:test-cert.pem", Transport.WEBSOCKET);
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem" })
void websocketTransportBasicSslCertificateFromFileSystemWithBundle(@ResourcePath("test-cert.pem") String testCert,
@ResourcePath("test-key.pem") String testKey) {
testBasicSslWithPemCertificateFromBundle(testCert, testKey, testCert, Transport.WEBSOCKET);
}
private void checkEchoRequest() {
String payload = "test payload";
Mono<String> response = this.requester.route("test").data(payload).retrieveMono(String.class);
StepVerifier.create(response).expectNext(payload).expectComplete().verify(Duration.ofSeconds(30));
}
private void testBasicSslWithKeyStore(String keyStore, String keyPassword, Transport transport) {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(transport);
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyPassword(keyPassword);
factory.setSsl(ssl);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
: createSecureRSocketWebSocketClient();
checkEchoRequest();
}
private void testBasicSslWithPemCertificate(String certificate, String certificatePrivateKey,
String trustCertificate, Transport transport) {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(transport);
Ssl ssl = new Ssl();
ssl.setCertificate(certificate);
ssl.setCertificatePrivateKey(certificatePrivateKey);
ssl.setTrustCertificate(trustCertificate);
ssl.setKeyStorePassword("");
factory.setSsl(ssl);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
: createSecureRSocketWebSocketClient();
checkEchoRequest();
}
private void testBasicSslWithKeyStoreFromBundle(String keyStore, String keyPassword, Transport transport) {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(transport);
JksSslStoreDetails keyStoreDetails = JksSslStoreDetails.forLocation(keyStore);
JksSslStoreDetails trustStoreDetails = null;
SslBundle sslBundle = SslBundle.of(new JksSslStoreBundle(keyStoreDetails, trustStoreDetails),
SslBundleKey.of(keyPassword));
factory.setSsl(Ssl.forBundle("test"));
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
: createSecureRSocketWebSocketClient();
checkEchoRequest();
}
private void testBasicSslWithPemCertificateFromBundle(String certificate, String certificatePrivateKey,
String trustCertificate, Transport transport) {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(transport);
PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(certificate)
.withPrivateKey(certificatePrivateKey);
PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(trustCertificate);
SslBundle sslBundle = SslBundle.of(new PemSslStoreBundle(keyStoreDetails, trustStoreDetails));
factory.setSsl(Ssl.forBundle("test"));
factory.setSslBundles(new DefaultSslBundleRegistry("test", sslBundle));
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = (transport == Transport.TCP) ? createSecureRSocketTcpClient()
: createSecureRSocketWebSocketClient();
checkEchoRequest();
}
@Test
void tcpTransportSslRejectsInsecureClient() {
NettyRSocketServerFactory factory = getFactory();
factory.setTransport(Transport.TCP);
Ssl ssl = new Ssl();
ssl.setKeyStore("classpath:org/springframework/boot/rsocket/netty/test.jks");
ssl.setKeyPassword("password");
factory.setSsl(ssl);
this.server = factory.create(new EchoRequestResponseAcceptor());
this.server.start();
this.requester = createRSocketTcpClient();
String payload = "test payload";
Mono<String> responseMono = this.requester.route("test").data(payload).retrieveMono(String.class);
StepVerifier.create(responseMono)
.verifyErrorSatisfies((ex) -> assertThat(ex).isInstanceOf(ClosedChannelException.class));
}
private RSocketRequester createRSocketTcpClient() {
return createRSocketRequesterBuilder().transport(TcpClientTransport.create(createTcpClient()));
}
private RSocketRequester createRSocketWebSocketClient() {
return createRSocketRequesterBuilder().transport(WebsocketClientTransport.create(createHttpClient(), "/"));
}
private RSocketRequester createSecureRSocketTcpClient() {
return createRSocketRequesterBuilder().transport(TcpClientTransport.create(createSecureTcpClient()));
}
private RSocketRequester createSecureRSocketWebSocketClient() {
return createRSocketRequesterBuilder()
.transport(WebsocketClientTransport.create(createSecureHttpClient(), "/"));
}
private HttpClient createSecureHttpClient() {
HttpClient httpClient = createHttpClient();
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE));
return httpClient.secure((spec) -> spec.sslContext(sslContextSpec));
}
private HttpClient createHttpClient() {
Assertions.assertThat(this.server).isNotNull();
InetSocketAddress address = this.server.address();
return HttpClient.create().host(address.getHostName()).port(address.getPort());
}
private TcpClient createSecureTcpClient() {
TcpClient tcpClient = createTcpClient();
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE));
return tcpClient.secure((spec) -> spec.sslContext(sslContextSpec));
}
private TcpClient createTcpClient() {
Assertions.assertThat(this.server).isNotNull();
InetSocketAddress address = this.server.address();
return TcpClient.create().host(address.getHostName()).port(address.getPort());
}
private RSocketRequester.Builder createRSocketRequesterBuilder() {
RSocketStrategies strategies = RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
.build();
return RSocketRequester.builder().rsocketStrategies(strategies);
}
private <T> T doWithRetry(Callable<T> action) {
Exception lastFailure = null;
for (int i = 0; i < 10; i++) {
try {
return action.call();
}
catch (Exception ex) {
lastFailure = ex;
}
}
throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure);
}
static class EchoRequestResponseAcceptor implements SocketAcceptor {
@Override
public Mono<RSocket> accept(ConnectionSetupPayload setupPayload, RSocket rSocket) {
return Mono.just(new RSocket() {
@Override
public Mono<Payload> requestResponse(Payload payload) {
return Mono.just(DefaultPayload.create(payload));
}
});
}
}
}

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,59 @@
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD8B1saBN8Y0ZjX
Q/pvnv2hZa+cBBkFHVkgw+tdzgdcO5Kjv2rnRKd3Oh8Qwdxj3BC9GpicF4GOgkDG
LrMrYbmV/xGUnNh3YpUwbv+U/pm3VnuSZCk4aYgNEWHrnEHyrge87+MqYqGuMIke
A5z7GV44PeThfKQs2BLLL/ME+Pb/jN6IVlIHxdBT5TBQkqNGFU7swdZoQ5Viqv44
UNdVqU5pP3UF/perbOm2CHCbeLiLRvmuvGuBbrbko2XUwBNH+UjmEQaRh/epoy8B
D31vGY87ym/YOEdpIrqD8bwg7NWP/Fdsryqi8p+U8fcfw4xZjotA0Sv04BC7zXHg
m1AyZoVzAgMBAAECggEAfEqiZqANaF+BqXQIb4Dw42ZTJzWsIyYYnPySOGZRoe5t
QJ03uwtULYv34xtANe1DQgd6SMyc46ugBzzjtprQ3ET5Jhn99U6kdcjf+dpf85dO
hOEppP0CkDNI39nleinSfh6uIOqYgt/D143/nqQhn8oCdSOzkbwT9KnWh1bC9T7I
vFjGfElvt1/xl88qYgrWgYLgXaencNGgiv/4/M0FNhiHEGsVC7SCu6kapC/WIQpE
5IdV+HR+tiLoGZhXlhqorY7QC4xKC4wwafVSiFxqDOQAuK+SMD4TCEv0Aop+c+SE
YBigVTmgVeJkjK7IkTEhKkAEFmRF5/5w+bZD9FhTNQKBgQD+4fNG1ChSU8RdizZT
5dPlDyAxpETSCEXFFVGtPPh2j93HDWn7XugNyjn5FylTH507QlabC+5wZqltdIjK
GRB5MIinQ9/nR2fuwGc9s+0BiSEwNOUB1MWm7wWL/JUIiKq6sTi6sJIfsYg79zco
qxl5WE94aoINx9Utq1cdWhwJTQKBgQD9IjPksd4Jprz8zMrGLzR8k1gqHyhv24qY
EJ7jiHKKAP6xllTUYwh1IBSL6w2j5lfZPpIkb4Jlk2KUoX6fN81pWkBC/fTBUSIB
EHM9bL51+yKEYUbGIy/gANuRbHXsWg3sjUsFTNPN4hGTFk3w2xChCyl/f5us8Lo8
Z633SNdpvwKBgQCGyDU9XzNzVZihXtx7wS0sE7OSjKtX5cf/UCbA1V0OVUWR3SYO
J0HPCQFfF0BjFHSwwYPKuaR9C8zMdLNhK5/qdh/NU7czNi9fsZ7moh7SkRFbzJzN
OxbKD9t/CzJEMQEXeF/nWTfsSpUgILqqZtAxuuFLbAcaAnJYlCKdAumQgQKBgQCK
mqjJh68pn7gJwGUjoYNe1xtGbSsqHI9F9ovZ0MPO1v6e5M7sQJHH+Fnnxzv/y8e8
d6tz8e73iX1IHymDKv35uuZHCGF1XOR+qrA/KQUc+vcKf21OXsP/JtkTRs1HLoRD
S5aRf2DWcfvniyYARSNU2xTM8GWgi2ueWbMDHUp+ZwKBgA/swC+K+Jg5DEWm6Sau
e6y+eC6S+SoXEKkI3wf7m9aKoZo0y+jh8Gas6gratlc181pSM8O3vZG0n19b493I
apCFomMLE56zEzvyzfpsNhFhk5MBMCn0LPyzX6MiynRlGyWIj0c99fbHI3pOMufP
WgmVLTZ8uDcSW1MbdUCwFSk5
-----END PRIVATE KEY-----
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
subject=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
issuer=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----