Browse Source
The responding side now relies on a new MetadataExtractor which decodes metadata entries of interest, and adds them to an output map whose values are then added as Message headers, and are hence accessible to controller methods. Decoded metadata entry values can be added to the output map one for one, or translated to any number of values (e.g. JSON properties), as long as one of the resulting pairs has a key called "route". On the requesting side, now any metadata can be sent, and a String route for example is not required to be provided explicitly. Instead an application could create any metadata (e.g. JSON properties) as long as the server can work out the route from it. The commit contains further refinements on the requesting side so that any mime type can be used, not only composite or routing metadata, e.g. a route in an "text/plain" entry. Closes gh-23157pull/23212/head
10 changed files with 761 additions and 226 deletions
@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
|
||||
/* |
||||
* Copyright 2002-2019 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.messaging.rsocket.annotation.support; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
import java.util.function.BiConsumer; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import io.rsocket.Payload; |
||||
import io.rsocket.metadata.CompositeMetadata; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.codec.Decoder; |
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.core.io.buffer.DataBufferFactory; |
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.messaging.rsocket.RSocketStrategies; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Default {@link MetadataExtractor} implementation that relies on {@link Decoder}s |
||||
* to deserialize the content of metadata entries. |
||||
* |
||||
* <p>By default only {@code "message/x.rsocket.routing.v0""} is extracted and |
||||
* saved under {@link MetadataExtractor#ROUTE_KEY}. Use the |
||||
* {@code metadataToExtract} methods to specify other metadata mime types of |
||||
* interest to extract. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
*/ |
||||
public class DefaultMetadataExtractor implements MetadataExtractor { |
||||
|
||||
private final RSocketStrategies rsocketStrategies; |
||||
|
||||
private final Map<String, EntryProcessor<?>> entryProcessors = new HashMap<>(); |
||||
|
||||
|
||||
/** |
||||
* Default constructor with {@link RSocketStrategies}. |
||||
*/ |
||||
public DefaultMetadataExtractor(RSocketStrategies strategies) { |
||||
Assert.notNull(strategies, "RSocketStrategies is required"); |
||||
this.rsocketStrategies = strategies; |
||||
// TODO: remove when rsocket-core API available
|
||||
metadataToExtract(MessagingRSocket.ROUTING, String.class, ROUTE_KEY); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Decode metadata entries with the given {@link MimeType} to the specified |
||||
* target class, and store the decoded value in the output map under the |
||||
* given name. |
||||
* @param mimeType the mime type of metadata entries to extract |
||||
* @param targetType the target value type to decode to |
||||
* @param name assign a name for the decoded value; if not provided, then |
||||
* the mime type is used as the key |
||||
*/ |
||||
public void metadataToExtract( |
||||
MimeType mimeType, Class<?> targetType, @Nullable String name) { |
||||
|
||||
String key = name != null ? name : mimeType.toString(); |
||||
metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); |
||||
} |
||||
|
||||
/** |
||||
* Variant of {@link #metadataToExtract(MimeType, Class, String)} that accepts |
||||
* {@link ParameterizedTypeReference} instead of {@link Class} for |
||||
* specifying a target type with generic parameters. |
||||
*/ |
||||
public void metadataToExtract( |
||||
MimeType mimeType, ParameterizedTypeReference<?> targetType, @Nullable String name) { |
||||
|
||||
String key = name != null ? name : mimeType.toString(); |
||||
metadataToExtract(mimeType, targetType, (value, map) -> map.put(key, value)); |
||||
} |
||||
|
||||
/** |
||||
* Variant of {@link #metadataToExtract(MimeType, Class, String)} that allows |
||||
* custom logic to be used to map the decoded value to any number of values |
||||
* in the output map. |
||||
* @param mimeType the mime type of metadata entries to extract |
||||
* @param targetType the target value type to decode to |
||||
* @param mapper custom logic to add the decoded value to the output map |
||||
* @param <T> the target value type |
||||
*/ |
||||
public <T> void metadataToExtract( |
||||
MimeType mimeType, Class<T> targetType, |
||||
BiConsumer<T, Map<String, Object>> mapper) { |
||||
|
||||
EntryProcessor<T> spec = new EntryProcessor<>(mimeType, targetType, mapper); |
||||
this.entryProcessors.put(mimeType.toString(), spec); |
||||
} |
||||
|
||||
/** |
||||
* Variant of {@link #metadataToExtract(MimeType, Class, BiConsumer)} that |
||||
* accepts {@link ParameterizedTypeReference} instead of {@link Class} for |
||||
* specifying a target type with generic parameters. |
||||
* @param mimeType the mime type of metadata entries to extract |
||||
* @param targetType the target value type to decode to |
||||
* @param mapper custom logic to add the decoded value to the output map |
||||
* @param <T> the target value type |
||||
*/ |
||||
public <T> void metadataToExtract( |
||||
MimeType mimeType, ParameterizedTypeReference<T> targetType, |
||||
BiConsumer<T, Map<String, Object>> mapper) { |
||||
|
||||
EntryProcessor<T> spec = new EntryProcessor<>(mimeType, targetType, mapper); |
||||
this.entryProcessors.put(mimeType.toString(), spec); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public Map<String, Object> extract(Payload payload, MimeType metadataMimeType) { |
||||
Map<String, Object> result = new HashMap<>(); |
||||
if (metadataMimeType.equals(MessagingRSocket.COMPOSITE_METADATA)) { |
||||
for (CompositeMetadata.Entry entry : new CompositeMetadata(payload.metadata(), false)) { |
||||
processEntry(entry.getContent(), entry.getMimeType(), result); |
||||
} |
||||
} |
||||
else { |
||||
processEntry(payload.metadata(), metadataMimeType.toString(), result); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
private void processEntry(ByteBuf content, @Nullable String mimeType, Map<String, Object> result) { |
||||
EntryProcessor<?> entryProcessor = this.entryProcessors.get(mimeType); |
||||
if (entryProcessor != null) { |
||||
content.retain(); |
||||
entryProcessor.process(content, result); |
||||
return; |
||||
} |
||||
if (MessagingRSocket.ROUTING.toString().equals(mimeType)) { |
||||
// TODO: use rsocket-core API when available
|
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Helps to decode a metadata entry and add the resulting value to the |
||||
* output map. |
||||
*/ |
||||
private class EntryProcessor<T> { |
||||
|
||||
private final MimeType mimeType; |
||||
|
||||
private final ResolvableType targetType; |
||||
|
||||
private final BiConsumer<T, Map<String, Object>> accumulator; |
||||
|
||||
private final Decoder<T> decoder; |
||||
|
||||
|
||||
public EntryProcessor( |
||||
MimeType mimeType, Class<T> targetType, |
||||
BiConsumer<T, Map<String, Object>> accumulator) { |
||||
|
||||
this(mimeType, ResolvableType.forClass(targetType), accumulator); |
||||
} |
||||
|
||||
public EntryProcessor( |
||||
MimeType mimeType, ParameterizedTypeReference<T> targetType, |
||||
BiConsumer<T, Map<String, Object>> accumulator) { |
||||
|
||||
this(mimeType, ResolvableType.forType(targetType), accumulator); |
||||
} |
||||
|
||||
private EntryProcessor( |
||||
MimeType mimeType, ResolvableType targetType, |
||||
BiConsumer<T, Map<String, Object>> accumulator) { |
||||
|
||||
this.mimeType = mimeType; |
||||
this.targetType = targetType; |
||||
this.accumulator = accumulator; |
||||
this.decoder = rsocketStrategies.decoder(targetType, mimeType); |
||||
} |
||||
|
||||
|
||||
public void process(ByteBuf byteBuf, Map<String, Object> result) { |
||||
DataBufferFactory factory = rsocketStrategies.dataBufferFactory(); |
||||
DataBuffer buffer = factory instanceof NettyDataBufferFactory ? |
||||
((NettyDataBufferFactory) factory).wrap(byteBuf) : |
||||
factory.wrap(byteBuf.nioBuffer()); |
||||
|
||||
T value = this.decoder.decode(buffer, this.targetType, this.mimeType, Collections.emptyMap()); |
||||
this.accumulator.accept(value, result); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
/* |
||||
* Copyright 2002-2019 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.messaging.rsocket.annotation.support; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import io.rsocket.Payload; |
||||
|
||||
import org.springframework.util.MimeType; |
||||
|
||||
/** |
||||
* Strategy to extract a map of values from the metadata of a {@link Payload}. |
||||
* This includes decoding metadata entries based on their mime type and |
||||
* assigning a name to the decoded value. The resulting name-value pairs can |
||||
* be added to the headers of a |
||||
* {@link org.springframework.messaging.Message Message}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.2 |
||||
*/ |
||||
public interface MetadataExtractor { |
||||
|
||||
/** |
||||
* The key to assign to the extracted "route" of the payload. |
||||
*/ |
||||
String ROUTE_KEY = "route"; |
||||
|
||||
|
||||
/** |
||||
* Extract a map of values from the given {@link Payload} metadata. |
||||
* <p>Metadata may be composite and consist of multiple entries |
||||
* Implementations are free to extract any number of name-value pairs per |
||||
* metadata entry. The Payload "route" should be saved under the |
||||
* {@link #ROUTE_KEY}. |
||||
* @param payload the payload whose metadata should be read |
||||
* @param metadataMimeType the mime type of the metadata; this is what was |
||||
* specified by the client at the start of the RSocket connection. |
||||
* @return a map of 0 or more decoded metadata values with assigned names |
||||
*/ |
||||
Map<String, Object> extract(Payload payload, MimeType metadataMimeType); |
||||
|
||||
} |
||||
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2002-2019 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.messaging.rsocket; |
||||
|
||||
import java.time.Duration; |
||||
import java.time.Instant; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import io.netty.buffer.ByteBuf; |
||||
import io.netty.buffer.ByteBufAllocator; |
||||
import io.netty.buffer.Unpooled; |
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer; |
||||
import org.springframework.core.io.buffer.NettyDataBuffer; |
||||
import org.springframework.core.io.buffer.NettyDataBufferFactory; |
||||
import org.springframework.core.io.buffer.PooledDataBuffer; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* Unlike {@link org.springframework.core.io.buffer.LeakAwareDataBufferFactory} |
||||
* this one is an instance of {@link NettyDataBufferFactory} which is necessary |
||||
* since {@link PayloadUtils} does instanceof checks, and that also allows |
||||
* intercepting {@link NettyDataBufferFactory#wrap(ByteBuf)}. |
||||
*/ |
||||
public class LeakAwareNettyDataBufferFactory extends NettyDataBufferFactory { |
||||
|
||||
private final List<DataBufferLeakInfo> created = new ArrayList<>(); |
||||
|
||||
|
||||
public LeakAwareNettyDataBufferFactory(ByteBufAllocator byteBufAllocator) { |
||||
super(byteBufAllocator); |
||||
} |
||||
|
||||
|
||||
public void checkForLeaks(Duration duration) throws InterruptedException { |
||||
Instant start = Instant.now(); |
||||
while (true) { |
||||
try { |
||||
this.created.forEach(info -> { |
||||
if (((PooledDataBuffer) info.getDataBuffer()).isAllocated()) { |
||||
throw info.getError(); |
||||
} |
||||
}); |
||||
break; |
||||
} |
||||
catch (AssertionError ex) { |
||||
if (Instant.now().isAfter(start.plus(duration))) { |
||||
throw ex; |
||||
} |
||||
} |
||||
Thread.sleep(50); |
||||
} |
||||
} |
||||
|
||||
public void reset() { |
||||
this.created.clear(); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public NettyDataBuffer allocateBuffer() { |
||||
return (NettyDataBuffer) recordHint(super.allocateBuffer()); |
||||
} |
||||
|
||||
@Override |
||||
public NettyDataBuffer allocateBuffer(int initialCapacity) { |
||||
return (NettyDataBuffer) recordHint(super.allocateBuffer(initialCapacity)); |
||||
} |
||||
|
||||
@Override |
||||
public NettyDataBuffer wrap(ByteBuf byteBuf) { |
||||
NettyDataBuffer dataBuffer = super.wrap(byteBuf); |
||||
if (byteBuf != Unpooled.EMPTY_BUFFER) { |
||||
recordHint(dataBuffer); |
||||
} |
||||
return dataBuffer; |
||||
} |
||||
|
||||
@Override |
||||
public DataBuffer join(List<? extends DataBuffer> dataBuffers) { |
||||
return recordHint(super.join(dataBuffers)); |
||||
} |
||||
|
||||
private DataBuffer recordHint(DataBuffer buffer) { |
||||
AssertionError error = new AssertionError(String.format( |
||||
"DataBuffer leak: {%s} {%s} not released.%nStacktrace at buffer creation: ", buffer, |
||||
ObjectUtils.getIdentityHexString(((NettyDataBuffer) buffer).getNativeBuffer()))); |
||||
this.created.add(new DataBufferLeakInfo(buffer, error)); |
||||
return buffer; |
||||
} |
||||
|
||||
|
||||
private static class DataBufferLeakInfo { |
||||
|
||||
private final DataBuffer dataBuffer; |
||||
|
||||
private final AssertionError error; |
||||
|
||||
DataBufferLeakInfo(DataBuffer dataBuffer, AssertionError error) { |
||||
this.dataBuffer = dataBuffer; |
||||
this.error = error; |
||||
} |
||||
|
||||
DataBuffer getDataBuffer() { |
||||
return this.dataBuffer; |
||||
} |
||||
|
||||
AssertionError getError() { |
||||
return this.error; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,178 @@
@@ -0,0 +1,178 @@
|
||||
/* |
||||
* Copyright 2002-2019 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.messaging.rsocket.annotation.support; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.Map; |
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator; |
||||
import io.rsocket.Payload; |
||||
import io.rsocket.RSocket; |
||||
import org.junit.After; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
import org.mockito.BDDMockito; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.core.codec.CharSequenceEncoder; |
||||
import org.springframework.core.codec.StringDecoder; |
||||
import org.springframework.core.io.buffer.DataBufferFactory; |
||||
import org.springframework.messaging.rsocket.LeakAwareNettyDataBufferFactory; |
||||
import org.springframework.messaging.rsocket.RSocketRequester; |
||||
import org.springframework.messaging.rsocket.RSocketStrategies; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MimeType; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.springframework.messaging.rsocket.annotation.support.MessagingRSocket.COMPOSITE_METADATA; |
||||
import static org.springframework.messaging.rsocket.annotation.support.MessagingRSocket.ROUTING; |
||||
import static org.springframework.messaging.rsocket.annotation.support.MetadataExtractor.ROUTE_KEY; |
||||
import static org.springframework.util.MimeTypeUtils.TEXT_HTML; |
||||
import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN; |
||||
import static org.springframework.util.MimeTypeUtils.TEXT_XML; |
||||
|
||||
|
||||
/** |
||||
* Unit tests for {@link DefaultMetadataExtractor}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class DefaultMetadataExtractorTests { |
||||
|
||||
private RSocketStrategies strategies; |
||||
|
||||
private ArgumentCaptor<Payload> captor; |
||||
|
||||
private RSocket rsocket; |
||||
|
||||
private DefaultMetadataExtractor extractor; |
||||
|
||||
|
||||
@Before |
||||
public void setUp() { |
||||
this.strategies = RSocketStrategies.builder() |
||||
.decoder(StringDecoder.allMimeTypes()) |
||||
.encoder(CharSequenceEncoder.allMimeTypes()) |
||||
.dataBufferFactory(new LeakAwareNettyDataBufferFactory(PooledByteBufAllocator.DEFAULT)) |
||||
.build(); |
||||
|
||||
this.rsocket = BDDMockito.mock(RSocket.class); |
||||
this.captor = ArgumentCaptor.forClass(Payload.class); |
||||
BDDMockito.when(this.rsocket.fireAndForget(captor.capture())).thenReturn(Mono.empty()); |
||||
|
||||
this.extractor = new DefaultMetadataExtractor(this.strategies); |
||||
} |
||||
|
||||
@After |
||||
public void tearDown() throws InterruptedException { |
||||
DataBufferFactory bufferFactory = this.strategies.dataBufferFactory(); |
||||
((LeakAwareNettyDataBufferFactory) bufferFactory).checkForLeaks(Duration.ofSeconds(5)); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void compositeMetadataWithDefaultSettings() { |
||||
|
||||
requester(COMPOSITE_METADATA).route("toA") |
||||
.metadata("text data", TEXT_PLAIN) |
||||
.metadata("html data", TEXT_HTML) |
||||
.metadata("xml data", TEXT_XML) |
||||
.data("data") |
||||
.send().block(); |
||||
|
||||
Payload payload = this.captor.getValue(); |
||||
Map<String, Object> result = this.extractor.extract(payload, COMPOSITE_METADATA); |
||||
payload.release(); |
||||
|
||||
assertThat(result).hasSize(1).containsEntry(ROUTE_KEY, "toA"); |
||||
} |
||||
|
||||
@Test |
||||
public void compositeMetadataWithMimeTypeRegistrations() { |
||||
|
||||
this.extractor.metadataToExtract(TEXT_PLAIN, String.class, "text-entry"); |
||||
this.extractor.metadataToExtract(TEXT_HTML, String.class, "html-entry"); |
||||
this.extractor.metadataToExtract(TEXT_XML, String.class, "xml-entry"); |
||||
|
||||
requester(COMPOSITE_METADATA).route("toA") |
||||
.metadata("text data", TEXT_PLAIN) |
||||
.metadata("html data", TEXT_HTML) |
||||
.metadata("xml data", TEXT_XML) |
||||
.data("data") |
||||
.send() |
||||
.block(); |
||||
|
||||
Payload payload = this.captor.getValue(); |
||||
Map<String, Object> result = this.extractor.extract(payload, COMPOSITE_METADATA); |
||||
payload.release(); |
||||
|
||||
assertThat(result).hasSize(4) |
||||
.containsEntry(ROUTE_KEY, "toA") |
||||
.containsEntry("text-entry", "text data") |
||||
.containsEntry("html-entry", "html data") |
||||
.containsEntry("xml-entry", "xml data"); |
||||
} |
||||
|
||||
@Test |
||||
public void route() { |
||||
|
||||
requester(ROUTING).route("toA").data("data").send().block(); |
||||
Payload payload = this.captor.getValue(); |
||||
Map<String, Object> result = this.extractor.extract(payload, ROUTING); |
||||
payload.release(); |
||||
|
||||
assertThat(result).hasSize(1).containsEntry(ROUTE_KEY, "toA"); |
||||
} |
||||
|
||||
@Test |
||||
public void routeAsText() { |
||||
|
||||
this.extractor.metadataToExtract(TEXT_PLAIN, String.class, ROUTE_KEY); |
||||
|
||||
requester(TEXT_PLAIN).route("toA").data("data").send().block(); |
||||
Payload payload = this.captor.getValue(); |
||||
Map<String, Object> result = this.extractor.extract(payload, TEXT_PLAIN); |
||||
payload.release(); |
||||
|
||||
assertThat(result).hasSize(1).containsEntry(ROUTE_KEY, "toA"); |
||||
} |
||||
|
||||
@Test |
||||
public void routeWithCustomFormatting() { |
||||
|
||||
this.extractor.metadataToExtract(TEXT_PLAIN, String.class, (text, result) -> { |
||||
String[] items = text.split(":"); |
||||
Assert.isTrue(items.length == 2, "Expected two items"); |
||||
result.put(ROUTE_KEY, items[0]); |
||||
result.put("entry1", items[1]); |
||||
}); |
||||
|
||||
requester(TEXT_PLAIN).metadata("toA:text data", null).data("data").send().block(); |
||||
Payload payload = this.captor.getValue(); |
||||
Map<String, Object> result = this.extractor.extract(payload, TEXT_PLAIN); |
||||
payload.release(); |
||||
|
||||
assertThat(result).hasSize(2) |
||||
.containsEntry(ROUTE_KEY, "toA") |
||||
.containsEntry("entry1", "text data"); |
||||
} |
||||
|
||||
|
||||
private RSocketRequester requester(MimeType metadataMimeType) { |
||||
return RSocketRequester.wrap(this.rsocket, TEXT_PLAIN, metadataMimeType, this.strategies); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue