Skip to content

Commit c45cc66

Browse files
authored
GH-3207: RSocket inbound: decode each flux item (#3208)
* GH-3207: RSocket inbound: decode each flux item Fixes #3207 Previously an incoming RSocket Publisher has been decoded as a single unit leading to extra work on the client side, e.g. a delimiter has to be provided to treat each payload item as independent * To have a consistency with Spring Messaging and its `PayloadMethodArgumentResolver` change an `RSocketInboundGateway` to process inbound payloads as `Flux` and decode each item independently. * Change `RSocketDslTests` to remove delimiters and make it consistent with the regular `RSocketRequester` client * * Add `decodeFluxAsUnit` option into `RSocketInboundGateway` * Document the change
1 parent 1511dd8 commit c45cc66

File tree

9 files changed

+103
-13
lines changed

9 files changed

+103
-13
lines changed

spring-integration-rsocket/src/main/java/org/springframework/integration/rsocket/config/RSocketInboundGatewayParser.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,7 +37,6 @@ public class RSocketInboundGatewayParser extends AbstractInboundGatewayParser {
3737

3838
private static final List<String> NON_ELIGIBLE_ATTRIBUTES =
3939
Arrays.asList("path",
40-
"interaction-models",
4140
"rsocket-strategies",
4241
"rsocket-connector",
4342
"request-element-type");
@@ -61,7 +60,6 @@ protected void doPostProcess(BeanDefinitionBuilder builder, Element element) {
6160
"rSocketStrategies");
6261
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "rsocket-connector",
6362
"RSocketConnector");
64-
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "interaction-models");
6563
}
6664

6765
}

spring-integration-rsocket/src/main/java/org/springframework/integration/rsocket/dsl/RSocketInboundGatewaySpec.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import org.springframework.integration.rsocket.inbound.RSocketInboundGateway;
2424
import org.springframework.messaging.rsocket.RSocketStrategies;
2525

26+
import reactor.core.publisher.Flux;
27+
2628
/**
2729
* The {@link MessagingGatewaySpec} implementation for the {@link RSocketInboundGateway}.
2830
*
@@ -82,4 +84,16 @@ public RSocketInboundGatewaySpec requestElementType(ResolvableType requestElemen
8284
return this;
8385
}
8486

87+
/**
88+
* Configure an option to decode an incoming {@link Flux} as a single unit or each its event separately.
89+
* @param decodeFluxAsUnit decode incoming {@link Flux} as a single unit or each event separately.
90+
* @return the spec
91+
* @since 5.3
92+
* @see RSocketInboundGateway#setDecodeFluxAsUnit(boolean)
93+
*/
94+
public RSocketInboundGatewaySpec decodeFluxAsUnit(boolean decodeFluxAsUnit) {
95+
this.target.setDecodeFluxAsUnit(decodeFluxAsUnit);
96+
return this;
97+
}
98+
8599
}

spring-integration-rsocket/src/main/java/org/springframework/integration/rsocket/inbound/RSocketInboundGateway.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -85,6 +85,8 @@ public class RSocketInboundGateway extends MessagingGatewaySupport implements In
8585
@Nullable
8686
private ResolvableType requestElementType;
8787

