现象
在项目进行数据初始化导入时,系统提示数据校验失败,报错信息如下: 校验未通过,错误信息:财务金额精度校验失败!币种:CNY,币种允许最大精度:2,当前值:1.5897500 但是在检查输入的Excel数据时发现,并没有1.58975这样的输入数据。进一步排查系统接口请求记录,发现了疑似报错的数据信息:
发现该金额字段在输出时,使用了科学计数法对数据进行描述。根据日志中的其它字段定位到了Excel中的数据行,发现原始金额为15897500。
按照预期来看15897500这个数据自然是符合CNY币种精度的,而且根据报错信息,数值变为了1.5897500,数值发生了很大的变化,需要进一步定位原因。
复现
按照已有的现象来看,基本可以确定是校验精度的方法逻辑中,发生了错误。精度校验的思路和代码非常简单,将传入的对象使用Hutool的NumberUtil转换为BigDecimal对象,然后按照指定的币种精度进行四舍五入,判断两者是否相等。因此,猜测是NumberUtil转换方法存在问题。编写测试代码进行验证:
@Test
public void scientificNotationTest() throws Exception {
BigDecimal originalAmount = new BigDecimal("1.58975E+7");
BigDecimal parseAmount = NumberUtil.toBigDecimal(originalAmount.toString());
log.info("originalAmount: {}, parseAmount: {}", originalAmount, parseAmount);
}
运行结果
originalAmount: 1.58975E+7, parseAmount: 1.58975
发现数值已经有异常了,可以定位NumberUtil的toBigDecimal存在BUG。
问题原因
查看源码cn.hutool.core.util.NumberUtil#toBigDecimal(java.lang.String)
/**
* 数字转{@link BigDecimal}<br>
* null或""或空白符转换为0
*
* @param numberStr 数字字符串
* @return {@link BigDecimal}
* @since 4.0.9
*/
public static BigDecimal toBigDecimal(String numberStr) {
if (StrUtil.isBlank(numberStr)) {
return BigDecimal.ZERO;
}
try {
// 支持类似于 1,234.55 格式的数字
final Number number = parseNumber(numberStr);
if (number instanceof BigDecimal) {
return (BigDecimal) number;
} else {
return new BigDecimal(number.toString());
}
} catch (Exception ignore) {
// 忽略解析错误
}
return new BigDecimal(numberStr);
}
很显然,针对科学计数法的字符串输入,会按照parseNumber的逻辑进行格式化,但查看parseNumber方法的实现,明显不支持科学计数法。
/**
* 将指定字符串转换为{@link Number} 对象<br>
* 此方法不支持科学计数法
*
* @param numberStr Number字符串
* @return Number对象
* @throws NumberFormatException 包装了{@link ParseException},当给定的数字字符串无法解析时抛出
* @since 4.1.15
*/
public static Number parseNumber(String numberStr) throws NumberFormatException {
try {
final NumberFormat format = NumberFormat.getInstance();
if(format instanceof DecimalFormat){
// issue#1818@Github
// 当字符串数字超出double的长度时,会导致截断,此处使用BigDecimal接收
((DecimalFormat) format).setParseBigDecimal(true);
}
return format.parse(numberStr);
} catch (ParseException e) {
final NumberFormatException nfe = new NumberFormatException(e.getMessage());
nfe.initCause(e);
throw nfe;
}
}
问题解决
当前服务依赖的hutool的版本是5.7.16版本。查看NumberUtil的最新开源代码发现,当前实现逻辑已经调整,调整后实现逻辑如下:
/**
* 数字转{@link BigDecimal}<br>
* null或""或空白符转换为0
*
* @param numberStr 数字字符串
* @return {@link BigDecimal}
* @since 4.0.9
*/
public static BigDecimal toBigDecimal(String numberStr) {
if (StrUtil.isBlank(numberStr)) {
return BigDecimal.ZERO;
}
try {
return new BigDecimal(numberStr);
} catch (Exception ignore) {
// 忽略解析错误
}
// 支持类似于 1,234.55 格式的数字
return toBigDecimal(parseNumber(numberStr));
}
通过调整执行顺序,修复了此bug。
查看代码提交记录和hutool的发版记录,发现该BUG修复于5.8.22版本。