问题背景
我们项目有个场景是上传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注解,完美解决字段复制及格式问题,准确率高。✅