Skip to content

Commit f46520e

Browse files
committed
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
1 parent 50493a0 commit f46520e

20 files changed

+749
-277
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,7 @@ project("spring-webflux") {
952952
optional "javax.servlet:javax.servlet-api:${servletVersion}"
953953
optional("javax.xml.bind:jaxb-api:${jaxbVersion}")
954954
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
955+
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jackson2Version}")
955956
optional("org.freemarker:freemarker:${freemarkerVersion}")
956957
optional("org.apache.httpcomponents:httpclient:${httpclientVersion}") {
957958
exclude group: "commons-logging", module: "commons-logging"

spring-web/src/main/java/org/springframework/http/codec/AbstractCodecConfigurer.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import org.springframework.core.codec.StringDecoder;
3535
import org.springframework.http.codec.json.Jackson2JsonDecoder;
3636
import org.springframework.http.codec.json.Jackson2JsonEncoder;
37+
import org.springframework.http.codec.json.Jackson2SmileDecoder;
38+
import org.springframework.http.codec.json.Jackson2SmileEncoder;
3739
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
3840
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
3941
import org.springframework.lang.Nullable;
@@ -54,6 +56,10 @@ abstract class AbstractCodecConfigurer implements CodecConfigurer {
5456
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",
5557
AbstractCodecConfigurer.class.getClassLoader());
5658

59+
private static final boolean jackson2SmilePresent =
60+
ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory",
61+
AbstractCodecConfigurer.class.getClassLoader());
62+
5763
protected static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder",
5864
AbstractCodecConfigurer.class.getClassLoader());
5965

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

121127
@Nullable
122-
private Jackson2JsonDecoder jackson2Decoder;
128+
private Jackson2JsonDecoder jackson2JsonDecoder;
123129

124130
@Nullable
125-
private Jackson2JsonEncoder jackson2Encoder;
131+
private Jackson2JsonEncoder jackson2JsonEncoder;
126132

127133
@Nullable
128134
private DefaultCustomCodecs customCodecs;
@@ -148,21 +154,21 @@ public DefaultCustomCodecs getCustomCodecs() {
148154
}
149155

