Allow registration of RSocket metadata extractors
Prior to this commit, customizing the extraction of RSocket metadata from frames would require developers to override the default `MetadataExtractor` while configuring `RSocketStrategies`. This touches on many infrastructure parts, whereas the goal is just to configure an extra metadata entry extractor using already configured codecs. This commit adds a way to register metadata entry extractors on the `RSocketStrategies` builder with a `Consumer`-based API. Closes gh-23645
This commit is contained in:
@@ -49,7 +49,7 @@ import org.springframework.util.MimeType;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.2
|
||||
*/
|
||||
public class DefaultMetadataExtractor implements MetadataExtractor {
|
||||
public class DefaultMetadataExtractor implements MetadataExtractor, MetadataExtractorRegistry {
|
||||
|
||||
private final List<Decoder<?>> decoders;
|
||||
|
||||
@@ -78,59 +78,14 @@ public class DefaultMetadataExtractor implements MetadataExtractor {
|
||||
return this.decoders;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param mimeType the mime type of metadata entries to extract
|
||||
* @param targetType the target value type to decode to
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@Override
|
||||
public <T> void metadataToExtract(
|
||||
MimeType mimeType, Class<T> targetType, BiConsumer<T, Map<String, Object>> mapper) {
|
||||
|
||||
registerMetadata(mimeType, ResolvableType.forClass(targetType), mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 type 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
|
||||
*/
|
||||
@Override
|
||||
public <T> void metadataToExtract(
|
||||
MimeType mimeType, ParameterizedTypeReference<T> type, BiConsumer<T, Map<String, Object>> mapper) {
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import org.springframework.util.SimpleRouteMatcher;
|
||||
* Default implementation of {@link RSocketStrategies}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.2
|
||||
*/
|
||||
final class DefaultRSocketStrategies implements RSocketStrategies {
|
||||
@@ -128,6 +129,8 @@ final class DefaultRSocketStrategies implements RSocketStrategies {
|
||||
@Nullable
|
||||
private MetadataExtractor metadataExtractor;
|
||||
|
||||
private final List<Consumer<MetadataExtractorRegistry>> metadataExtractors = new ArrayList<>();
|
||||
|
||||
DefaultRSocketStrategiesBuilder() {
|
||||
this.encoders.add(CharSequenceEncoder.allMimeTypes());
|
||||
this.encoders.add(new ByteBufferEncoder());
|
||||
@@ -200,6 +203,12 @@ final class DefaultRSocketStrategies implements RSocketStrategies {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder metadataExtractors(Consumer<MetadataExtractorRegistry> consumer) {
|
||||
this.metadataExtractors.add(consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RSocketStrategies build() {
|
||||
RouteMatcher matcher = (this.routeMatcher != null ? this.routeMatcher : initRouteMatcher());
|
||||
@@ -213,6 +222,10 @@ final class DefaultRSocketStrategies implements RSocketStrategies {
|
||||
MetadataExtractor extractor = (this.metadataExtractor != null ?
|
||||
this.metadataExtractor : new DefaultMetadataExtractor(this.decoders));
|
||||
|
||||
if (extractor instanceof MetadataExtractorRegistry) {
|
||||
this.metadataExtractors.forEach(consumer -> consumer.accept((MetadataExtractorRegistry) extractor));
|
||||
}
|
||||
|
||||
return new DefaultRSocketStrategies(
|
||||
this.encoders, this.decoders, matcher, registry, factory, extractor);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.util.MimeType;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.2
|
||||
* @see MetadataExtractorRegistry
|
||||
*/
|
||||
public interface MetadataExtractor {
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Stores registrations of extractors for metadata entries.
|
||||
* Each metadata entry is decoded based on its {@code MimeType} and a name
|
||||
* is assigned to the decoded value.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.2
|
||||
* @see MetadataExtractor
|
||||
*/
|
||||
public interface MetadataExtractorRegistry {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
default 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.
|
||||
* @param mimeType the mime type of metadata entries to extract
|
||||
* @param targetType the target value type to decode to
|
||||
*/
|
||||
default 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
|
||||
*/
|
||||
<T> void metadataToExtract(
|
||||
MimeType mimeType, Class<T> targetType, BiConsumer<T, Map<String, Object>> mapper);
|
||||
|
||||
/**
|
||||
* 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 type 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
|
||||
*/
|
||||
<T> void metadataToExtract(
|
||||
MimeType mimeType, ParameterizedTypeReference<T> type, BiConsumer<T, Map<String, Object>> mapper);
|
||||
|
||||
}
|
||||
@@ -219,6 +219,12 @@ public interface RSocketStrategies {
|
||||
*/
|
||||
Builder metadataExtractor(@Nullable MetadataExtractor metadataExtractor);
|
||||
|
||||
/**
|
||||
* Apply the consumer to the {@link MetadataExtractorRegistry} in order
|
||||
* to register extra metadata entry extractors.
|
||||
*/
|
||||
Builder metadataExtractors(Consumer<MetadataExtractorRegistry> extractorRegistry);
|
||||
|
||||
/**
|
||||
* Build the {@code RSocketStrategies} instance.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.messaging.rsocket;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
@@ -30,16 +32,20 @@ import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.SimpleRouteMatcher;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link RSocketStrategies}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.2
|
||||
*/
|
||||
public class DefaultRSocketStrategiesTests {
|
||||
class DefaultRSocketStrategiesTests {
|
||||
|
||||
@Test
|
||||
public void defaultSettings() {
|
||||
void defaultSettings() {
|
||||
RSocketStrategies strategies = RSocketStrategies.create();
|
||||
|
||||
assertThat(strategies.encoders()).hasSize(4).hasOnlyElementsOfTypes(
|
||||
@@ -62,8 +68,7 @@ public class DefaultRSocketStrategiesTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void explicitValues() {
|
||||
|
||||
void explicitValues() {
|
||||
SimpleRouteMatcher matcher = new SimpleRouteMatcher(new AntPathMatcher());
|
||||
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor();
|
||||
ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry();
|
||||
@@ -90,7 +95,7 @@ public class DefaultRSocketStrategiesTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyConstructor() {
|
||||
void copyConstructor() {
|
||||
RSocketStrategies strategies1 = RSocketStrategies.create();
|
||||
RSocketStrategies strategies2 = strategies1.mutate().build();
|
||||
|
||||
@@ -101,4 +106,12 @@ public class DefaultRSocketStrategiesTests {
|
||||
assertThat(strategies1.reactiveAdapterRegistry()).isSameAs(strategies2.reactiveAdapterRegistry());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void applyMetadataExtractors() {
|
||||
Consumer<MetadataExtractorRegistry> consumer = (Consumer<MetadataExtractorRegistry>) mock(Consumer.class);
|
||||
RSocketStrategies strategies = RSocketStrategies.builder().metadataExtractors(consumer).build();
|
||||
verify(consumer, times(1)).accept(any());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user