踩坑系列 - BigDecimal

本文讲述了在金融开发中使用BigDecimal时遇到的三个常见问题:实例化可能产生的数值误差、equals方法比较大小的精度问题以及做运算时处理无限小数的方法。作者分享了解决方案,以帮助开发者避免这些陷阱。

BigDecimal引发的血案:

从事金融开发已经有快一年了,最近开发一个关于汇率和利息计算的业务,发现在使用BigDecimal的时候,遇到了几个很抓马的BUG,为了避免再犯,所以写了这个系列的文章记录一下,主要是记录工作期间(金融行业)遇到的一些坑,今天是第一篇,以后遇到的坑都会记录在此系列里面,还望各位大佬指点,也希望能帮助到一些小伙伴。

场景一、BigDecimal实例化造成的数值错误

问题场景代码

/**
* 计算利息Service
*/
public class CountInterestServiceImple implements ICountInterestService {
	public void countInterest() {
		// 计算利息
		BigDecimal rate = new BigDecimal(0.01);
		System.out.println("rate = " + rate );
		// 业务逻辑
		// ...
	}
}

在这里插入图片描述
通过输出发现这里的rate期望值应该是0.01,但是实际上却是上图所示的值。

原因
是因为BigDecimal在new的时候,会将double类型的参数转化为二进制的等价字节,然后存入BigDecimal中,但是对于想0.01这种没有值可以转化为二进制的等价数值的时候,就会出现一个近似值的情况
解决方式
有两种解决方式:

  1. 使用BigDecimal.valueof()方法,构建出一个BigDecimal
  2. 使用new BigDecimal(“”),直接绕过二进制转化,直接使用String,底层double.valueof转化。
BigDecimal bigDecimal2 = BigDecimal.valueOf(0.01);
BigDecimal bigDecimal3 = new BigDecimal("12345678456734535675.9543245347543475");
System.out.println("bigDecimal2 = " + bigDecimal2);
System.out.println("bigDecimal3 = " + bigDecimal3);

在这里插入图片描述
源码分析
BigDecimal.valueOf()底层就是通过Double.toString(val)将其转为字符串存入BigDecimal里面的,所以归根到底,最直接的方式就是使用new BigDecimal(“”)的方式构建BigDecimal

public static BigDecimal valueOf(double val) {
     // Reminder: a zero double returns '0.0', so we cannot fastpath
     // to use the constant ZERO.  This might be important enough to
     // justify a factory approach, a cache, or a few private
     // constants, later.
     return new BigDecimal(Double.toString(val));
}

public BigDecimal(String val) {
    this(val.toCharArray(), 0, val.length());
}

场景二、BigDecimal比较大小

问题代码复现

public class CountInterestServiceImple implements ICountInterestService {
	public void countInterest() {
		// 比较大小
		BigDecimal bigDecimal1 = new BigDecimal("0.01");
		BigDecimal bigDecimal2 = new BigDecimal("0.010");
		System.out.println(bigDecimal1.equals(bigDecimal2));
		// 业务逻辑
		// ...
	}
}

在这里插入图片描述
通过输出发现这里打印的是false,与期望的true并不符。

原因
是因为在使用equals比较大小的时候,BigDecimal里重写了Object的equals方法,不仅比较了数值大小,还比较了数值精度,也就是小数位数。0.01是两位小数,而0.010是三位小数,所以返回的是false。

解决方式
使用CompareTo方法比较大小,有三种返回值

  • -1:表示前面的数小于后面的数
  • 0:表示两数相等
  • 1:表示前面的数大于后面的数
BigDecimal bigDecimal1 = new BigDecimal("0.01");
BigDecimal bigDecimal2 = new BigDecimal("0.010");
System.out.println(bigDecimal1.compareTo(bigDecimal2));

在这里插入图片描述

场景三、BigDecimal做运算

bigDecimal在计算的时候,如果计算出来是一个无限小数,则会报错,需要考虑保留的小数位和保留模式

问题代码复现

public class CountInterestServiceImple implements ICountInterestService {
	public void countInterest() {
		// 计算
		BigDecimal bigDecimal1 = new BigDecimal("1");
		BigDecimal bigDecimal2 = new BigDecimal("3");
		BigDecimal bigDecimal3 = bigDecimal1.divide(bigDecimal2); // 0.3333...
		System.out.println(bigDecimal3);
		// 业务逻辑
		// ...
	}
}

