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

问题背景

我们项目有个场景是上传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,看是否能有不错的解决方案
https://github.com/alibaba/easyexcel/issues/2595

解决方案

对此,经过几天的源码解读,我发现可以用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注解,完美解决字段复制及格式问题,准确率高。✅
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
好的,了解您的要求。关于easyexcel读写,可以参考以下步骤: 1. 引入依赖 ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.10</version> </dependency> ``` 2. 写入Excel文件 ```java // 创建Excel对应的实体对象 public class DemoData { @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; } // 写入Excel public void writeExcel() { // 文件输出位置 String fileName = "demo.xlsx"; // 写法1 EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(dataList); // 写法2 ExcelWriter excelWriter = null; try { excelWriter = EasyExcel.write(fileName, DemoData.class).build(); WriteSheet writeSheet = EasyExcel.writerSheet("模板").build(); excelWriter.write(dataList, writeSheet); } finally { if (excelWriter != null) { excelWriter.finish(); } } } ``` 3. 读取Excel文件 ```java // 读取Excel public void readExcel() { // 文件路径 String fileName = "demo.xlsx"; // 读取sheet1内容 EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead(); // 读取所有sheet内容 EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll(); } // 读取监听器 public class DemoDataListener extends AnalysisEventListener<DemoData> { private List<DemoData> dataList = new ArrayList<>(); @Override public void invoke(DemoData data, AnalysisContext context) { // 读取到一条数据时执行 dataList.add(data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 读取完成后执行 // 可以将dataList进行保存操作 } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小飞侠fly

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

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

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

打赏作者

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

抵扣说明:

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

余额充值