88+
private boolean decodeFluxAsUnit;
89+
8890
/**
8991
* Instantiate based on the provided path patterns to map this endpoint for incoming RSocket requests.
9092
* @param pathArg the mapping patterns to use.
@@ -160,6 +162,20 @@ public void setRequestElementType(ResolvableType requestElementType) {
160162
this.requestElementType = requestElementType;
161163
}
162164

165+
/**
166+
* Configure an option to decode an incoming {@link Flux} as a single unit or each its event separately.
167+
* Defaults to {@code false} for consistency with Spring Messaging {@code @MessageMapping}.
168+
* The target {@link Flux} decoding logic depends on the {@link Decoder} selected.
169+
* For example a {@link org.springframework.core.codec.StringDecoder} requires a new line separator to
170+
* be present in the stream to indicate a byte buffer end.
171+
* @param decodeFluxAsUnit decode incoming {@link Flux} as a single unit or each event separately.
172+
* @since 5.3
173+
* @see Decoder#decode(Publisher, ResolvableType, MimeType, java.util.Map)
174+
*/
175+
public void setDecodeFluxAsUnit(boolean decodeFluxAsUnit) {
176+
this.decodeFluxAsUnit = decodeFluxAsUnit;
177+
}
178+
163179
@Override
164180
protected void onInit() {
165181
super.onInit();
@@ -219,14 +235,17 @@ private Mono<Message<?>> decodeRequestMessage(Message<?> requestMessage) {
219235
@SuppressWarnings("unchecked")
220236
@Nullable
221237
private Object decodePayload(Message<?> requestMessage) {
222-
ResolvableType elementType = this.requestElementType;
238+
ResolvableType elementType;
223239
MimeType mimeType = requestMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class);
224-
if (elementType == null) {
240+
if (this.requestElementType == null) {
225241
elementType =
226242
mimeType != null && "text".equals(mimeType.getType())
227243
? ResolvableType.forClass(String.class)
228244
: ResolvableType.forClass(byte[].class);
229245
}
246+
else {
247+
elementType = this.requestElementType;
248+
}
230249

231250
Object payload = requestMessage.getPayload();
232251

@@ -235,9 +254,18 @@ private Object decodePayload(Message<?> requestMessage) {
235254
if (payload instanceof DataBuffer) {
236255
return decoder.decode((DataBuffer) payload, elementType, mimeType, null);
237256
}
238-
else {
257+
else if (this.decodeFluxAsUnit) {
239258
return decoder.decode((Publisher<DataBuffer>) payload, elementType, mimeType, null);
240259
}
260+
else {
261+
return Flux.from((Publisher<DataBuffer>) payload)
262+
.handle((buffer, synchronousSink) -> {
263+
Object value = decoder.decode(buffer, elementType, mimeType, null);
264+
if (value != null) {
265+
synchronousSink.next(value);
266+
}
267+
});
268+
}
241269
}
242270

243271
private Flux<DataBuffer> createReply(Object reply, Message<?> requestMessage) {

spring-integration-rsocket/src/main/resources/org/springframework/integration/rsocket/config/spring-integration-rsocket.xsd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@
9393
</xsd:documentation>
9494
</xsd:annotation>
9595
</xsd:attribute>
96+
<xsd:attribute name="decode-flux-as-unit" default="false">
97+
<xsd:annotation>
98+
<xsd:documentation>
99+
Decode incoming Flux as a single unit or each event separately.
100+
</xsd:documentation>
101+
</xsd:annotation>
102+
<xsd:simpleType>
103+
<xsd:union memberTypes="xsd:boolean xsd:string"/>
104+
</xsd:simpleType>
105+
</xsd:attribute>
96106
</xsd:extension>
97107
</xsd:complexContent>
98108
</xsd:complexType>

spring-integration-rsocket/src/test/java/org/springframework/integration/rsocket/config/RSocketInboundGatewayParserTests-context.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
auto-startup="false"
2626
request-channel="requestChannel"
2727
rsocket-strategies="rsocketStrategies"
28-
request-element-type="byte[]"/>
28+
request-element-type="byte[]"
29+
decode-flux-as-unit="true"/>
2930

3031
</beans>

spring-integration-rsocket/src/test/java/org/springframework/integration/rsocket/config/RSocketInboundGatewayParserTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ class RSocketInboundGatewayParserTests {
4444
private RSocketInboundGateway inboundGateway;
4545

4646
@Test
47-
void testOutboundGatewayParser() {
47+
void testInboundGatewayParser() {
4848
assertThat(TestUtils.getPropertyValue(this.inboundGateway, "rsocketConnector"))
4949
.isSameAs(this.clientRSocketConnector);
5050
assertThat(TestUtils.getPropertyValue(this.inboundGateway, "rsocketStrategies"))
@@ -54,6 +54,7 @@ void testOutboundGatewayParser() {
5454
.isEqualTo(byte[].class);
5555
assertThat(this.inboundGateway.getInteractionModels())
5656
.containsExactly(RSocketInteractionModel.fireAndForget, RSocketInteractionModel.requestChannel);
57+
assertThat(TestUtils.getPropertyValue(this.inboundGateway, "decodeFluxAsUnit", Boolean.class)).isTrue();
5758
}
5859

5960
}

spring-integration-rsocket/src/test/java/org/springframework/integration/rsocket/dsl/RSocketDslTests.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.springframework.integration.rsocket.ClientRSocketConnector;
3232
import org.springframework.integration.rsocket.RSocketInteractionModel;
3333
import org.springframework.integration.rsocket.ServerRSocketConnector;
34+
import org.springframework.integration.support.MessageBuilder;
35+
import org.springframework.messaging.Message;
3436
import org.springframework.test.annotation.DirtiesContext;
3537
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3638

@@ -48,17 +50,30 @@ public class RSocketDslTests {
4850

4951
@Autowired
5052
@Qualifier("rsocketUpperCaseRequestFlow.gateway")
51-
private Function<Flux<String>, Flux<String>> rsocketUpperCaseFlowFunction;
53+
private Function<Object, Flux<String>> rsocketUpperCaseFlowFunction;
5254

5355
@Test
5456
void testRsocketUpperCaseFlows() {
55-
Flux<String> result = this.rsocketUpperCaseFlowFunction.apply(Flux.just("a\n", "b\n", "c\n"));
57+
Flux<String> result = this.rsocketUpperCaseFlowFunction.apply(Flux.just("a", "b", "c"));
5658

5759
StepVerifier.create(result)
5860
.expectNext("A", "B", "C")
5961
.verifyComplete();
6062
}
6163

64+
@Test
65+
void testRsocketUpperCaseWholeFlows() {
66+
Message<Flux<String>> testMessage =
67+
MessageBuilder.withPayload(Flux.just("a", "b", "c", "\n"))
68+
.setHeader("route", "/uppercaseWhole")
69+
.build();
70+
Flux<String> result = this.rsocketUpperCaseFlowFunction.apply(testMessage);
71+
72+
StepVerifier.create(result)
73+
.expectNext("ABC")
74+
.verifyComplete();
75+
}
76+
6277
@Configuration
6378
@EnableIntegration
6479
public static class TestConfiguration {
@@ -80,7 +95,8 @@ public ClientRSocketConnector clientRSocketConnector(ServerRSocketConnector serv
8095
public IntegrationFlow rsocketUpperCaseRequestFlow(ClientRSocketConnector clientRSocketConnector) {
8196
return IntegrationFlows
8297
.from(Function.class)
83-
.handle(RSockets.outboundGateway("/uppercase")
98+
.handle(RSockets.outboundGateway(message ->
99+
message.getHeaders().getOrDefault("route", "/uppercase"))
84100
.interactionModel((message) -> RSocketInteractionModel.requestChannel)
85101
.expectedResponseType("T(java.lang.String)")
86102
.clientRSocketConnector(clientRSocketConnector),
@@ -100,6 +116,16 @@ public IntegrationFlow rsocketUpperCaseFlow() {
100116
.get();
101117
}
102118

119+
@Bean
120+
public IntegrationFlow rsocketUpperCaseWholeFlow() {
121+
return IntegrationFlows
122+
.from(RSockets.inboundGateway("/uppercaseWhole")
123+
.interactionModels(RSocketInteractionModel.requestChannel)
124+
.decodeFluxAsUnit(true))
125+
.<Flux<String>, Flux<String>>transform((flux) -> flux.map(String::toUpperCase))
126+
.get();
127+
}
128+
103129
}
104130

105131
}

src/reference/asciidoc/rsocket.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ The `payload` of the message to send downstream is always a `Flux` according to
137137
When in a `fireAndForget` RSocket interaction model, the message has a plain converted `payload`.
138138
The reply `payload` could be a plain object or a `Publisher` - the `RSocketInboundGateway` converts both of them properly into an RSocket response according to the encoders provided in the `RSocketStrategies`.
139139

140+
Starting with version 5.3, a `decodeFluxAsUnit` option (default `false`) is added to the `RSocketInboundGateway`.
141+
By default incoming `Flux` is transformed the way that each its event is decoded separately.
142+
This is an exact behavior present currently with `@MessageMapping` semantics.
143+
To restore a previous behavior or decode the whole `Flux` as single unit according application requirements, the `decodeFluxAsUnit` has to be set to `true`.
144+
However the target decoding logic depends on the `Decoder` selected, e.g. a `StringDecoder` requires a new line separator (by default) to be present in the stream to indicate a byte buffer end.
145+
140146
See <<rsocket-java-config>> for samples how to configure an `RSocketInboundGateway` endpoint and deal with payloads downstream.
141147

142148
[[rsocket-outbound]]

src/reference/asciidoc/whats-new.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,9 @@ See <<./ws.adoc#ws,Web Services Support>> for more information.
9898

9999
The `FailoverClientConnectionFactory` no longer fails back, by default, until the current connection fails.
100100
See <<./ip.adoc#failover-cf,TCP Failover Client Connection Factory>> for more information.
101+
102+
[[x5.3-rsocket]]
103+
=== RSocket Changes
104+
105+
A `decodeFluxAsUnit` option has been added to the `RSocketInboundGateway` with the meaning to decode incoming `Flux` as a single unit or apply decoding for each event in it.
106+
See <<./rsocket.adoc#rsocket-inbound,RSocket Inbound Gateway>> for more information.

0 commit comments

Comments
 (0)