diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java index 512826045b..0d878c8339 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultControllerSpec.java @@ -25,8 +25,8 @@ import java.util.function.Consumer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.format.FormatterRegistry; -import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.validation.Validator; @@ -85,14 +85,8 @@ class DefaultControllerSpec extends AbstractMockServerSpec>> consumer) { - this.configurer.readersConsumer = consumer; - return this; - } - - @Override - public DefaultControllerSpec messageWriters(Consumer>> consumer) { - this.configurer.writersConsumer = consumer; + public DefaultControllerSpec httpMessageCodecs(Consumer consumer) { + this.configurer.messageCodecsConsumer = consumer; return this; } @@ -145,7 +139,7 @@ class DefaultControllerSpec extends AbstractMockServerSpec pathMatchConsumer; - private Consumer>> readersConsumer; + private Consumer messageCodecsConsumer; private Consumer>> writersConsumer; @@ -178,16 +172,9 @@ class DefaultControllerSpec extends AbstractMockServerSpec> readers) { - if (this.readersConsumer != null) { - this.readersConsumer.accept(readers); - } - } - - @Override - public void extendMessageWriters(List> writers) { - if (this.writersConsumer != null) { - this.writersConsumer.accept(writers); + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + if (this.messageCodecsConsumer != null) { + this.messageCodecsConsumer.accept(configurer); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 73869bac55..8b1750ecc1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -34,8 +34,7 @@ import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpRequest; -import org.springframework.http.codec.ServerHttpMessageReader; -import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.MultiValueMap; import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; @@ -244,16 +243,10 @@ public interface WebTestClient { ControllerSpec pathMatching(Consumer consumer); /** - * Modify or extend the list of built-in message readers. - * @see WebFluxConfigurer#configureMessageReaders + * Modify or extend the list of built-in message readers and writers. + * @see WebFluxConfigurer#configureHttpMessageCodecs */ - ControllerSpec messageReaders(Consumer>> readers); - - /** - * Modify or extend the list of built-in message writers. - * @see WebFluxConfigurer#configureMessageWriters - */ - ControllerSpec messageWriters(Consumer>> writers); + ControllerSpec httpMessageCodecs(Consumer configurer); /** * Register formatters and converters to use for type conversion. diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java new file mode 100644 index 0000000000..c9feafb92c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java @@ -0,0 +1,356 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.http.codec; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.core.codec.DataBufferDecoder; +import org.springframework.core.codec.DataBufferEncoder; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.ResourceDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.util.ClassUtils; + +/** + * Helps to configure a list of server-side HTTP message readers and writers + * with support for built-in defaults and options to register additional custom + * readers and writers via {@link #customCodec()}. + * + *

The built-in defaults include basic data types such as + * {@link Byte byte[]}, {@link java.nio.ByteBuffer ByteBuffer}, + * {@link org.springframework.core.io.buffer.DataBuffer DataBuffer}, + * {@link String}, {@link org.springframework.core.io.Resource Resource}, + * in addition to JAXB2 and Jackson 2 based on classpath detection, as well as + * support for Server-Sent Events. There are options to {@link #defaultCodec() + * override} some of the defaults or to have them + * {@link #registerDefaults(boolean) turned off} completely. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ServerCodecConfigurer { + + private static final boolean jackson2Present = + ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", + ServerCodecConfigurer.class.getClassLoader()) && + ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", + ServerCodecConfigurer.class.getClassLoader()); + + private static final boolean jaxb2Present = + ClassUtils.isPresent("javax.xml.bind.Binder", ServerCodecConfigurer.class.getClassLoader()); + + + private final DefaultCodecConfigurer defaultCodecs = new DefaultCodecConfigurer(); + + private final CustomCodecConfigurer customCodecs = new CustomCodecConfigurer(); + + + /** + * Provide overrides for built-in HTTP message readers and writers. + */ + public DefaultCodecConfigurer defaultCodec() { + return this.defaultCodecs; + } + + /** + * Whether to make default HTTP message reader and writer registrations. + *

By default this is set to {@code "true"}. + */ + public void registerDefaults(boolean register) { + this.defaultCodec().setSuppressed(!register); + } + + /** + * Register a custom encoder or decoder. + */ + public CustomCodecConfigurer customCodec() { + return this.customCodecs; + } + + + /** + * Prepare a list of HTTP message readers. + */ + public List> getReaders() { + + // Built-in, concrete Java type readers + List> result = new ArrayList<>(); + this.defaultCodecs.addReaderTo(result, ByteArrayDecoder.class, ByteArrayDecoder::new); + this.defaultCodecs.addReaderTo(result, ByteBufferDecoder.class, ByteBufferDecoder::new); + this.defaultCodecs.addReaderTo(result, DataBufferDecoder.class, DataBufferDecoder::new); + this.defaultCodecs.addReaderTo(result, ResourceDecoder.class, ResourceDecoder::new); + this.defaultCodecs.addStringReaderTextOnlyTo(result); + + // Custom, concrete Java type readers + this.customCodecs.addTypedReadersTo(result); + + // Built-in, Object-based readers + if (jaxb2Present) { + this.defaultCodecs.addReaderTo(result, Jaxb2XmlDecoder.class, Jaxb2XmlDecoder::new); + } + if (jackson2Present) { + this.defaultCodecs.addReaderTo(result, Jackson2JsonDecoder.class, Jackson2JsonDecoder::new); + } + + // Custom, Object-based readers + this.customCodecs.addObjectReadersTo(result); + + // Potentially overlapping Java types + "*/*" + this.defaultCodecs.addStringReaderTo(result); + return result; + } + + /** + * Prepare a list of HTTP message writers. + */ + public List> getWriters() { + + // Built-in, concrete Java type readers + List> result = new ArrayList<>(); + this.defaultCodecs.addWriterTo(result, ByteArrayEncoder.class, ByteArrayEncoder::new); + this.defaultCodecs.addWriterTo(result, ByteBufferEncoder.class, ByteBufferEncoder::new); + this.defaultCodecs.addWriterTo(result, DataBufferEncoder.class, DataBufferEncoder::new); + this.defaultCodecs.addWriterTo(result, ResourceHttpMessageWriter::new); + this.defaultCodecs.addStringWriterTextPlainOnlyTo(result); + + // Custom, concrete Java type readers + this.customCodecs.addTypedWritersTo(result); + + // Built-in, Object-based readers + if (jaxb2Present) { + this.defaultCodecs.addWriterTo(result, Jaxb2XmlEncoder.class, Jaxb2XmlEncoder::new); + } + if (jackson2Present) { + this.defaultCodecs.addWriterTo(result, Jackson2JsonEncoder.class, Jackson2JsonEncoder::new); + } + this.defaultCodecs.addSseWriterTo(result); + + // Custom, Object-based readers + this.customCodecs.addObjectWritersTo(result); + + // Potentially overlapping Java types + "*/*" + this.defaultCodecs.addStringWriterTo(result); + return result; + } + + + /** + * A registry and a factory for built-in HTTP message readers and writers. + */ + public static class DefaultCodecConfigurer { + + private boolean suppressed = false; + + private final Map, ServerHttpMessageReader> readers = new HashMap<>(); + + private final Map, ServerHttpMessageWriter> writers = new HashMap<>(); + + + /** + * Override the default Jackson {@code Decoder}. + * @param decoder the decoder to use + */ + public void jackson2Decoder(Jackson2JsonDecoder decoder) { + this.readers.put(Jackson2JsonDecoder.class, new DecoderHttpMessageReader<>(decoder)); + } + + /** + * Override the default Jackson {@code Encoder} for JSON. Also used for + * SSE unless further overridden via {@link #sse(Encoder)}. + * @param encoder the encoder to use + */ + public void jackson2Encoder(Jackson2JsonEncoder encoder) { + this.writers.put(Jackson2JsonEncoder.class, new EncoderHttpMessageWriter<>(encoder)); + } + + /** + * Configure the {@code Encoder} to use for Server-Sent Events. + *

By default the {@link #jackson2Encoder} override is used for SSE. + * @param encoder the encoder to use + */ + public void sse(Encoder encoder) { + ServerHttpMessageWriter writer = new ServerSentEventHttpMessageWriter(encoder); + this.writers.put(ServerSentEventHttpMessageWriter.class, writer); + } + + + // Internal methods for building a list of default readers or writers... + + private void setSuppressed(boolean suppressed) { + this.suppressed = suppressed; + } + + private > void addReaderTo(List> result, + Class key, Supplier fallback) { + + addReaderTo(result, () -> findReader(key, fallback)); + } + + private void addReaderTo(List> result, + Supplier> reader) { + + if (!this.suppressed) { + result.add(reader.get()); + } + } + + private > DecoderHttpMessageReader findReader( + Class key, Supplier fallback) { + + DecoderHttpMessageReader reader = (DecoderHttpMessageReader) this.readers.get(key); + return reader != null ? reader : new DecoderHttpMessageReader<>(fallback.get()); + } + + + private > void addWriterTo(List> result, + Class key, Supplier fallback) { + + addWriterTo(result, () -> findWriter(key, fallback)); + } + + private void addWriterTo(List> result, + Supplier> writer) { + + if (!this.suppressed) { + result.add(writer.get()); + } + } + + private > EncoderHttpMessageWriter findWriter( + Class key, Supplier fallback) { + + EncoderHttpMessageWriter writer = (EncoderHttpMessageWriter) this.writers.get(key); + return writer != null ? writer : new EncoderHttpMessageWriter<>(fallback.get()); + } + + + private void addStringReaderTextOnlyTo(List> result) { + addReaderTo(result, () -> new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly(true))); + } + + private void addStringReaderTo(List> result) { + addReaderTo(result, () -> new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); + } + + private void addStringWriterTextPlainOnlyTo(List> result) { + addWriterTo(result, () -> new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); + } + + private void addStringWriterTo(List> result) { + addWriterTo(result, () -> new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); + } + + private void addSseWriterTo(List> result) { + addWriterTo(result, () -> { + ServerHttpMessageWriter writer = this.writers.get(ServerSentEventHttpMessageWriter.class); + if (writer != null) { + return writer; + } + if (jackson2Present) { + return new ServerSentEventHttpMessageWriter( + findWriter(Jackson2JsonEncoder.class, Jackson2JsonEncoder::new).getEncoder()); + } + return new ServerSentEventHttpMessageWriter(); + }); + } + } + + /** + * Registry and container for custom HTTP message readers and writers. + */ + public static class CustomCodecConfigurer { + + private final List> typedReaders = new ArrayList<>(); + + private final List> typedWriters = new ArrayList<>(); + + private final List> objectReaders = new ArrayList<>(); + + private final List> objectWriters = new ArrayList<>(); + + + /** + * Add a custom {@code Decoder} internally wrapped with + * {@link DecoderHttpMessageReader}). + */ + public void decoder(Decoder decoder) { + reader(new DecoderHttpMessageReader<>(decoder)); + } + + /** + * Add a custom {@code Encoder}, internally wrapped with + * {@link EncoderHttpMessageWriter}. + */ + public void encoder(Encoder encoder) { + writer(new EncoderHttpMessageWriter<>(encoder)); + } + + /** + * Add a custom {@link ServerHttpMessageReader}. For readers of type + * {@link DecoderHttpMessageReader} consider using the shortcut + * {@link #decoder(Decoder)} instead. + */ + public void reader(ServerHttpMessageReader reader) { + boolean canReadToObject = reader.canRead(ResolvableType.forClass(Object.class), null); + (canReadToObject ? this.objectReaders : this.typedReaders).add(reader); + } + + /** + * Add a custom {@link ServerHttpMessageWriter}. For readers of type + * {@link EncoderHttpMessageWriter} consider using the shortcut + * {@link #encoder(Encoder)} instead. + */ + public void writer(ServerHttpMessageWriter writer) { + boolean canWriteObject = writer.canWrite(ResolvableType.forClass(Object.class), null); + (canWriteObject ? this.objectWriters : this.typedWriters).add(writer); + } + + + // Internal methods for building a list of custom readers or writers... + + private void addTypedReadersTo(List> result) { + result.addAll(this.typedReaders); + } + + private void addObjectReadersTo(List> result) { + result.addAll(this.objectReaders); + } + + private void addTypedWritersTo(List> result) { + result.addAll(this.typedWriters); + } + + private void addObjectWritersTo(List> result) { + result.addAll(this.objectWriters); + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java new file mode 100644 index 0000000000..09a008c9e6 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/codec/ServerCodecConfigurerTests.java @@ -0,0 +1,299 @@ +/* + * Copyright 2002-2017 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 + * + * http://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.http.codec; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteArrayEncoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.core.codec.ByteBufferEncoder; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.core.codec.DataBufferDecoder; +import org.springframework.core.codec.DataBufferEncoder; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.ResourceDecoder; +import org.springframework.core.codec.StringDecoder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.xml.Jaxb2XmlDecoder; +import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.core.ResolvableType.forClass; + +/** + * Unit tests for {@link ServerCodecConfigurer}. + * @author Rossen Stoyanchev + */ +public class ServerCodecConfigurerTests { + + private final ServerCodecConfigurer configurer = new ServerCodecConfigurer(); + + private final AtomicInteger index = new AtomicInteger(0); + + + @Test + public void defaultReaders() throws Exception { + List> readers = this.configurer.getReaders(); + assertEquals(8, readers.size()); + assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); + assertStringDecoder(getNextDecoder(readers), true); + assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertStringDecoder(getNextDecoder(readers), false); + } + + @Test + public void defaultWriters() throws Exception { + List> writers = this.configurer.getWriters(); + assertEquals(9, writers.size()); + assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); + assertStringEncoder(getNextEncoder(writers), true); + assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertSseWriter(writers); + assertStringEncoder(getNextEncoder(writers), false); + } + + @Test + public void defaultAndCustomReaders() throws Exception { + + Decoder customDecoder1 = mock(Decoder.class); + Decoder customDecoder2 = mock(Decoder.class); + + when(customDecoder1.canDecode(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customDecoder2.canDecode(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + ServerHttpMessageReader customReader1 = mock(ServerHttpMessageReader.class); + ServerHttpMessageReader customReader2 = mock(ServerHttpMessageReader.class); + + when(customReader1.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customReader2.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + this.configurer.customCodec().decoder(customDecoder1); + this.configurer.customCodec().decoder(customDecoder2); + + this.configurer.customCodec().reader(customReader1); + this.configurer.customCodec().reader(customReader2); + + List> readers = this.configurer.getReaders(); + + assertEquals(12, readers.size()); + assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(ResourceDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(StringDecoder.class, getNextDecoder(readers).getClass()); + assertSame(customDecoder1, getNextDecoder(readers)); + assertSame(customReader1, readers.get(this.index.getAndIncrement())); + assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass()); + assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass()); + assertSame(customDecoder2, getNextDecoder(readers)); + assertSame(customReader2, readers.get(this.index.getAndIncrement())); + assertEquals(StringDecoder.class, getNextDecoder(readers).getClass()); + } + + @Test + public void defaultAndCustomWriters() throws Exception { + + Encoder customEncoder1 = mock(Encoder.class); + Encoder customEncoder2 = mock(Encoder.class); + + when(customEncoder1.canEncode(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customEncoder2.canEncode(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + ServerHttpMessageWriter customWriter1 = mock(ServerHttpMessageWriter.class); + ServerHttpMessageWriter customWriter2 = mock(ServerHttpMessageWriter.class); + + when(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + this.configurer.customCodec().encoder(customEncoder1); + this.configurer.customCodec().encoder(customEncoder2); + + this.configurer.customCodec().writer(customWriter1); + this.configurer.customCodec().writer(customWriter2); + + List> writers = this.configurer.getWriters(); + + assertEquals(13, writers.size()); + assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ResourceHttpMessageWriter.class, writers.get(index.getAndIncrement()).getClass()); + assertEquals(CharSequenceEncoder.class, getNextEncoder(writers).getClass()); + assertSame(customEncoder1, getNextEncoder(writers)); + assertSame(customWriter1, writers.get(this.index.getAndIncrement())); + assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass()); + assertEquals(ServerSentEventHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass()); + assertSame(customEncoder2, getNextEncoder(writers)); + assertSame(customWriter2, writers.get(this.index.getAndIncrement())); + assertEquals(CharSequenceEncoder.class, getNextEncoder(writers).getClass()); + } + + @Test + public void defaultsOffCustomReaders() throws Exception { + + Decoder customDecoder1 = mock(Decoder.class); + Decoder customDecoder2 = mock(Decoder.class); + + when(customDecoder1.canDecode(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customDecoder2.canDecode(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + ServerHttpMessageReader customReader1 = mock(ServerHttpMessageReader.class); + ServerHttpMessageReader customReader2 = mock(ServerHttpMessageReader.class); + + when(customReader1.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customReader2.canRead(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + this.configurer.customCodec().decoder(customDecoder1); + this.configurer.customCodec().decoder(customDecoder2); + + this.configurer.customCodec().reader(customReader1); + this.configurer.customCodec().reader(customReader2); + + this.configurer.registerDefaults(false); + + List> readers = this.configurer.getReaders(); + + assertEquals(4, readers.size()); + assertSame(customDecoder1, getNextDecoder(readers)); + assertSame(customReader1, readers.get(this.index.getAndIncrement())); + assertSame(customDecoder2, getNextDecoder(readers)); + assertSame(customReader2, readers.get(this.index.getAndIncrement())); + } + + @Test + public void defaultsOffWithCustomWriters() throws Exception { + + Encoder customEncoder1 = mock(Encoder.class); + Encoder customEncoder2 = mock(Encoder.class); + + when(customEncoder1.canEncode(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customEncoder2.canEncode(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + ServerHttpMessageWriter customWriter1 = mock(ServerHttpMessageWriter.class); + ServerHttpMessageWriter customWriter2 = mock(ServerHttpMessageWriter.class); + + when(customWriter1.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(false); + when(customWriter2.canWrite(ResolvableType.forClass(Object.class), null)).thenReturn(true); + + this.configurer.customCodec().encoder(customEncoder1); + this.configurer.customCodec().encoder(customEncoder2); + + this.configurer.customCodec().writer(customWriter1); + this.configurer.customCodec().writer(customWriter2); + + this.configurer.registerDefaults(false); + + List> writers = this.configurer.getWriters(); + + assertEquals(4, writers.size()); + assertSame(customEncoder1, getNextEncoder(writers)); + assertSame(customWriter1, writers.get(this.index.getAndIncrement())); + assertSame(customEncoder2, getNextEncoder(writers)); + assertSame(customWriter2, writers.get(this.index.getAndIncrement())); + } + + @Test + public void jackson2DecoderOverride() throws Exception { + + Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(); + this.configurer.defaultCodec().jackson2Decoder(decoder); + + assertSame(decoder, this.configurer.getReaders().stream() + .filter(writer -> writer instanceof DecoderHttpMessageReader) + .map(writer -> ((DecoderHttpMessageReader) writer).getDecoder()) + .filter(e -> Jackson2JsonDecoder.class.equals(e.getClass())) + .findFirst() + .filter(e -> e == decoder).orElse(null)); + } + + @Test + public void jackson2EncoderOverride() throws Exception { + + Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); + this.configurer.defaultCodec().jackson2Encoder(encoder); + + assertSame(encoder, this.configurer.getWriters().stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder()) + .filter(e -> Jackson2JsonEncoder.class.equals(e.getClass())) + .findFirst() + .filter(e -> e == encoder).orElse(null)); + + assertSame(encoder, this.configurer.getWriters().stream() + .filter(writer -> ServerSentEventHttpMessageWriter.class.equals(writer.getClass())) + .map(writer -> (ServerSentEventHttpMessageWriter) writer) + .findFirst() + .map(ServerSentEventHttpMessageWriter::getEncoder) + .filter(e -> e == encoder).orElse(null)); + } + + + private Decoder getNextDecoder(List> readers) { + HttpMessageReader reader = readers.get(this.index.getAndIncrement()); + assertEquals(DecoderHttpMessageReader.class, reader.getClass()); + return ((DecoderHttpMessageReader) reader).getDecoder(); + } + + private Encoder getNextEncoder(List> writers) { + HttpMessageWriter writer = writers.get(this.index.getAndIncrement()); + assertEquals(EncoderHttpMessageWriter.class, writer.getClass()); + return ((EncoderHttpMessageWriter) writer).getEncoder(); + } + + private void assertStringDecoder(Decoder decoder, boolean textOnly) { + assertEquals(StringDecoder.class, decoder.getClass()); + assertTrue(decoder.canDecode(forClass(String.class), MimeTypeUtils.TEXT_PLAIN)); + assertEquals(!textOnly, decoder.canDecode(forClass(String.class), MediaType.TEXT_EVENT_STREAM)); + } + + private void assertStringEncoder(Encoder encoder, boolean textOnly) { + assertEquals(CharSequenceEncoder.class, encoder.getClass()); + assertTrue(encoder.canEncode(forClass(String.class), MimeTypeUtils.TEXT_PLAIN)); + assertEquals(!textOnly, encoder.canEncode(forClass(String.class), MediaType.TEXT_EVENT_STREAM)); + } + + private void assertSseWriter(List> writers) { + ServerHttpMessageWriter writer = writers.get(this.index.getAndIncrement()); + assertEquals(ServerSentEventHttpMessageWriter.class, writer.getClass()); + Encoder encoder = ((ServerSentEventHttpMessageWriter) writer).getEncoder(); + assertNotNull(encoder); + assertEquals(Jackson2JsonEncoder.class, encoder.getClass()); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java index 18cb061f3e..a986150401 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/DelegatingWebFluxConfiguration.java @@ -21,8 +21,7 @@ import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; -import org.springframework.http.codec.ServerHttpMessageReader; -import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -76,13 +75,8 @@ public class DelegatingWebFluxConfiguration extends WebFluxConfigurationSupport } @Override - protected void configureMessageReaders(List> messageReaders) { - this.configurers.configureMessageReaders(messageReaders); - } - - @Override - protected void extendMessageReaders(List> messageReaders) { - this.configurers.extendMessageReaders(messageReaders); + protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + this.configurers.configureHttpMessageCodecs(configurer); } @Override @@ -100,16 +94,6 @@ public class DelegatingWebFluxConfiguration extends WebFluxConfigurationSupport return this.configurers.getMessageCodesResolver().orElse(super.getMessageCodesResolver()); } - @Override - protected void configureMessageWriters(List> messageWriters) { - this.configurers.configureMessageWriters(messageWriters); - } - - @Override - protected void extendMessageWriters(List> messageWriters) { - this.configurers.extendMessageWriters(messageWriters); - } - @Override protected void configureViewResolvers(ViewResolverRegistry registry) { this.configurers.configureViewResolvers(registry); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index fc657dfa12..c8f6ed9a7c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -30,32 +30,13 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.Order; -import org.springframework.core.codec.ByteArrayDecoder; -import org.springframework.core.codec.ByteArrayEncoder; -import org.springframework.core.codec.ByteBufferDecoder; -import org.springframework.core.codec.ByteBufferEncoder; -import org.springframework.core.codec.CharSequenceEncoder; -import org.springframework.core.codec.DataBufferDecoder; -import org.springframework.core.codec.DataBufferEncoder; -import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.ResourceDecoder; -import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.converter.Converter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.MediaType; -import org.springframework.http.codec.DecoderHttpMessageReader; -import org.springframework.http.codec.EncoderHttpMessageWriter; -import org.springframework.http.codec.ResourceHttpMessageWriter; -import org.springframework.http.codec.ServerHttpMessageReader; -import org.springframework.http.codec.ServerHttpMessageWriter; -import org.springframework.http.codec.ServerSentEventHttpMessageWriter; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; -import org.springframework.http.codec.xml.Jaxb2XmlDecoder; -import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; @@ -104,9 +85,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { private PathMatchConfigurer pathMatchConfigurer; - private List> messageReaders; - - private List> messageWriters; + private ServerCodecConfigurer messageCodecsConfigurer; private ApplicationContext applicationContext; @@ -267,7 +246,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); - adapter.setMessageReaders(getMessageReaders()); + adapter.setMessageReaders(getMessageCodecsConfigurer().getReaders()); adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); adapter.setReactiveAdapterRegistry(webFluxAdapterRegistry()); @@ -294,58 +273,22 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { } /** - * Main method to access message readers to use for decoding - * controller method arguments with. - *

Use {@link #configureMessageReaders} to configure the list or - * {@link #extendMessageReaders} to add in addition to the default ones. + * Main method to access the configurer for HTTP message readers and writers. + *

Use {@link #configureHttpMessageCodecs(ServerCodecConfigurer)} to + * configure the readers and writers. */ - protected final List> getMessageReaders() { - if (this.messageReaders == null) { - this.messageReaders = new ArrayList<>(); - configureMessageReaders(this.messageReaders); - if (this.messageReaders.isEmpty()) { - addDefaultHttpMessageReaders(this.messageReaders); - } - extendMessageReaders(this.messageReaders); + protected final ServerCodecConfigurer getMessageCodecsConfigurer() { + if (this.messageCodecsConfigurer == null) { + this.messageCodecsConfigurer = new ServerCodecConfigurer(); + configureHttpMessageCodecs(this.getMessageCodecsConfigurer()); } - return this.messageReaders; + return this.messageCodecsConfigurer; } /** - * Override to configure the message readers to use for decoding - * controller method arguments. - *

If no message readres are specified, default will be added via - * {@link #addDefaultHttpMessageReaders}. - * @param messageReaders a list to add message readers to, initially an empty + * Override to configure the HTTP message readers and writers to use. */ - protected void configureMessageReaders(List> messageReaders) { - } - - /** - * Adds default converters that sub-classes can call from - * {@link #configureMessageReaders(List)} for {@code byte[]}, - * {@code ByteBuffer}, {@code String}, {@code Resource}, JAXB2, and Jackson - * (if present on the classpath). - */ - protected final void addDefaultHttpMessageReaders(List> readers) { - readers.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); - readers.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - readers.add(new DecoderHttpMessageReader<>(new DataBufferDecoder())); - readers.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); - readers.add(new DecoderHttpMessageReader<>(new ResourceDecoder())); - if (jaxb2Present) { - readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); - } - if (jackson2Present) { - readers.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); - } - } - - /** - * Override this to modify the list of message readers after it has been - * configured, for example to add some in addition to the default ones. - */ - protected void extendMessageReaders(List> messageReaders) { + protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { } /** @@ -435,76 +378,14 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { @Bean public ResponseEntityResultHandler responseEntityResultHandler() { - return new ResponseEntityResultHandler( - getMessageWriters(), webFluxContentTypeResolver(), webFluxAdapterRegistry()); + return new ResponseEntityResultHandler(getMessageCodecsConfigurer().getWriters(), + webFluxContentTypeResolver(), webFluxAdapterRegistry()); } @Bean public ResponseBodyResultHandler responseBodyResultHandler() { - return new ResponseBodyResultHandler( - getMessageWriters(), webFluxContentTypeResolver(), webFluxAdapterRegistry()); - } - - /** - * Main method to access message writers to use for encoding return values. - *

Use {@link #configureMessageWriters(List)} to configure the list or - * {@link #extendMessageWriters(List)} to add in addition to the default ones. - */ - protected final List> getMessageWriters() { - if (this.messageWriters == null) { - this.messageWriters = new ArrayList<>(); - configureMessageWriters(this.messageWriters); - if (this.messageWriters.isEmpty()) { - addDefaultHttpMessageWriters(this.messageWriters); - } - extendMessageWriters(this.messageWriters); - } - return this.messageWriters; - } - /** - * Override to configure the message writers to use for encoding - * return values. - *

If no message readers are specified, default will be added via - * {@link #addDefaultHttpMessageWriters}. - * @param messageWriters a list to add message writers to, initially an empty - */ - protected void configureMessageWriters(List> messageWriters) { - } - - /** - * Adds default converters that sub-classes can call from - * {@link #configureMessageWriters(List)}. - */ - protected final void addDefaultHttpMessageWriters(List> writers) { - writers.add(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); - writers.add(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); - writers.add(new EncoderHttpMessageWriter<>(new DataBufferEncoder())); - writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); - writers.add(new ResourceHttpMessageWriter()); - if (jaxb2Present) { - writers.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); - } - if (jackson2Present) { - writers.add(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder())); - } - writers.add(new ServerSentEventHttpMessageWriter(getSseEncoder())); - writers.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); - } - - private Encoder getSseEncoder() { - if (jackson2Present) { - return new Jackson2JsonEncoder(); - } - else { - return null; - } - } - - /** - * Override this to modify the list of message writers after it has been - * configured, for example to add some in addition to the default ones. - */ - protected void extendMessageWriters(List> messageWriters) { + return new ResponseBodyResultHandler(getMessageCodecsConfigurer().getWriters(), + webFluxContentTypeResolver(), webFluxAdapterRegistry()); } @Bean diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java index fdf29d8f54..1f51f69901 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurer.java @@ -22,8 +22,7 @@ import java.util.Optional; import org.springframework.core.convert.converter.Converter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; -import org.springframework.http.codec.ServerHttpMessageReader; -import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.reactive.accept.CompositeContentTypeResolver; @@ -89,22 +88,10 @@ public interface WebFluxConfigurer { } /** - * Configure the message readers to use for decoding the request body where - * {@code @RequestBody} and {@code HttpEntity} controller method arguments - * are used. If none are specified, default ones are added based on - * {@link WebFluxConfigurationSupport#addDefaultHttpMessageReaders}. - *

See {@link #extendMessageReaders(List)} for adding readers - * in addition to the default ones. - * @param readers an empty list to add message readers to + * Configure custom HTTP message readers and writers or override built-in ones. + * @param configurer the configurer to use */ - default void configureMessageReaders(List> readers) { - } - - /** - * An alternative to {@link #configureMessageReaders(List)} that allows - * modifying the message readers to use after default ones have been added. - */ - default void extendMessageReaders(List> readers) { + default void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { } /** @@ -132,25 +119,6 @@ public interface WebFluxConfigurer { return Optional.empty(); } - /** - * Configure the message writers to use to encode the response body based on - * the return values of {@code @ResponseBody}, and {@code ResponseEntity} - * controller methods. If none are specified, default ones are added based on - * {@link WebFluxConfigurationSupport#addDefaultHttpMessageWriters(List)}. - *

See {@link #extendMessageWriters(List)} for adding writers - * in addition to the default ones. - * @param writers a empty list to add message writers to - */ - default void configureMessageWriters(List> writers) { - } - - /** - * An alternative to {@link #configureMessageWriters(List)} that allows - * modifying the message writers to use after default ones have been added. - */ - default void extendMessageWriters(List> writers) { - } - /** * Configure view resolution for processing the return values of controller * methods that rely on resolving a diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java index 51070f12c0..bfb92ff1bb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurerComposite.java @@ -23,8 +23,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.format.FormatterRegistry; -import org.springframework.http.codec.ServerHttpMessageReader; -import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.CollectionUtils; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -52,42 +51,37 @@ public class WebFluxConfigurerComposite implements WebFluxConfigurer { @Override public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) { - this.delegates.stream().forEach(delegate -> delegate.configureContentTypeResolver(builder)); + this.delegates.forEach(delegate -> delegate.configureContentTypeResolver(builder)); } @Override public void addCorsMappings(CorsRegistry registry) { - this.delegates.stream().forEach(delegate -> delegate.addCorsMappings(registry)); + this.delegates.forEach(delegate -> delegate.addCorsMappings(registry)); } @Override public void configurePathMatching(PathMatchConfigurer configurer) { - this.delegates.stream().forEach(delegate -> delegate.configurePathMatching(configurer)); + this.delegates.forEach(delegate -> delegate.configurePathMatching(configurer)); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - this.delegates.stream().forEach(delegate -> delegate.addResourceHandlers(registry)); + this.delegates.forEach(delegate -> delegate.addResourceHandlers(registry)); } @Override public void addArgumentResolvers(List resolvers) { - this.delegates.stream().forEach(delegate -> delegate.addArgumentResolvers(resolvers)); + this.delegates.forEach(delegate -> delegate.addArgumentResolvers(resolvers)); } @Override - public void configureMessageReaders(List> readers) { - this.delegates.stream().forEach(delegate -> delegate.configureMessageReaders(readers)); - } - - @Override - public void extendMessageReaders(List> readers) { - this.delegates.stream().forEach(delegate -> delegate.extendMessageReaders(readers)); + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + this.delegates.forEach(delegate -> delegate.configureHttpMessageCodecs(configurer)); } @Override public void addFormatters(FormatterRegistry registry) { - this.delegates.stream().forEach(delegate -> delegate.addFormatters(registry)); + this.delegates.forEach(delegate -> delegate.addFormatters(registry)); } @Override @@ -100,19 +94,9 @@ public class WebFluxConfigurerComposite implements WebFluxConfigurer { return createSingleBean(WebFluxConfigurer::getMessageCodesResolver, MessageCodesResolver.class); } - @Override - public void configureMessageWriters(List> writers) { - this.delegates.stream().forEach(delegate -> delegate.configureMessageWriters(writers)); - } - - @Override - public void extendMessageWriters(List> writers) { - this.delegates.stream().forEach(delegate -> delegate.extendMessageWriters(writers)); - } - @Override public void configureViewResolvers(ViewResolverRegistry registry) { - this.delegates.stream().forEach(delegate -> delegate.configureViewResolvers(registry)); + this.delegates.forEach(delegate -> delegate.configureViewResolvers(registry)); } private Optional createSingleBean(Function> factory, diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java index 218b8df555..2d2d334149 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultHandlerStrategiesBuilder.java @@ -26,26 +26,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.context.ApplicationContext; -import org.springframework.core.codec.ByteArrayDecoder; -import org.springframework.core.codec.ByteArrayEncoder; -import org.springframework.core.codec.ByteBufferDecoder; -import org.springframework.core.codec.ByteBufferEncoder; -import org.springframework.core.codec.CharSequenceEncoder; -import org.springframework.core.codec.Encoder; -import org.springframework.core.codec.StringDecoder; -import org.springframework.http.codec.DecoderHttpMessageReader; -import org.springframework.http.codec.EncoderHttpMessageWriter; -import org.springframework.http.codec.FormHttpMessageReader; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; -import org.springframework.http.codec.ResourceHttpMessageWriter; -import org.springframework.http.codec.ServerSentEventHttpMessageWriter; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; -import org.springframework.http.codec.xml.Jaxb2XmlDecoder; -import org.springframework.http.codec.xml.Jaxb2XmlEncoder; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.web.reactive.result.view.ViewResolver; /** @@ -61,16 +45,6 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { .map(Locale.LanguageRange::getRange) .map(Locale::forLanguageTag).findFirst(); - private static final boolean jackson2Present = - ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", - DefaultHandlerStrategiesBuilder.class.getClassLoader()) && - ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", - DefaultHandlerStrategiesBuilder.class.getClassLoader()); - - private static final boolean jaxb2Present = - ClassUtils.isPresent("javax.xml.bind.Binder", - DefaultHandlerStrategiesBuilder.class.getClassLoader()); - private final List> messageReaders = new ArrayList<>(); @@ -82,40 +56,12 @@ class DefaultHandlerStrategiesBuilder implements HandlerStrategies.Builder { public void defaultConfiguration() { - messageReader(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); - messageReader(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); - messageReader(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); - messageReader(new FormHttpMessageReader()); - - messageWriter(new EncoderHttpMessageWriter<>(new ByteArrayEncoder())); - messageWriter(new EncoderHttpMessageWriter<>(new ByteBufferEncoder())); - messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); - messageWriter(new ResourceHttpMessageWriter()); - - if (jaxb2Present) { - messageReader(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); - messageWriter(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); - } - if (jackson2Present) { - messageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())); - messageWriter(new EncoderHttpMessageWriter<>(new Jackson2JsonEncoder())); - } - - messageWriter(new ServerSentEventHttpMessageWriter(getSseEncoder())); - messageWriter(new EncoderHttpMessageWriter<>(CharSequenceEncoder.allMimeTypes())); - + ServerCodecConfigurer configurer = new ServerCodecConfigurer(); + configurer.getReaders().forEach(this::messageReader); + configurer.getWriters().forEach(this::messageWriter); localeResolver(DEFAULT_LOCALE_RESOLVER); } - private Encoder getSseEncoder() { - if (jackson2Present) { - return new Jackson2JsonEncoder(); - } - else { - return null; - } - } - public void applicationContext(ApplicationContext applicationContext) { applicationContext.getBeansOfType(HttpMessageReader.class).values().forEach(this::messageReader); applicationContext.getBeansOfType(HttpMessageWriter.class).values().forEach(this::messageWriter); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index dbed19a45c..9a062d5e46 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -42,8 +42,10 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.DataBufferDecoder; +import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -114,16 +116,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application public RequestMappingHandlerAdapter() { - // TODO: improve with better (shared) defaults this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new DataBufferDecoder())); + this.messageReaders.add(new DecoderHttpMessageReader<>(new ResourceDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes(true))); } /** - * Configure message readers to de-serialize the request body with. + * Configure HTTP message readers to de-serialize the request body with. + *

By default only basic data types such as bytes and text are registered. + * Consider using {@link ServerCodecConfigurer} to configure a richer list + * including JSON encoding . + * @see ServerCodecConfigurer */ public void setMessageReaders(List> messageReaders) { this.messageReaders.clear(); @@ -131,7 +137,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application } /** - * Return the configured message readers. + * Return the configured HTTP message readers. */ public List> getMessageReaders() { return this.messageReaders; diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java index dc1fbc1494..8a373ab0f8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/DelegatingWebFluxConfigurationTests.java @@ -30,8 +30,8 @@ import org.mockito.MockitoAnnotations; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.convert.ConversionService; import org.springframework.format.FormatterRegistry; -import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; @@ -58,7 +58,7 @@ public class DelegatingWebFluxConfigurationTests { private WebFluxConfigurer webFluxConfigurer; @Captor - private ArgumentCaptor>> readers; + private ArgumentCaptor codecsConfigurer; @Captor private ArgumentCaptor>> writers; @@ -96,15 +96,14 @@ public class DelegatingWebFluxConfigurationTests { ConversionService initializerConversionService = initializer.getConversionService(); assertTrue(initializer.getValidator() instanceof LocalValidatorFactoryBean); - verify(webFluxConfigurer).configureMessageReaders(readers.capture()); - verify(webFluxConfigurer).extendMessageReaders(readers.capture()); + verify(webFluxConfigurer).configureHttpMessageCodecs(codecsConfigurer.capture()); verify(webFluxConfigurer).getValidator(); verify(webFluxConfigurer).getMessageCodesResolver(); verify(webFluxConfigurer).addFormatters(formatterRegistry.capture()); verify(webFluxConfigurer).addArgumentResolvers(any()); assertSame(formatterRegistry.getValue(), initializerConversionService); - assertEquals(7, readers.getValue().size()); + assertEquals(8, codecsConfigurer.getValue().getReaders().size()); } @Test @@ -126,8 +125,7 @@ public class DelegatingWebFluxConfigurationTests { delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer)); delegatingConfig.responseBodyResultHandler(); - verify(webFluxConfigurer).configureMessageWriters(writers.capture()); - verify(webFluxConfigurer).extendMessageWriters(writers.capture()); + verify(webFluxConfigurer).configureHttpMessageCodecs(codecsConfigurer.capture()); verify(webFluxConfigurer).configureContentTypeResolver(any(RequestedContentTypeResolverBuilder.class)); } 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 99919d89e3..db3efd0ac1 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 @@ -34,10 +34,9 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; -import org.springframework.http.codec.DecoderHttpMessageReader; -import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.ServerHttpMessageReader; import org.springframework.http.codec.ServerHttpMessageWriter; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder; @@ -128,7 +127,7 @@ public class WebFluxConfigurationSupportTests { assertNotNull(adapter); List> readers = adapter.getMessageReaders(); - assertEquals(7, readers.size()); + assertEquals(8, readers.size()); assertHasMessageReader(readers, byte[].class, APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, ByteBuffer.class, APPLICATION_OCTET_STREAM); @@ -297,23 +296,12 @@ public class WebFluxConfigurationSupportTests { static class CustomMessageConverterConfig extends WebFluxConfigurationSupport { @Override - protected void configureMessageReaders(List> messageReaders) { - messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.textPlainOnly(true))); - } - - @Override - protected void configureMessageWriters(List> messageWriters) { - messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly())); - } - - @Override - protected void extendMessageReaders(List> messageReaders) { - messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder())); - } - - @Override - protected void extendMessageWriters(List> messageWriters) { - messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder())); + protected void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + configurer.registerDefaults(false); + configurer.customCodec().decoder(StringDecoder.textPlainOnly(true)); + configurer.customCodec().decoder(new Jaxb2XmlDecoder()); + configurer.customCodec().encoder(CharSequenceEncoder.textPlainOnly()); + configurer.customCodec().encoder(new Jaxb2XmlEncoder()); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java index 911436c0ec..7563397ad8 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DispatcherHandlerIntegrationTests.java @@ -120,16 +120,19 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr @Bean public HandlerMapping handlerMapping(RouterFunction routerFunction, ApplicationContext applicationContext) { + return RouterFunctions.toHandlerMapping(routerFunction, new HandlerStrategies() { @Override public Supplier>> messageReaders() { - return () -> getMessageReaders().stream().map(reader -> (HttpMessageReader) reader); + return () -> getMessageCodecsConfigurer().getReaders().stream() + .map(reader -> (HttpMessageReader) reader); } @Override public Supplier>> messageWriters() { - return () -> getMessageWriters().stream().map(writer -> (HttpMessageWriter) writer); + return () -> getMessageCodecsConfigurer().getWriters().stream() + .map(writer -> (HttpMessageWriter) writer); } @Override