diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index 8263b1242067..972fc70ce234 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -191,18 +191,66 @@ interface DefaultCodecs { */ interface CustomCodecs { + /** + * Register a custom codec. This is expected to be one of the following: + * + * @param codec the codec to register + * @since 5.2.3 + */ + void register(Object codec); + + /** + * Variant of {@link #register(Object)} that also applies the below + * properties, if configured, via {@link #defaultCodecs()}: + * + *

The properties are applied every time {@link #getReaders()} or + * {@link #getWriters()} are used to obtain the list of configured + * readers or writers. + * @param codec the codec to register and apply default config to + * @since 5.2.3 + */ + void registerWithDefaultConfig(Object codec); + + /** + * Variant of {@link #register(Object)} that also allows the caller to + * apply the properties from {@link DefaultCodecConfig} to the given + * codec. If you want to apply all the properties, prefer using + * {@link #registerWithDefaultConfig(Object)}. + *

The consumer is called every time {@link #getReaders()} or + * {@link #getWriters()} are used to obtain the list of configured + * readers or writers. + * @param codec the codec to register + * @param configConsumer consumer of the default config + * @since 5.2.3 + */ + void registerWithDefaultConfig(Object codec, Consumer configConsumer); + /** * Add a custom {@code Decoder} internally wrapped with * {@link DecoderHttpMessageReader}). * @param decoder the decoder to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void decoder(Decoder decoder); /** * Add a custom {@code Encoder}, internally wrapped with * {@link EncoderHttpMessageWriter}. * @param encoder the encoder to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void encoder(Encoder encoder); /** @@ -210,7 +258,10 @@ interface CustomCodecs { * {@link DecoderHttpMessageReader} consider using the shortcut * {@link #decoder(Decoder)} instead. * @param reader the reader to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void reader(HttpMessageReader reader); /** @@ -218,7 +269,10 @@ interface CustomCodecs { * {@link EncoderHttpMessageWriter} consider using the shortcut * {@link #encoder(Encoder)} instead. * @param writer the writer to add + * @deprecated as of 5.1.13, use {@link #register(Object)} or + * {@link #registerWithDefaultConfig(Object)} instead. */ + @Deprecated void writer(HttpMessageWriter writer); /** @@ -227,16 +281,21 @@ interface CustomCodecs { * guidelines applied to default ones, such as logging details and limiting * the amount of buffered data. * @param codecsConfigConsumer the default codecs configuration callback - * @since 5.1.12 + * @deprecated as of 5.1.13, use {@link #registerWithDefaultConfig(Object)} + * or {@link #registerWithDefaultConfig(Object, Consumer)} instead. */ + @Deprecated void withDefaultCodecConfig(Consumer codecsConfigConsumer); } /** - * Common options applied to default codecs and passed in a callback to custom codecs - * so they get a chance to align their behavior on the default ones. + * Exposes the values of properties configured through + * {@link #defaultCodecs()} that are applied to default codecs. + * The main purpose of this interface is to provide access to them so they + * can also be applied to custom codecs if needed. * @since 5.1.12 + * @see CustomCodecs#registerWithDefaultConfig(Object, Consumer) */ interface DefaultCodecConfig { diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java index 325c407e5e3d..cac8a5e7d8b8 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseCodecConfigurer.java @@ -17,7 +17,9 @@ package org.springframework.http.codec.support; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import org.springframework.core.ResolvableType; @@ -40,8 +42,6 @@ */ abstract class BaseCodecConfigurer implements CodecConfigurer { - protected boolean customCodecsInitialized; - protected final BaseDefaultCodecs defaultCodecs; protected final DefaultCustomCodecs customCodecs; @@ -91,21 +91,20 @@ public CustomCodecs customCodecs() { @Override public List> getReaders() { - initializeCustomCodecs(); - List> result = new ArrayList<>(); + this.defaultCodecs.applyDefaultConfig(this.customCodecs); - result.addAll(this.customCodecs.getTypedReaders()); + List> result = new ArrayList<>(); + result.addAll(this.customCodecs.getTypedReaders().keySet()); result.addAll(this.defaultCodecs.getTypedReaders()); - - result.addAll(this.customCodecs.getObjectReaders()); + result.addAll(this.customCodecs.getObjectReaders().keySet()); result.addAll(this.defaultCodecs.getObjectReaders()); - result.addAll(this.defaultCodecs.getCatchAllReaders()); return result; } @Override public List> getWriters() { + this.defaultCodecs.applyDefaultConfig(this.customCodecs); return getWritersInternal(false); } @@ -117,13 +116,12 @@ public List> getWriters() { * same except for the multipart writer itself. */ protected List> getWritersInternal(boolean forMultipart) { - initializeCustomCodecs(); List> result = new ArrayList<>(); - result.addAll(this.customCodecs.getTypedWriters()); + result.addAll(this.customCodecs.getTypedWriters().keySet()); result.addAll(this.defaultCodecs.getTypedWriters(forMultipart)); - result.addAll(this.customCodecs.getObjectWriters()); + result.addAll(this.customCodecs.getObjectWriters().keySet()); result.addAll(this.defaultCodecs.getObjectWriters(forMultipart)); result.addAll(this.defaultCodecs.getCatchAllWriters()); @@ -133,28 +131,21 @@ protected List> getWritersInternal(boolean forMultipart) { @Override public abstract CodecConfigurer clone(); - private void initializeCustomCodecs() { - if(!this.customCodecsInitialized) { - this.customCodecs.configConsumers.forEach(consumer -> consumer.accept(this.defaultCodecs)); - this.customCodecsInitialized = true; - } - } - /** * Default implementation of {@code CustomCodecs}. */ protected static final class DefaultCustomCodecs implements CustomCodecs { - private final List> typedReaders = new ArrayList<>(); + private final Map, Boolean> typedReaders = new LinkedHashMap<>(4); - private final List> typedWriters = new ArrayList<>(); + private final Map, Boolean> typedWriters = new LinkedHashMap<>(4); - private final List> objectReaders = new ArrayList<>(); + private final Map, Boolean> objectReaders = new LinkedHashMap<>(4); - private final List> objectWriters = new ArrayList<>(); + private final Map, Boolean> objectWriters = new LinkedHashMap<>(4); - private final List> configConsumers = new ArrayList<>(); + private final List> defaultConfigConsumers = new ArrayList<>(4); DefaultCustomCodecs() { } @@ -164,56 +155,103 @@ protected static final class DefaultCustomCodecs implements CustomCodecs { * @since 5.1.12 */ DefaultCustomCodecs(DefaultCustomCodecs other) { - other.typedReaders.addAll(this.typedReaders); - other.typedWriters.addAll(this.typedWriters); - other.objectReaders.addAll(this.objectReaders); - other.objectWriters.addAll(this.objectWriters); + other.typedReaders.putAll(this.typedReaders); + other.typedWriters.putAll(this.typedWriters); + other.objectReaders.putAll(this.objectReaders); + other.objectWriters.putAll(this.objectWriters); + } + + @Override + public void register(Object codec) { + addCodec(codec, false); } + @Override + public void registerWithDefaultConfig(Object codec) { + addCodec(codec, true); + } + + @Override + public void registerWithDefaultConfig(Object codec, Consumer configConsumer) { + addCodec(codec, false); + this.defaultConfigConsumers.add(configConsumer); + } + + @SuppressWarnings("deprecation") @Override public void decoder(Decoder decoder) { - reader(new DecoderHttpMessageReader<>(decoder)); + addCodec(decoder, false); } + @SuppressWarnings("deprecation") @Override public void encoder(Encoder encoder) { - writer(new EncoderHttpMessageWriter<>(encoder)); + addCodec(encoder, false); } + @SuppressWarnings("deprecation") @Override public void reader(HttpMessageReader reader) { - boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null); - (canReadToObject ? this.objectReaders : this.typedReaders).add(reader); + addCodec(reader, false); } + @SuppressWarnings("deprecation") @Override public void writer(HttpMessageWriter writer) { - boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null); - (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); + addCodec(writer, false); } + @SuppressWarnings("deprecation") @Override public void withDefaultCodecConfig(Consumer codecsConfigConsumer) { - this.configConsumers.add(codecsConfigConsumer); + this.defaultConfigConsumers.add(codecsConfigConsumer); + } + + private void addCodec(Object codec, boolean applyDefaultConfig) { + + if (codec instanceof Decoder) { + codec = new DecoderHttpMessageReader<>((Decoder) codec); + } + else if (codec instanceof Encoder) { + codec = new EncoderHttpMessageWriter<>((Encoder) codec); + } + + if (codec instanceof HttpMessageReader) { + HttpMessageReader reader = (HttpMessageReader) codec; + boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null); + (canReadToObject ? this.objectReaders : this.typedReaders).put(reader, applyDefaultConfig); + } + else if (codec instanceof HttpMessageWriter) { + HttpMessageWriter writer = (HttpMessageWriter) codec; + boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null); + (canWriteObject ? this.objectWriters : this.typedWriters).put(writer, applyDefaultConfig); + } + else { + throw new IllegalArgumentException("Unexpected codec type: " + codec.getClass().getName()); + } } // Package private accessors... - List> getTypedReaders() { + Map, Boolean> getTypedReaders() { return this.typedReaders; } - List> getTypedWriters() { + Map, Boolean> getTypedWriters() { return this.typedWriters; } - List> getObjectReaders() { + Map, Boolean> getObjectReaders() { return this.objectReaders; } - List> getObjectWriters() { + Map, Boolean> getObjectWriters() { return this.objectWriters; } + + List> getDefaultConfigConsumers() { + return this.defaultConfigConsumers; + } } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 3b9678984124..4dce1d044e35 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import org.springframework.core.codec.AbstractDataBufferDecoder; import org.springframework.core.codec.ByteArrayDecoder; @@ -433,6 +434,21 @@ List> getCatchAllWriters() { return result; } + void applyDefaultConfig(BaseCodecConfigurer.DefaultCustomCodecs customCodecs) { + applyDefaultConfig(customCodecs.getTypedReaders()); + applyDefaultConfig(customCodecs.getObjectReaders()); + applyDefaultConfig(customCodecs.getTypedWriters()); + applyDefaultConfig(customCodecs.getObjectWriters()); + customCodecs.getDefaultConfigConsumers().forEach(consumer -> consumer.accept(this)); + } + + private void applyDefaultConfig(Map readers) { + readers.entrySet().stream() + .filter(Map.Entry::getValue) + .map(Map.Entry::getKey) + .forEach(this::initCodec); + } + // Accessors for use in subclasses... diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 938c225178a3..b2ca4667d5cd 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -121,11 +121,11 @@ public void defaultAndCustomReaders() { given(customReader1.canRead(ResolvableType.forClass(Object.class), null)).willReturn(false); given(customReader2.canRead(ResolvableType.forClass(Object.class), null)).willReturn(true); - this.configurer.customCodecs().decoder(customDecoder1); - this.configurer.customCodecs().decoder(customDecoder2); + this.configurer.customCodecs().register(customDecoder1); + this.configurer.customCodecs().register(customDecoder2); - this.configurer.customCodecs().reader(customReader1); - this.configurer.customCodecs().reader(customReader2); + this.configurer.customCodecs().register(customReader1); + this.configurer.customCodecs().register(customReader2); List> readers = this.configurer.getReaders(); @@ -161,11 +161,11 @@ public void defaultAndCustomWriters() { given(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).willReturn(false); given(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).willReturn(true); - this.configurer.customCodecs().encoder(customEncoder1); - this.configurer.customCodecs().encoder(customEncoder2); + this.configurer.customCodecs().register(customEncoder1); + this.configurer.customCodecs().register(customEncoder2); - this.configurer.customCodecs().writer(customWriter1); - this.configurer.customCodecs().writer(customWriter2); + this.configurer.customCodecs().register(customWriter1); + this.configurer.customCodecs().register(customWriter2); List> writers = this.configurer.getWriters(); @@ -200,11 +200,11 @@ public void defaultsOffCustomReaders() { given(customReader1.canRead(ResolvableType.forClass(Object.class), null)).willReturn(false); given(customReader2.canRead(ResolvableType.forClass(Object.class), null)).willReturn(true); - this.configurer.customCodecs().decoder(customDecoder1); - this.configurer.customCodecs().decoder(customDecoder2); + this.configurer.customCodecs().register(customDecoder1); + this.configurer.customCodecs().register(customDecoder2); - this.configurer.customCodecs().reader(customReader1); - this.configurer.customCodecs().reader(customReader2); + this.configurer.customCodecs().register(customReader1); + this.configurer.customCodecs().register(customReader2); this.configurer.registerDefaults(false); @@ -231,11 +231,11 @@ public void defaultsOffWithCustomWriters() { given(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).willReturn(false); given(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).willReturn(true); - this.configurer.customCodecs().encoder(customEncoder1); - this.configurer.customCodecs().encoder(customEncoder2); + this.configurer.customCodecs().register(customEncoder1); + this.configurer.customCodecs().register(customEncoder2); - this.configurer.customCodecs().writer(customWriter1); - this.configurer.customCodecs().writer(customWriter2); + this.configurer.customCodecs().register(customWriter1); + this.configurer.customCodecs().register(customWriter2); this.configurer.registerDefaults(false); @@ -277,10 +277,10 @@ public void cloneCustomCodecs() { this.configurer.registerDefaults(false); CodecConfigurer clone = this.configurer.clone(); - clone.customCodecs().encoder(new Jackson2JsonEncoder()); - clone.customCodecs().decoder(new Jackson2JsonDecoder()); - clone.customCodecs().reader(new ServerSentEventHttpMessageReader()); - clone.customCodecs().writer(new ServerSentEventHttpMessageWriter()); + clone.customCodecs().register(new Jackson2JsonEncoder()); + clone.customCodecs().register(new Jackson2JsonDecoder()); + clone.customCodecs().register(new ServerSentEventHttpMessageReader()); + clone.customCodecs().register(new ServerSentEventHttpMessageWriter()); assertThat(this.configurer.getReaders().size()).isEqualTo(0); assertThat(this.configurer.getWriters().size()).isEqualTo(0); @@ -337,6 +337,7 @@ public void cloneDefaultCodecs() { assertThat(encoders).doesNotContain(jacksonEncoder, jaxb2Encoder, protoEncoder); } + @SuppressWarnings("deprecation") @Test void withDefaultCodecConfig() { AtomicBoolean callbackCalled = new AtomicBoolean(false); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index 0aa1bede0b31..ed18f6f8edf7 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -40,6 +40,7 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.http.MediaType; +import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.FormHttpMessageReader; @@ -129,8 +130,8 @@ public void jackson2EncoderOverride() { public void maxInMemorySize() { int size = 99; this.configurer.defaultCodecs().maxInMemorySize(size); + List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(13); assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -150,6 +151,28 @@ public void maxInMemorySize() { assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); } + @Test + public void maxInMemorySizeWithCustomCodecs() { + + int size = 99; + this.configurer.defaultCodecs().maxInMemorySize(size); + this.configurer.registerDefaults(false); + + CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs(); + customCodecs.register(new ByteArrayDecoder()); + customCodecs.registerWithDefaultConfig(new ByteArrayDecoder()); + customCodecs.register(new Jackson2JsonDecoder()); + customCodecs.registerWithDefaultConfig(new Jackson2JsonDecoder()); + + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + List> readers = this.configurer.getReaders(); + assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024); + assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024); + assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + } + @Test public void enableRequestLoggingDetails() { this.configurer.defaultCodecs().enableLoggingRequestDetails(true); @@ -164,6 +187,21 @@ public void enableRequestLoggingDetails() { assertThat(reader.isEnableLoggingRequestDetails()).isTrue(); } + @Test + public void enableRequestLoggingDetailsWithCustomCodecs() { + + this.configurer.registerDefaults(false); + this.configurer.defaultCodecs().enableLoggingRequestDetails(true); + + CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs(); + customCodecs.register(new FormHttpMessageReader()); + customCodecs.registerWithDefaultConfig(new FormHttpMessageReader()); + + List> readers = this.configurer.getReaders(); + assertThat(((FormHttpMessageReader) readers.get(0)).isEnableLoggingRequestDetails()).isFalse(); + assertThat(((FormHttpMessageReader) readers.get(1)).isEnableLoggingRequestDetails()).isTrue(); + } + @Test public void cloneConfigurer() { ServerCodecConfigurer clone = this.configurer.clone(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index 31d0433f487a..8a6e77ae0f9d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -350,10 +350,10 @@ static class CustomMessageConverterConfig extends WebFluxConfigurationSupport { @Override protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { configurer.registerDefaults(false); - configurer.customCodecs().decoder(StringDecoder.textPlainOnly()); - configurer.customCodecs().decoder(new Jaxb2XmlDecoder()); - configurer.customCodecs().encoder(CharSequenceEncoder.textPlainOnly()); - configurer.customCodecs().encoder(new Jaxb2XmlEncoder()); + configurer.customCodecs().register(StringDecoder.textPlainOnly()); + configurer.customCodecs().register(new Jaxb2XmlDecoder()); + configurer.customCodecs().register(CharSequenceEncoder.textPlainOnly()); + configurer.customCodecs().register(new Jaxb2XmlEncoder()); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java index b85e6c4248e2..22db275a41a6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java @@ -68,8 +68,8 @@ public void setup() { resolvers.addCustomResolver(new CustomSyncArgumentResolver()); ServerCodecConfigurer codecs = ServerCodecConfigurer.create(); - codecs.customCodecs().decoder(new ByteArrayDecoder()); - codecs.customCodecs().decoder(new ByteBufferDecoder()); + codecs.customCodecs().register(new ByteArrayDecoder()); + codecs.customCodecs().register(new ByteBufferDecoder()); AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(TestControllerAdvice.class);