Add Jackson Smile support to WebFlux

This binary format more efficient than JSON should be useful for server
to server communication, for example in micro-services use cases.

Issue: SPR-15424
This commit is contained in:
Sebastien Deleuze
2017-07-12 17:44:07 +02:00
parent 50493a0f5f
commit f46520e6e8
20 changed files with 749 additions and 277 deletions

View File

@@ -40,6 +40,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder;
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
@@ -62,7 +64,7 @@ public class ClientCodecConfigurerTests {
@Test
public void defaultReaders() throws Exception {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertEquals(9, readers.size());
assertEquals(10, readers.size());
assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass());
assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass());
assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass());
@@ -70,6 +72,7 @@ public class ClientCodecConfigurerTests {
assertStringDecoder(getNextDecoder(readers), true);
assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass());
assertSseReader(readers);
assertStringDecoder(getNextDecoder(readers), false);
}
@@ -77,7 +80,7 @@ public class ClientCodecConfigurerTests {
@Test
public void defaultWriters() throws Exception {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertEquals(10, writers.size());
assertEquals(11, writers.size());
assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass());
assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass());
assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass());
@@ -87,6 +90,7 @@ public class ClientCodecConfigurerTests {
assertEquals(MultipartHttpMessageWriter.class, writers.get(this.index.getAndIncrement()).getClass());
assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass());
assertStringEncoder(getNextEncoder(writers), false);
}
@@ -94,7 +98,7 @@ public class ClientCodecConfigurerTests {
public void jackson2EncoderOverride() throws Exception {
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
this.configurer.defaultCodecs().jackson2Decoder(decoder);
this.configurer.defaultCodecs().jackson2JsonDecoder(decoder);
assertSame(decoder, this.configurer.getReaders().stream()
.filter(reader -> ServerSentEventHttpMessageReader.class.equals(reader.getClass()))

View File

@@ -35,6 +35,8 @@ 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.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.util.MimeTypeUtils;
@@ -60,7 +62,7 @@ public class CodecConfigurerTests {
@Test
public void defaultReaders() throws Exception {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertEquals(8, readers.size());
assertEquals(9, readers.size());
assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass());
assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass());
assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass());
@@ -68,13 +70,14 @@ public class CodecConfigurerTests {
assertStringDecoder(getNextDecoder(readers), true);
assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass());
assertStringDecoder(getNextDecoder(readers), false);
}
@Test
public void defaultWriters() throws Exception {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertEquals(8, writers.size());
assertEquals(9, writers.size());
assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass());
assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass());
assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass());
@@ -82,6 +85,7 @@ public class CodecConfigurerTests {
assertStringEncoder(getNextEncoder(writers), true);
assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass());
assertStringEncoder(getNextEncoder(writers), false);
}
@@ -108,7 +112,7 @@ public class CodecConfigurerTests {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertEquals(12, readers.size());
assertEquals(13, readers.size());
assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass());
assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass());
assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass());
@@ -118,6 +122,7 @@ public class CodecConfigurerTests {
assertSame(customReader1, readers.get(this.index.getAndIncrement()));
assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass());
assertSame(customDecoder2, getNextDecoder(readers));
assertSame(customReader2, readers.get(this.index.getAndIncrement()));
assertEquals(StringDecoder.class, getNextDecoder(readers).getClass());
@@ -146,7 +151,7 @@ public class CodecConfigurerTests {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertEquals(12, writers.size());
assertEquals(13, writers.size());
assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass());
assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass());
assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass());
@@ -156,6 +161,7 @@ public class CodecConfigurerTests {
assertSame(customWriter1, writers.get(this.index.getAndIncrement()));
assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass());
assertSame(customEncoder2, getNextEncoder(writers));
assertSame(customWriter2, writers.get(this.index.getAndIncrement()));
assertEquals(CharSequenceEncoder.class, getNextEncoder(writers).getClass());
@@ -229,7 +235,7 @@ public class CodecConfigurerTests {
public void jackson2DecoderOverride() throws Exception {
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
this.configurer.defaultCodecs().jackson2Decoder(decoder);
this.configurer.defaultCodecs().jackson2JsonDecoder(decoder);
assertSame(decoder, this.configurer.getReaders().stream()
.filter(writer -> writer instanceof DecoderHttpMessageReader)
@@ -243,7 +249,7 @@ public class CodecConfigurerTests {
public void jackson2EncoderOverride() throws Exception {
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
this.configurer.defaultCodecs().jackson2Encoder(encoder);
this.configurer.defaultCodecs().jackson2JsonEncoder(encoder);
assertSame(encoder, this.configurer.getWriters().stream()
.filter(writer -> writer instanceof EncoderHttpMessageWriter)

View File

@@ -41,6 +41,8 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.json.Jackson2SmileDecoder;
import org.springframework.http.codec.json.Jackson2SmileEncoder;
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
import org.springframework.http.codec.multipart.SynchronossPartHttpMessageReader;
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
@@ -64,7 +66,7 @@ public class ServerCodecConfigurerTests {
@Test
public void defaultReaders() throws Exception {
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
assertEquals(11, readers.size());
assertEquals(12, readers.size());
assertEquals(ByteArrayDecoder.class, getNextDecoder(readers).getClass());
assertEquals(ByteBufferDecoder.class, getNextDecoder(readers).getClass());
assertEquals(DataBufferDecoder.class, getNextDecoder(readers).getClass());
@@ -75,13 +77,14 @@ public class ServerCodecConfigurerTests {
assertEquals(MultipartHttpMessageReader.class, readers.get(this.index.getAndIncrement()).getClass());
assertEquals(Jaxb2XmlDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2JsonDecoder.class, getNextDecoder(readers).getClass());
assertEquals(Jackson2SmileDecoder.class, getNextDecoder(readers).getClass());
assertStringDecoder(getNextDecoder(readers), false);
}
@Test
public void defaultWriters() throws Exception {
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
assertEquals(9, writers.size());
assertEquals(10, writers.size());
assertEquals(ByteArrayEncoder.class, getNextEncoder(writers).getClass());
assertEquals(ByteBufferEncoder.class, getNextEncoder(writers).getClass());
assertEquals(DataBufferEncoder.class, getNextEncoder(writers).getClass());
@@ -89,6 +92,7 @@ public class ServerCodecConfigurerTests {
assertStringEncoder(getNextEncoder(writers), true);
assertEquals(Jaxb2XmlEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2JsonEncoder.class, getNextEncoder(writers).getClass());
assertEquals(Jackson2SmileEncoder.class, getNextEncoder(writers).getClass());
assertSseWriter(writers);
assertStringEncoder(getNextEncoder(writers), false);
}
@@ -97,7 +101,7 @@ public class ServerCodecConfigurerTests {
public void jackson2EncoderOverride() throws Exception {
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
this.configurer.defaultCodecs().jackson2Encoder(encoder);
this.configurer.defaultCodecs().jackson2JsonEncoder(encoder);
assertSame(encoder, this.configurer.getWriters().stream()
.filter(writer -> ServerSentEventHttpMessageWriter.class.equals(writer.getClass()))

View File

@@ -0,0 +1,118 @@
/*
* 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.json;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.Pojo;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.http.MediaType.APPLICATION_JSON;
/**
* Unit tests for {@link Jackson2SmileDecoder}.
*
* @author Sebastien Deleuze
*/
public class Jackson2SmileDecoderTests extends AbstractDataBufferAllocatingTestCase {
private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile");
private final Jackson2SmileDecoder decoder = new Jackson2SmileDecoder();
@Test
public void canDecode() {
assertTrue(decoder.canDecode(forClass(Pojo.class), SMILE_MIME_TYPE));
assertTrue(decoder.canDecode(forClass(Pojo.class), null));
assertFalse(decoder.canDecode(forClass(String.class), null));
assertFalse(decoder.canDecode(forClass(Pojo.class), APPLICATION_JSON));
}
@Test
public void decodePojo() throws Exception {
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
Pojo pojo = new Pojo("foo", "bar");
byte[] serializedPojo = mapper.writer().writeValueAsBytes(pojo);
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedPojo));
ResolvableType elementType = forClass(Pojo.class);
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap());
StepVerifier.create(flux)
.expectNext(pojo)
.verifyComplete();
}
@Test
public void decodePojoWithError() throws Exception {
Flux<DataBuffer> source = Flux.just(stringBuffer("123"));
ResolvableType elementType = forClass(Pojo.class);
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap());
StepVerifier.create(flux).verifyError(CodecException.class);
}
@Test
public void decodeToList() throws Exception {
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"));
byte[] serializedList = mapper.writer().writeValueAsBytes(list);
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedList));
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
Mono<Object> mono = decoder.decodeToMono(source, elementType, null, emptyMap());
StepVerifier.create(mono)
.expectNext(list)
.expectComplete()
.verify();
}
@Test
public void decodeToFlux() throws Exception {
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
List<Pojo> list = asList(new Pojo("f1", "b1"), new Pojo("f2", "b2"));
byte[] serializedList = mapper.writer().writeValueAsBytes(list);
Flux<DataBuffer> source = Flux.just(this.bufferFactory.wrap(serializedList));
ResolvableType elementType = forClass(Pojo.class);
Flux<Object> flux = decoder.decode(source, elementType, null, emptyMap());
StepVerifier.create(flux)
.expectNext(new Pojo("f1", "b1"))
.expectNext(new Pojo("f2", "b2"))
.verifyComplete();
}
}

View File

@@ -0,0 +1,126 @@
/*
* 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.json;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.AbstractDataBufferAllocatingTestCase;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.Pojo;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.http.MediaType.APPLICATION_XML;
/**
* Unit tests for {@link Jackson2SmileEncoder}.
*
* @author Sebastien Deleuze
*/
public class Jackson2SmileEncoderTests extends AbstractDataBufferAllocatingTestCase {
private final static MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile");
private final Jackson2SmileEncoder encoder = new Jackson2SmileEncoder();
@Test
public void canEncode() {
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
assertTrue(this.encoder.canEncode(pojoType, SMILE_MIME_TYPE));
assertTrue(this.encoder.canEncode(pojoType, null));
// SPR-15464
assertTrue(this.encoder.canEncode(ResolvableType.NONE, null));
}
@Test
public void canNotEncode() {
assertFalse(this.encoder.canEncode(ResolvableType.forClass(String.class), null));
assertFalse(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML));
ResolvableType sseType = ResolvableType.forClass(ServerSentEvent.class);
assertFalse(this.encoder.canEncode(sseType, SMILE_MIME_TYPE));
}
@Test
public void encode() throws Exception {
Flux<Pojo> source = Flux.just(
new Pojo("foo", "bar"),
new Pojo("foofoo", "barbar"),
new Pojo("foofoofoo", "barbarbar")
);
ResolvableType type = ResolvableType.forClass(Pojo.class);
Flux<DataBuffer> output = this.encoder.encode(source, this.bufferFactory, type, null, emptyMap());
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
StepVerifier.create(output)
.consumeNextWith(dataBuffer -> readPojo(mapper, List.class, dataBuffer))
.verifyComplete();
}
@Test
public void encodeAsStream() throws Exception {
Flux<Pojo> source = Flux.just(
new Pojo("foo", "bar"),
new Pojo("foofoo", "barbar"),
new Pojo("foofoofoo", "barbarbar")
);
ResolvableType type = ResolvableType.forClass(Pojo.class);
MediaType mediaType = new MediaType("application", "stream+x-jackson-smile");
Flux<DataBuffer> output = this.encoder.encode(source, this.bufferFactory, type, mediaType, emptyMap());
ObjectMapper mapper = Jackson2ObjectMapperBuilder.smile().build();
StepVerifier.create(output)
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer))
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer))
.consumeNextWith(dataBuffer -> readPojo(mapper, Pojo.class, dataBuffer))
.verifyComplete();
}
public <T> T readPojo(ObjectMapper mapper, Class<T> valueType, DataBuffer dataBuffer) {
try {
T value = mapper.reader().forType(valueType).readValue(DataBufferTestUtils.dumpBytes(dataBuffer));
DataBufferUtils.release(dataBuffer);
return value;
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}