150156
@Override
151-
public void jackson2Decoder(Jackson2JsonDecoder decoder) {
152-
this.jackson2Decoder = decoder;
157+
public void jackson2JsonDecoder(Jackson2JsonDecoder decoder) {
158+
this.jackson2JsonDecoder = decoder;
153159
}
154160

155-
protected Jackson2JsonDecoder jackson2Decoder() {
156-
return (this.jackson2Decoder != null ? this.jackson2Decoder : new Jackson2JsonDecoder());
161+
protected Jackson2JsonDecoder jackson2JsonDecoder() {
162+
return (this.jackson2JsonDecoder != null ? this.jackson2JsonDecoder : new Jackson2JsonDecoder());
157163
}
158164

159165
@Override
160-
public void jackson2Encoder(Jackson2JsonEncoder encoder) {
161-
this.jackson2Encoder = encoder;
166+
public void jackson2JsonEncoder(Jackson2JsonEncoder encoder) {
167+
this.jackson2JsonEncoder = encoder;
162168
}
163169

164-
protected Jackson2JsonEncoder jackson2Encoder() {
165-
return (this.jackson2Encoder != null ? this.jackson2Encoder : new Jackson2JsonEncoder());
170+
protected Jackson2JsonEncoder jackson2JsonEncoder() {
171+
return (this.jackson2JsonEncoder != null ? this.jackson2JsonEncoder : new Jackson2JsonEncoder());
166172
}
167173

168174
// Readers...
@@ -191,7 +197,10 @@ public List<HttpMessageReader<?>> getObjectReaders() {
191197
result.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
192198
}
193199
if (jackson2Present) {
194-
result.add(new DecoderHttpMessageReader<>(jackson2Decoder()));
200+
result.add(new DecoderHttpMessageReader<>(jackson2JsonDecoder()));
201+
}
202+
if (jackson2SmilePresent) {
203+
result.add(new DecoderHttpMessageReader<>(new Jackson2SmileDecoder()));
195204
}
196205
return result;
197206
}
@@ -229,7 +238,10 @@ public List<HttpMessageWriter<?>> getObjectWriters() {
229238
result.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
230239
}
231240
if (jackson2Present) {
232-
result.add(new EncoderHttpMessageWriter<>(jackson2Encoder()));
241+
result.add(new EncoderHttpMessageWriter<>(jackson2JsonEncoder()));
242+
}
243+
if (jackson2SmilePresent) {
244+
result.add(new EncoderHttpMessageWriter<>(new Jackson2SmileEncoder()));
233245
}
234246
return result;
235247
}

spring-web/src/main/java/org/springframework/http/codec/ClientCodecConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ interface ClientDefaultCodecs extends DefaultCodecs {
6464
/**
6565
* Configure the {@code Decoder} to use for Server-Sent Events.
6666
* <p>By default if this is not set, and Jackson is available, the
67-
* {@link #jackson2Decoder} override is used instead. Use this property
67+
* {@link #jackson2JsonDecoder} override is used instead. Use this property
6868
* if you want to further customize the SSE decoder.
6969
* @param decoder the decoder to use
7070
*/

spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ interface DefaultCodecs {
7474
* Override the default Jackson JSON {@code Decoder}.
7575
* @param decoder the decoder instance to use
7676
*/
77-
void jackson2Decoder(Jackson2JsonDecoder decoder);
77+
void jackson2JsonDecoder(Jackson2JsonDecoder decoder);
7878

7979
/**
8080
* Override the default Jackson JSON {@code Encoder}.
8181
* @param encoder the encoder instance to use
8282
*/
83-
void jackson2Encoder(Jackson2JsonEncoder encoder);
83+
void jackson2JsonEncoder(Jackson2JsonEncoder encoder);
8484

8585
}
8686

spring-web/src/main/java/org/springframework/http/codec/DefaultClientCodecConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private Decoder<?> getSseDecoder() {
8383
if (this.sseDecoder != null) {
8484
return this.sseDecoder;
8585
}
86-
return (jackson2Present ? jackson2Decoder() : null);
86+
return (jackson2Present ? jackson2JsonDecoder() : null);
8787
}
8888

8989
@Override

spring-web/src/main/java/org/springframework/http/codec/DefaultServerCodecConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private Encoder<?> getSseEncoder() {
9595
if (this.sseEncoder != null) {
9696
return this.sseEncoder;
9797
}
98-
return jackson2Present ? jackson2Encoder() : null;
98+
return jackson2Present ? jackson2JsonEncoder() : null;
9999
}
100100
}
101101

spring-web/src/main/java/org/springframework/http/codec/ServerCodecConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ interface ServerDefaultCodecs extends DefaultCodecs {
5555
/**
5656
* Configure the {@code Encoder} to use for Server-Sent Events.
5757
* <p>By default if this is not set, and Jackson is available, the
58-
* {@link #jackson2Encoder} override is used instead. Use this property
58+
* {@link #jackson2JsonEncoder} override is used instead. Use this property
5959
* if you want to further customize the SSE encoder.
6060
*/
6161
void serverSentEventEncoder(Encoder<?> encoder);
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.codec.json;
17+
18+
import java.io.IOException;
19+
import java.io.UncheckedIOException;
20+
import java.lang.annotation.Annotation;
21+
import java.util.Map;
22+
23+
import com.fasterxml.jackson.core.JsonFactory;
24+
import com.fasterxml.jackson.core.JsonParser;
25+
import com.fasterxml.jackson.core.JsonProcessingException;
26+
import com.fasterxml.jackson.databind.JavaType;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
import com.fasterxml.jackson.databind.ObjectReader;
29+
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
30+
import com.fasterxml.jackson.databind.util.TokenBuffer;
31+
import org.reactivestreams.Publisher;
32+
import reactor.core.publisher.Flux;
33+
import reactor.core.publisher.Mono;
34+
35+
import org.springframework.core.MethodParameter;
36+
import org.springframework.core.ResolvableType;
37+
import org.springframework.core.codec.CodecException;
38+
import org.springframework.core.codec.DecodingException;
39+
import org.springframework.core.io.buffer.DataBuffer;
40+
import org.springframework.http.codec.HttpMessageDecoder;
41+
import org.springframework.http.server.reactive.ServerHttpRequest;
42+
import org.springframework.http.server.reactive.ServerHttpResponse;
43+
import org.springframework.lang.Nullable;
44+
import org.springframework.util.Assert;
45+
import org.springframework.util.MimeType;
46+
47+
/**
48+
* Base class providing support methods for Jackson 2.9 decoding.
49+
*
50+
* @author Sebastien Deleuze
51+
* @author Rossen Stoyanchev
52+
* @since 5.0
53+
*/
54+
public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder<Object> {
55+
56+
/**
57+
* Constructor with a Jackson {@link ObjectMapper} to use.
58+
*/
59+
protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) {
60+
super(mapper, mimeTypes);
61+
}
62+
63+
@Override
64+
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
65+
JavaType javaType = objectMapper().getTypeFactory().constructType(elementType.getType());
66+
// Skip String: CharSequenceDecoder + "*/*" comes after
67+
return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
68+
objectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));
69+
}
70+
71+
@Override
72+
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
73+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
74+
75+
Flux<TokenBuffer> tokens = tokenize(input, true);
76+
return decodeInternal(tokens, elementType, mimeType, hints);
77+
}
78+
79+
@Override
80+
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
81+
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
82+
83+
Flux<TokenBuffer> tokens = tokenize(input, false);
84+
return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty();
85+
}
86+
87+
private Flux<TokenBuffer> tokenize(Publisher<DataBuffer> input, boolean tokenizeArrayElements) {
88+
try {
89+
JsonFactory factory = objectMapper().getFactory();
90+
JsonParser nonBlockingParser = factory.createNonBlockingByteArrayParser();
91+
Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(nonBlockingParser,
92+
tokenizeArrayElements);
93+
return Flux.from(input)
94+
.flatMap(tokenizer)
95+
.doFinally(t -> tokenizer.endOfInput());
96+
}
97+
catch (IOException ex) {
98+
return Flux.error(new UncheckedIOException(ex));
99+
}
100+
101+
}
102+
103+
private Flux<Object> decodeInternal(Flux<TokenBuffer> tokens,
104+
ResolvableType elementType, @Nullable MimeType mimeType,
105+
@Nullable Map<String, Object> hints) {
106+
107+
Assert.notNull(tokens, "'tokens' must not be null");
108+
Assert.notNull(elementType, "'elementType' must not be null");
109+
110+
MethodParameter param = getParameter(elementType);
111+
Class<?> contextClass = (param != null ? param.getContainingClass() : null);
112+
JavaType javaType = getJavaType(elementType.getType(), contextClass);
113+
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
114+
115+
ObjectReader reader = (jsonView != null ?
116+
objectMapper().readerWithView(jsonView).forType(javaType) :
117+
objectMapper().readerFor(javaType));
118+
119+
return tokens.map(tokenBuffer -> {
120+
try {
121+
return reader.readValue(tokenBuffer.asParser());
122+
}
123+
catch (InvalidDefinitionException ex) {
124+
throw new CodecException("Type definition error: " + ex.getType(), ex);
125+
}
126+
catch (JsonProcessingException ex) {
127+
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
128+
}
129+
catch (IOException ex) {
130+
throw new DecodingException("I/O error while parsing input stream", ex);
131+
}
132+
});
133+
}
134+
135+
136+
// HttpMessageDecoder...
137+
138+
@Override
139+
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
140+
ServerHttpRequest request, ServerHttpResponse response) {
141+
142+
return getHints(actualType);
143+
}
144+
145+
@Override
146+
protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) {
147+
return parameter.getParameterAnnotation(annotType);
148+
}
149+
}

0 commit comments

Comments
 (0)