在这里插入图片描述
可以看到打印抛出一个Non-terminating decimal Execption的错误,意思就是无线不循环小数,无法使用BigDecimal表示。

解决方式
使用BigDecimal的带有小数精度和保留模式的构造函数,指定计算后小数的保留规则

BigDecimal bigDecimal1 = new BigDecimal("1");
BigDecimal bigDecimal2 = new BigDecimal("3");
BigDecimal bigDecimal3 = bigDecimal1.divide(bigDecimal2,2,RoundingMode.HALF_UP); // 0.33
System.out.println(bigDecimal3);

在这里插入图片描述

小结

在金融行业里,BigDecimal的使用频率必然少不了,有的时候可以用Double来替换,大对于精度要求很高的场景,就不得不使用它,但是如果没有掌握正确的打开方式,又会造成反攻影响工期。上述三种场景是是我踩过的坑,记录下来,希望可以帮助到正在面临这类问题的小伙伴们。

<think> 我们已经在Spring Boot项目中配置了`spring.jackson.serialization.write-bigdecimal-as-plain=true`,但`LinkedHashMap`中的`BigDecimal`字段返回前端时仍未保留小数位数。可能的原因及解决方案如下: --- ### 原因分析 1. **配置未生效** - 检查配置位置:确保`application.properties`/`application.yml`位于`src/main/resources`目录且文件名正确。 - 配置格式正确性: - `.properties`格式:`spring.jackson.serialization.write-bigdecimal-as-plain=true` - `.yml`格式: ```yaml spring: jackson: serialization: WRITE_BIGDECIMAL_AS_PLAIN: true ``` 2. **数值被非`BigDecimal`类型存储** `LinkedHashMap<String, Object>`中若数值以`Double`/`Float`类型存储,配置对它们无效: ```java // 错误示例:使用Double类型 map.put("price", 123.456); // 实际类型为Double // 正确做法:显式使用BigDecimal map.put("price", new BigDecimal("123.456")); ``` 3. **自定义`ObjectMapper`覆盖全局配置** 若项目中存在自定义`ObjectMapper` Bean,可能覆盖Spring Boot的默认配置: ```java @Bean public ObjectMapper objectMapper() { // 若未设置WRITE_BIGDECIMAL_AS_PLAIN,会导致配置失效 return new ObjectMapper(); } ``` --- ### 解决方案 #### 步骤1:验证配置是否生效 在Controller中注入`ObjectMapper`,检查其配置: ```java @Autowired private ObjectMapper objectMapper; @GetMapping("/check-config") public String checkConfig() { SerializationConfig config = objectMapper.getSerializationConfig(); // 检查是否启用了WRITE_BIGDECIMAL_AS_PLAIN boolean isEnabled = config.isEnabled(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); return "WRITE_BIGDECIMAL_AS_PLAIN enabled: " + isEnabled; } ``` 若返回`false`,说明配置未生效[^1]。 --- #### 步骤2:确保使用`BigDecimal`类型 修改存值逻辑,避免自动装箱为`Double`: ```java import java.math.BigDecimal; LinkedHashMap<String, Object> map = new LinkedHashMap<>(); // 正确:使用字符串构造确保精度 map.put("amount", new BigDecimal("123.456789")); // 错误:Double会丢失精度 map.put("errorAmount", 123.456789); ``` --- #### 步骤3:修复自定义`ObjectMapper`配置 若存在自定义`ObjectMapper`,需显式启用`WRITE_BIGDECIMAL_AS_PLAIN`: ```java @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); return mapper; } ``` --- #### 步骤4:使用注解局部控制(可选) 在返回的DTO字段上直接使用注解: ```java public class ResponseDTO { @JsonFormat(shape = JsonFormat.Shape.STRING) // 以字符串形式输出,避免科学计数法 private BigDecimal value; } ``` --- ### 验证结果 调用接口返回结果示例: ```json { "amount": 123.456789 // 完整保留小数位 } ``` --- ### 相关问题 1. 如何验证Spring Boot中Jackson的全局配置是否生效? 2. 为什么`BigDecimal`在某些场景下仍会输出科学计数法? 3. 使用`BigDecimal`时如何避免前端接收为字符串? 4. 如何为不同精度的数值动态设置小数位数? 5. 除了小数精度,Jackson还有哪些控制数字输出的配置? [^1]: 关于Jackson配置的验证方法 </think> ### 问题原因分析 在 Spring Boot 项目中配置了 `spring.jackson.serialization.write-bigdecimal-as-plain=true` 后,`LinkedHashMap` 中的 `BigDecimal` 值仍未保留小数,可能由以下原因导致: --- ### 原因 1:配置未正确生效 **问题描述** Spring Boot 的 Jackson 配置可能存在覆盖或冲突: ```yaml # 正确配置示例 spring: jackson: serialization: WRITE_BIGDECIMAL_AS_PLAIN: true # 必须大写 ``` **解决方案** 1. 检查配置文件名和位置:确保是 `application.yml` 或 `application.properties` 2. 验证配置格式: - `.yml` 文件使用 `WRITE_BIGDECIMAL_AS_PLAIN: true` - `.properties` 文件使用 `spring.jackson.serialization.WRITE_BIGDECIMAL_AS_PLAIN=true` 3. 添加配置验证端点: ```java @RestController public class ConfigCheckController { @Autowired private ObjectMapper objectMapper; @GetMapping("/jackson-config") public String checkConfig() { return "WRITE_BIGDECIMAL_AS_PLAIN enabled: " + objectMapper.isEnabled(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); } } ``` --- ### 原因 2:值未真正转为 BigDecimal 类型 **问题描述** 当 `LinkedHashMap<String, Object>` 存入数值时,若未显式声明为 `BigDecimal`,可能被识别为 `Double`: ```java // 错误示例:自动装箱为 Double map.put("price", 123.456); // 正确示例:显式声明为 BigDecimal map.put("price", new BigDecimal("123.456")); ``` **解决方案** 强制使用 `BigDecimal` 构造器: ```java LinkedHashMap<String, Object> map = new LinkedHashMap<>(); Double value = 123.456789; map.put("price", BigDecimal.valueOf(value).setScale(6, RoundingMode.HALF_UP)); ``` --- ### 原因 3:自定义 ObjectMapper 覆盖配置 **问题描述** 若存在自定义 `ObjectMapper` Bean,会覆盖 Spring Boot 的默认配置: ```java @Bean // 此Bean会覆盖自动配置 public ObjectMapper customObjectMapper() { return new ObjectMapper(); // 未启用 WRITE_BIGDECIMAL_AS_PLAIN } ``` **解决方案** 在自定义 `ObjectMapper` 中显式启用配置: ```java @Bean public ObjectMapper customObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN); return mapper; } ``` --- ### 原因 4:序列化时精度丢失 **问题描述** 未设置 `BigDecimal` 的小数位数,Jackson 会输出原始值(可能包含多余尾随零) **解决方案** 使用 `setScale()` 显式设置精度: ```java BigDecimal value = new BigDecimal("123.45678900") .setScale(6, RoundingMode.HALF_UP); // 强制保留6位小数 map.put("price", value); ``` --- ### 完整验证示例 ```java @RestController public class DemoController { @GetMapping("/data") public LinkedHashMap<String, Object> getData() { LinkedHashMap<String, Object> map = new LinkedHashMap<>(); // 正确做法 BigDecimal price = new BigDecimal("123.456789") .setScale(6, RoundingMode.HALF_UP); map.put("product", "Phone"); map.put("price", price); return map; } } ``` **返回结果**: ```json { "product": "Phone", "price": 123.456789 // 保留完整小数位 } ``` --- ### 相关问题 1. 如何验证 Jackson 的全局配置是否生效? 2. 使用 `BigDecimal` 时如何避免内存占用过高? 3. 如何为不同接口动态设置小数位数? 4. Jackson 序列化时如何忽略某些字段? 5. 除了小数精度,还有哪些常用 Jackson 序列化配置? [^1]: Jackson 配置及 POJO 规范 [^2]: 自定义 ObjectMapper 的处理方式
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@WAT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值