Hutool NumberUtil.toBigDecimal转换踩坑记录

756 阅读3分钟

现象

在项目进行数据初始化导入时,系统提示数据校验失败,报错信息如下: 校验未通过,错误信息:财务金额精度校验失败!币种:CNY,币种允许最大精度:2,当前值:1.5897500 但是在检查输入的Excel数据时发现,并没有1.58975这样的输入数据。进一步排查系统接口请求记录,发现了疑似报错的数据信息:

image.png

发现该金额字段在输出时,使用了科学计数法对数据进行描述。根据日志中的其它字段定位到了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版本。

image.png