From 17117bd21adae1e4cb7cfb3219db35a6a987c55f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 7 Feb 2020 14:43:24 +0100 Subject: [PATCH] Force TokenBuffer to use BigDecimal if necessary 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 --- .../codec/json/AbstractJackson2Decoder.java | 21 ++++++++--- .../http/codec/json/Jackson2Tokenizer.java | 7 ++-- .../codec/json/Jackson2JsonDecoderTests.java | 35 ++++++++++++++----- .../codec/json/Jackson2TokenizerTests.java | 14 ++++---- 4 files changed, 54 insertions(+), 23 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 95586080ae..b0df789824 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 @@ -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. @@ -18,11 +18,13 @@ 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.JsonFactory; 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; @@ -113,8 +115,13 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple public Flux decode(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), true, getMaxInMemorySize()); + boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + forceUseOfBigDecimal = true; + } + + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + true, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints); } @@ -122,8 +129,12 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple public Mono decodeToMono(Publisher input, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - Flux tokens = Jackson2Tokenizer.tokenize( - Flux.from(input), this.jsonFactory, getObjectMapper(), false, getMaxInMemorySize()); + boolean forceUseOfBigDecimal = getObjectMapper().isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); + if (elementType != null && BigDecimal.class.equals(elementType.getType())) { + forceUseOfBigDecimal = true; + } + Flux tokens = Jackson2Tokenizer.tokenize(Flux.from(input), this.jsonFactory, getObjectMapper(), + false, forceUseOfBigDecimal, getMaxInMemorySize()); return decodeInternal(tokens, elementType, mimeType, hints).singleOrEmpty(); } 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 292888f7ea..4861ced5d1 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; @@ -232,10 +231,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(); @@ -244,7 +246,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.flatMap(tokenizer::tokenize, Flux::error, 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 3a0e941add..e3b158ee73 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; @@ -42,13 +43,21 @@ import org.springframework.http.MediaType; import org.springframework.http.codec.Pojo; import org.springframework.util.MimeType; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.junit.Assert.*; -import static org.springframework.core.ResolvableType.*; -import static org.springframework.http.MediaType.*; -import static org.springframework.http.codec.json.Jackson2JsonDecoder.*; -import static org.springframework.http.codec.json.JacksonViewBean.*; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.core.ResolvableType.forClass; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8; +import static org.springframework.http.MediaType.APPLICATION_STREAM_JSON; +import static org.springframework.http.MediaType.APPLICATION_XML; +import static org.springframework.http.codec.json.Jackson2JsonDecoder.JSON_VIEW_HINT; +import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1; +import static org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3; /** * Unit tests for {@link Jackson2JsonDecoder}. @@ -199,6 +208,16 @@ public class Jackson2JsonDecoderTests extends AbstractDecoderTestCase 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 ffda9a8ffc..9b6e90d1a3 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 @@ -26,7 +26,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; @@ -248,7 +247,8 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase { 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) @@ -258,7 +258,8 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase { @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) @@ -268,11 +269,10 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase { @Test public void useBigDecimalForFloats() { for (boolean useBigDecimalForFloats : Arrays.asList(false, true)) { - 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); + Jackson2Tokenizer.tokenize(source, this.jsonFactory, this.objectMapper, false, + useBigDecimalForFloats, -1); StepVerifier.create(tokens) .assertNext(tokenBuffer -> { @@ -300,7 +300,7 @@ public class Jackson2TokenizerTests extends AbstractLeakCheckingTestCase { 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 -> {