From a03a116f6bd4c6ffa03265eeb3bbd4d7e7a9749d Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 7 Feb 2020 14:13:15 +0100 Subject: [PATCH] Force TokenBuffer to use BigDecimal if elementtype This commit makes the Jackson2Tokenizer enable TokenBuffer.forceUseOfBigDecimal if the element type given to the Decoder is BigDecimal. Previous to this commit, values would be converted to floats. Closes gh-24479 --- .../http/codec/json/AbstractJackson2Decoder.java | 12 ++++++++++-- .../http/codec/json/Jackson2Tokenizer.java | 7 ++++--- .../http/codec/json/Jackson2JsonDecoderTests.java | 13 ++++++++++++- .../http/codec/json/Jackson2TokenizerTests.java | 13 ++++++------- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java index 74cdbe42f54..7aae8092886 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/AbstractJackson2Decoder.java @@ -18,10 +18,12 @@ package org.springframework.http.codec.json; import java.io.IOException; import java.lang.annotation.Annotation; +import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -106,8 +108,14 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple @Nullable MimeType mimeType, @Nullable Map hints) { ObjectMapper mapper = getObjectMapper(); - Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), mapper.getFactory(), mapper, true, getMaxInMemorySize()); + + boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + forceUseOfBigDecimal = true; + } + + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), mapper.getFactory(), mapper, + true, forceUseOfBigDecimal, getMaxInMemorySize()); ObjectReader reader = getObjectReader(elementType, hints); diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java index fd31bee84f7..ff614066233 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2Tokenizer.java @@ -27,7 +27,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.async.ByteArrayFeeder; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; import com.fasterxml.jackson.databind.util.TokenBuffer; @@ -234,10 +233,13 @@ final class Jackson2Tokenizer { * @param objectMapper the current mapper instance * @param tokenizeArrays if {@code true} and the "top level" JSON object is * an array, each element is returned individually immediately after it is received + * @param forceUseOfBigDecimal if {@code true}, any floating point values encountered in source will use + * {@link java.math.BigDecimal} + * @param maxInMemorySize maximum memory size * @return the resulting token buffers */ public static Flux tokenize(Flux dataBuffers, JsonFactory jsonFactory, - ObjectMapper objectMapper, boolean tokenizeArrays, int maxInMemorySize) { + ObjectMapper objectMapper, boolean tokenizeArrays, boolean forceUseOfBigDecimal, int maxInMemorySize) { try { JsonParser parser = jsonFactory.createNonBlockingByteArrayParser(); @@ -246,7 +248,6 @@ final class Jackson2Tokenizer { context = ((DefaultDeserializationContext) context).createInstance( objectMapper.getDeserializationConfig(), parser, objectMapper.getInjectableValues()); } - boolean forceUseOfBigDecimal = objectMapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); Jackson2Tokenizer tokenizer = new Jackson2Tokenizer(parser, context, tokenizeArrays, forceUseOfBigDecimal, maxInMemorySize); return dataBuffers.concatMapIterable(tokenizer::tokenize).concatWith(tokenizer.endOfInput()); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java index dd2ba1334b2..a69c5bab3a4 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2JsonDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -17,6 +17,7 @@ package org.springframework.http.codec.json; import java.io.IOException; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -205,6 +206,16 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTests input = stringBuffer("[ 1E+2 ]").flux(); + + testDecode(input, BigDecimal.class, step -> step + .expectNext(new BigDecimal("1E+2")) + .verifyComplete() + ); + } + private Mono stringBuffer(String value) { return Mono.defer(() -> { byte[] bytes = value.getBytes(StandardCharsets.UTF_8); diff --git a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java index 5af6a0d3b83..501e9b4bbff 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/json/Jackson2TokenizerTests.java @@ -25,7 +25,6 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.TokenBuffer; import org.json.JSONException; @@ -249,7 +248,8 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTests { public void errorInStream() { DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":"); Flux source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException())); - Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, -1); + Flux result = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, true, + false, -1); StepVerifier.create(result) .expectError(RuntimeException.class) @@ -259,7 +259,8 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTests { @Test // SPR-16521 public void jsonEOFExceptionIsWrappedAsDecodingError() { Flux source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}")); - Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); + Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, + false, -1); StepVerifier.create(tokens) .expectError(DecodingException.class) @@ -269,10 +270,8 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTests { @ParameterizedTest @ValueSource(booleans = {false, true}) public void useBigDecimalForFloats(boolean useBigDecimalForFloats) { - this.objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, useBigDecimalForFloats); - Flux source = Flux.just(stringBuffer("1E+2")); - Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, -1); + Flux tokens = Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, useBigDecimalForFloats, -1); StepVerifier.create(tokens) .assertNext(tokenBuffer -> { @@ -299,7 +298,7 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTests { Flux tokens = Jackson2Tokenizer.tokenize( Flux.fromIterable(source).map(this::stringBuffer), - this.jsonFactory, this.objectMapper, tokenize, maxInMemorySize); + this.jsonFactory, this.objectMapper, tokenize, false, maxInMemorySize); return tokens .map(tokenBuffer -> {