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:
Brian Clozel
2019-09-16 19:01:14 +02:00
parent a676059699
commit 848804a227
6 changed files with 131 additions and 53 deletions

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -31,6 +31,7 @@ import org.springframework.util.MimeType;
*
* @author Rossen Stoyanchev
* @since 5.2
* @see MetadataExtractorRegistry
*/
public interface MetadataExtractor {

View File

@@ -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);
}

View File

@@ -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.
*/

View File

@@ -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());
}
}