EasyExcel读写数字类型数据丢失精度解决方案

2,098 阅读2分钟

本文已参与[新人创作礼]活动,一起开启掘金创作之路。

问题背景

我们项目有个场景是上传excel文件,上传的文档中有保留两位小数的数字格式,上传之后的数据会经过处理后在写入另一个文档。如果用String去读写,会保留精度,但读文件的时候只能读格式化之后的数字。如果用BigDecimal去读写,能读到真实的数字,但在处理之后写入excel时有可能会丢失精度。 如果用BigDecimal去读,String写入,可以解决这个问题,但涉及大量的dto copy,影响效率。下面我们来看看demo演示。

demo演示

  • 代码如下
public class Test5 {
    public static void main(String[] args) {
        String fileName = "g://" + "demo" + File.separator + "demo.xlsx";
        List<DemoData> list = EasyExcel.read(fileName, DemoData.class, null).sheet().doReadSync();
        List<DemoData1> result = new ArrayList<>();
        for(DemoData demoData:list){
            if(demoData.getCol2() != null){
                demoData.setCol2(demoData.getCol2().add(new BigDecimal("500000000000.123456")));

                DemoData1 demoData1= new DemoData1();
                demoData1.setCol1(demoData.getCol1());
                demoData1.setCol2(demoData.getCol2().toString());
                result.add(demoData1);
            }
        }
        //case1
        EasyExcel.write("g://" + "demo" + File.separator + "demoTemp1.xlsx",DemoData.class).sheet().doWrite(list);
        //case2
        EasyExcel.write("g://" + "demo" + File.separator + "demoTemp2.xlsx",DemoData1.class).sheet().doWrite(result);
    }
}
public class DemoData {
    @ExcelProperty("序号")
    private String col1;
    @ExcelProperty("数字1")
    private BigDecimal col2;

    public String getCol1() {
        return col1;
    }

    public void setCol1(String col1) {
        this.col1 = col1;
    }

    public BigDecimal getCol2() {
        return col2;
    }

    public void setCol2(BigDecimal col2) {
        this.col2 = col2;
    }
}
public class DemoData1 {
    @ExcelProperty("序号")
    private String col1;
    @ExcelProperty("数字1")
    private String col2;

    public String getCol1() {
        return col1;
    }

    public void setCol1(String col1) {
        this.col1 = col1;
    }

    public String getCol2() {
        return col2;
    }

    public void setCol2(String col2) {
        this.col2 = col2;
    }
}
  • 准备以下导入文档 图一

执行结果说明

在Test5中,我们预想的是读到"500000000.123456"再经过数据处理,与"500000000000.123456"相加,输出结果"500500000000.246912" case1是用BigDecimal读跟写,case2用BigDecimal读,String写。 下面我们来看一下执行结果,读入内存数据如下图: 图二 case1执行结果如下图 图三 case2执行结果如下图 图四 通过以上截图可以发现以下几点: 1、读入内存数据正确 2、case1以BigDecimal读写数据后,精度丢失,这个精度会丢失是跟excel低层语言特性有关。 3、case2以BigDecimal去读数据,然后以String类型写数据,结果正确,但对比case1,涉及到大量的dto copy,影响读写效率。 如果我们以String去读写,则会读到如下数据,可以发现是按单元格格式读的,不是我们想要的结果 图五

对此,我也像官方提了issue,看是否能有不错的解决方案 github.com/alibaba/eas…

解决方案

对此,经过几天的源码解读,我发现可以用String类型去读写数字,只需要在对应字段上添加注解**@NumberFormat**,添加注解之后读取数据截图如下,可以发现能正常读取小数位数据。 这样就能实现用String类型的dto去同时读写excel文档,而不影响数字精度。 图六

源码展现

能这样绕过源码,主要是在如下源码处com.alibaba.excel.converters.string.StringNumberConverter#convertToJavaData 如果发现有@NumberFormat注解,则会优先处理此注解,否则就会使用表格格式来格式化数据。

@Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
        GlobalConfiguration globalConfiguration) {
        // If there are "DateTimeFormat", read as date
        if (contentProperty != null && contentProperty.getDateTimeFormatProperty() != null) {
            return DateUtils.format(
                DateUtil.getJavaDate(cellData.getNumberValue().doubleValue(),
                    contentProperty.getDateTimeFormatProperty().getUse1904windowing(), null),
                contentProperty.getDateTimeFormatProperty().getFormat());
        }
        // If there are "NumberFormat", read as number
        if (contentProperty != null && contentProperty.getNumberFormatProperty() != null) {
            return NumberUtils.format(cellData.getNumberValue(), contentProperty);
        }
        // Excel defines formatting
        boolean hasDataFormatData = cellData.getDataFormatData() != null
            && cellData.getDataFormatData().getIndex() != null && !StringUtils.isEmpty(
            cellData.getDataFormatData().getFormat());
        if (hasDataFormatData) {
            return NumberDataFormatterUtils.format(cellData.getNumberValue(),
                cellData.getDataFormatData().getIndex(), cellData.getDataFormatData().getFormat(), globalConfiguration);
        }
        // Default conversion number
        return NumberUtils.format(cellData.getNumberValue(), contentProperty);
    }

深入看处理@NumberFormat注解的代码中,如果判定有此注解,但是注解上的格式化数据为空的话,则会走第一个if逻辑,直接返回toPlainString()的数据,因此能以最简单的方式解决此问题。

 public static String format(Number num, ExcelContentProperty contentProperty) {
        if (contentProperty == null || contentProperty.getNumberFormatProperty() == null
            || StringUtils.isEmpty(contentProperty.getNumberFormatProperty().getFormat())) {
            if (num instanceof BigDecimal) {
                return ((BigDecimal)num).toPlainString();
            } else {
                return num.toString();
            }
        }
        String format = contentProperty.getNumberFormatProperty().getFormat();
        RoundingMode roundingMode = contentProperty.getNumberFormatProperty().getRoundingMode();
        DecimalFormat decimalFormat = new DecimalFormat(format);
        decimalFormat.setRoundingMode(roundingMode);
        return decimalFormat.format(num);
    }

总结

在使用easyExcel来读写数字类型时

  • 用BigDecimal读写,在写入时容易丢失精度。❌
  • 用BigDecimal读,String写,读写都没问题,但涉及字段复制,影响效率。❌
  • 用String读写,读数据时易丢失精度,且各种格式都需要处理。❌
  • 用String读写,在字段上添加@NumberFormat注解,完美解决字段复制及格式问题,准确率高。✅