Skip to content

Commit

Permalink
Add Jackson Smile support to WebFlux
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sdeleuze committed Jul 13, 2017
1 parent 50493a0 commit f46520e
Show file tree
Hide file tree
Showing 20 changed files with 749 additions and 277 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ project("spring-webflux") {
optional "javax.servlet:javax.servlet-api:${servletVersion}"
optional("javax.xml.bind:jaxb-api:${jaxbVersion}")
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jackson2Version}")
optional("org.freemarker:freemarker:${freemarkerVersion}")
optional("org.apache.httpcomponents:httpclient:${httpclientVersion}") {
exclude group: "commons-logging", module: "commons-logging"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
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.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.lang.Nullable;
Expand All @@ -54,6 +56,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer {
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
AbstractCodecConfigurer.class.getClassLoader());

private static final boolean jackson2SmilePresent =
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory",
AbstractCodecConfigurer.class.getClassLoader());

protected static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder",
AbstractCodecConfigurer.class.getClassLoader());

Expand Down Expand Up @@ -119,10 +125,10 @@ abstract protected static class AbstractDefaultCodecs implements DefaultCodecs {
private boolean registerDefaults = true;

@Nullable
private Jackson2JsonDecoder jackson2Decoder;
private Jackson2JsonDecoder jackson2JsonDecoder;

@Nullable
private Jackson2JsonEncoder jackson2Encoder;
private Jackson2JsonEncoder jackson2JsonEncoder;

@Nullable
private DefaultCustomCodecs customCodecs;
Expand All @@ -148,21 +154,21 @@ public DefaultCustomCodecs getCustomCodecs() {
}

@Override
public void jackson2Decoder(Jackson2JsonDecoder decoder) {
this.jackson2Decoder = decoder;
public void jackson2JsonDecoder(Jackson2JsonDecoder decoder) {
this.jackson2JsonDecoder = decoder;
}

protected Jackson2JsonDecoder jackson2Decoder() {
return (this.jackson2Decoder != null ? this.jackson2Decoder : new Jackson2JsonDecoder());
protected Jackson2JsonDecoder jackson2JsonDecoder() {
return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder());
}

@Override
public void jackson2Encoder(Jackson2JsonEncoder encoder) {
this.jackson2Encoder = encoder;
public void jackson2JsonEncoder(Jackson2JsonEncoder encoder) {
this.jackson2JsonEncoder = encoder;
}

protected Jackson2JsonEncoder jackson2Encoder() {
return (this.jackson2Encoder != null ? this.jackson2Encoder : new Jackson2JsonEncoder());
protected Jackson2JsonEncoder jackson2JsonEncoder() {
return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder());
}

// Readers...
Expand Down Expand Up @@ -191,7 +197,10 @@ public List<HttpMessageReader<?>> getObjectReaders() {
result.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
}
if (jackson2Present) {
result.add(new DecoderHttpMessageReader<>(jackson2Decoder()));
result.add(new DecoderHttpMessageReader<>(jackson2JsonDecoder()));
}
if (jackson2SmilePresent) {
result.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
}
return result;
}
Expand Down Expand Up @@ -229,7 +238,10 @@ public List<HttpMessageWriter<?>> getObjectWriters() {
result.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
}
if (jackson2Present) {
result.add(new EncoderHttpMessageWriter<>(jackson2Encoder()));
result.add(new EncoderHttpMessageWriter<>(jackson2JsonEncoder()));
}
if (jackson2SmilePresent) {
result.add(new EncoderHttpMessageWriter<>(new Jackson2SmileEncoder()));
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ interface ClientDefaultCodecs extends DefaultCodecs {
/**
* Configure the {@code Decoder} to use for Server-Sent Events.
* <p>By default if this is not set, and Jackson is available, the
* {@link #jackson2Decoder} override is used instead. Use this property
* {@link #jackson2JsonDecoder} override is used instead. Use this property
* if you want to further customize the SSE decoder.
* @param decoder the decoder to use
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ interface DefaultCodecs {
* Override the default Jackson JSON {@code Decoder}.
* @param decoder the decoder instance to use
*/
void jackson2Decoder(Jackson2JsonDecoder decoder);
void jackson2JsonDecoder(Jackson2JsonDecoder decoder);

/**
* Override the default Jackson JSON {@code Encoder}.
* @param encoder the encoder instance to use
*/
void jackson2Encoder(Jackson2JsonEncoder encoder);
void jackson2JsonEncoder(Jackson2JsonEncoder encoder);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ private Decoder<?> getSseDecoder() {
if (this.sseDecoder != null) {
return this.sseDecoder;
}
return (jackson2Present ? jackson2Decoder() : null);
return (jackson2Present ? jackson2JsonDecoder() : null);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private Encoder<?> getSseEncoder() {
if (this.sseEncoder != null) {
return this.sseEncoder;
}
return jackson2Present ? jackson2Encoder() : null;
return jackson2Present ? jackson2JsonEncoder() : null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface ServerDefaultCodecs extends DefaultCodecs {
/**
* Configure the {@code Encoder} to use for Server-Sent Events.
* <p>By default if this is not set, and Jackson is available, the
* {@link #jackson2Encoder} override is used instead. Use this property
* {@link #jackson2JsonEncoder} override is used instead. Use this property
* if you want to further customize the SSE encoder.
*/
void serverSentEventEncoder(Encoder<?> encoder);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.lang.annotation.Annotation;
import java.util.Map;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.HttpMessageDecoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;

/**
* Base class providing support methods for Jackson 2.9 decoding.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder<Object> {

/**
* Constructor with a Jackson {@link ObjectMapper} to use.
*/
protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
}

@Override
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
JavaType javaType = objectMapper().getTypeFactory().constructType(elementType.getType());
// Skip String: CharSequenceDecoder + "*/*" comes after
return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
objectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));
}

@Override
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

Flux<TokenBuffer> tokens = tokenize(input, true);
return decodeInternal(tokens, elementType, mimeType, hints);
}

@Override
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {

Flux<TokenBuffer> tokens = tokenize(input, false);
return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty();
}

private Flux<TokenBuffer> tokenize(Publisher<DataBuffer> input, boolean tokenizeArrayElements) {
try {
JsonFactory factory = objectMapper().getFactory();
JsonParser nonBlockingParser = factory.createNonBlockingByteArrayParser();
Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(nonBlockingParser,
tokenizeArrayElements);
return Flux.from(input)
.flatMap(tokenizer)
.doFinally(t -> tokenizer.endOfInput());
}
catch (IOException ex) {
return Flux.error(new UncheckedIOException(ex));
}

}

private Flux<Object> decodeInternal(Flux<TokenBuffer> tokens,
ResolvableType elementType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {

Assert.notNull(tokens, "'tokens' must not be null");
Assert.notNull(elementType, "'elementType' must not be null");

MethodParameter param = getParameter(elementType);
Class<?> contextClass = (param != null ? param.getContainingClass() : null);
JavaType javaType = getJavaType(elementType.getType(), contextClass);
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);

ObjectReader reader = (jsonView != null ?
objectMapper().readerWithView(jsonView).forType(javaType) :
objectMapper().readerFor(javaType));

return tokens.map(tokenBuffer -> {
try {
return reader.readValue(tokenBuffer.asParser());
}
catch (InvalidDefinitionException ex) {
throw new CodecException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
}
catch (IOException ex) {
throw new DecodingException("I/O error while parsing input stream", ex);
}
});
}


// HttpMessageDecoder...

@Override
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
ServerHttpRequest request, ServerHttpResponse response) {

return getHints(actualType);
}

@Override
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
return parameter.getParameterAnnotation(annotType);
}
}
Loading

0 comments on commit f46520e

Please sign in to comment.