使用Jackson将Double序列化为保留2位小数的格式

49
我正在使用Jackson和Spring MVC将一些简单的对象以JSON格式输出。其中一个对象有一个名为“amount”的属性,类型为Double。(我知道Double不应该用作货币金额,但这不是我的代码。)
在JSON输出中,我想将金额限制为2位小数。目前显示为:
"amount":459.99999999999994

我尝试使用Spring 3的@NumberFormat注解,但在这方面没有成功。看起来其他人也遇到了问题:MappingJacksonHttpMessageConverter的ObjectMapper在将JSON绑定到JavaBean属性时不使用ConversionService。

此外,我尝试使用@JsonSerialize注解,并使用自定义序列化器。
在模型中:

@JsonSerialize(using = CustomDoubleSerializer.class)
public Double getAmount()

下面是序列化器(serializer)的实现代码:

public class CustomDoubleSerializer extends JsonSerializer<Double> {
    @Override
    public void serialize(Double value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        if (null == value) {
            //write the word 'null' if there's no value available
            jgen.writeNull();
        } else {
            final String pattern = ".##";
            //final String pattern = "###,###,##0.00";
            final DecimalFormat myFormatter = new DecimalFormat(pattern);
            final String output = myFormatter.format(value);
            jgen.writeNumber(output);
        }
    }
}

CustomDoubleSerializer "似乎"可以工作。不过,有没有人能建议其他更简单(或更标准)的方法来实现这个功能。


1
一种方法是在setter方法中格式化金额,然后将值设置为字段。因此,getAmount()将返回2位小数。不确定是否符合您的要求。如果其他人期望字段的精度,则此实现可能会产生副作用。 - devang
1
一个四舍五入的序列化器对我来说似乎是正确的方法。或者,创建两个getter,即getAmountPrecise()getAmountRounded(),并且只序列化后者。 - millimoose
你能否为这个属性添加任何新的方法,例如在POJO中?你能否更改这个类或它属于外部库? - Michał Ziober
也许你可以通过使用 private static final DecimalFormat formatter = new DecimalFormat(".##"); 来提高性能,然后引用字段 formatter,因为它始终是相同的? - randers
你试过@JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT, pattern=...)吗?但我认为你的方法是标准的。 - WesternGun
@WesternGun 你好。我尝试了您建议的方法,但无法使其工作。您能为这种情况提供一个示例吗? - Sarvar Nishonboyev
5个回答

10

我知道使用Double 存储货币金额是不合适的。但是,这不是我的代码。

确实,不应该使用Double来存储货币金额,因为它不是无损的,并且提供更少的小数位控制。更好的选择是使用 BigDecimal

所以对于那些可以控制代码的人,可以像这样使用:

double amount = 111.222;
setAmount(new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP));

这将会被序列化为111.22。不需要自定义序列化器。


确实不应该:没错 ;) - mlo55

7
我在我的项目中遇到了类似的情况。我将格式化代码添加到POJO的setter方法中。DecimalFormatter、Math和其他类最终会四舍五入,然而,我的要求不是四舍五入值,而是只限制显示两位小数。
我重新创建了这种情况。 Product是一个具有成员Double amount的POJO。 JavaToJSON是一个类,它将创建Product的实例并将其转换为JSON。 在Product中的setter setAmount 将负责格式化为两个小数点。
以下是完整代码。
Product.java
package com;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Product {

    private String name;
    private Double amount;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Double getAmount() {
        return amount;
    }
    public void setAmount(Double amount) {
        BigDecimal bd = new BigDecimal(amount).setScale(2, RoundingMode.FLOOR);
        this.amount = bd.doubleValue();
    }

    @Override
    public String toString() {
        return "Product [name=" + name + ", amount=" + amount + "]";
    }
}

JavaToJSON.java

package com;

import java.io.File;
import java.io.IOException;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class JavaToJSON {

    public static void main(String[] args){

        ObjectMapper mapper = new ObjectMapper();

        try {
            Product product = new Product();
            product.setName("TestProduct");
            product.setAmount(Double.valueOf("459.99999999999994"));

            // Convert product to JSON and write to file
            mapper.writeValue(new File("d:\\user.json"), product);

            // display to console
            System.out.println(product);

        } catch (JsonGenerationException e) {

            e.printStackTrace();

        } catch (JsonMappingException e) {

            e.printStackTrace();

        } catch (IOException e) {

            e.printStackTrace();

        }
    }

}

我还没有积累足够的积分,所以无法上传截图来展示输出结果。

希望这能帮到您。


3
不需要截图。如果你想展示结果,只需在几行开头留出空格(就像你处理代码一样)。 - David Newcomb

3
关于上面提到的内容,我只是想修正一点,以免像我一样浪费时间。实际上应该使用

标签。
BigDecimal.valueOf(amount).xxx

替代

new BigDecimal(amount).xxx

而这实际上是非常关键的。因为如果你不这样做,你的小数部分将会出现问题。这是浮点表示法的一个限制,正如这里所述。


2
到目前为止,我看到的最好的方法是创建一个自定义的序列化器并使用@JsonSerializer(using=NewClass.class)。想尝试使用@JsonFormat(pattern=".##")之类的方式,但根据OP的一条评论(我认为格式化程序不会遵守这个规则),可能无法正常工作。
请参见此处:https://github.com/FasterXML/jackson-databind/issues/632
public class MoneyDeserializer extends JsonDeserializer<BigDecimal> {

    private NumberDeserializers.BigDecimalDeserializer delegate = NumberDeserializers.BigDecimalDeserializer.instance;

    @Override
    public BigDecimal deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        BigDecimal bd = delegate.deserialize(jp, ctxt);
        bd = bd.setScale(2, RoundingMode.HALF_UP);
        return bd;
    }    
}

但是,虽然更加方便且写的代码更少,通常来说,决定字段的规模是业务逻辑的关注点,而不是(反)序列化的一部分。要明确这一点。Jackson 应该能够原样传递数据。


-4
注意,459.99999999999994实际上是460,并且预期以这种方式进行序列化。因此,你的逻辑应该比仅丢弃数字更加巧妙。 我可能会建议像这样做:
Math.round(value*10)/10.0

你可能想把它放到setter中,并且摆脱自定义序列化。


2
像这样更改值仍可能导致序列化时带有显着尾数的数字。舍入严格是一个表示问题。例如,在Chrome控制台中尝试此操作 - (Math.round(Math.PI * 10) / 10.0).toFixed(20)。输出不是3.10000000000000000000,而是3.10000000000000008882 - Alnitak
1
答案是针对Java而不是JavaScript的。 - theme
2
IEEE-754浮点数的处理在两种语言中是相同的。 - Alnitak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接