在一些导出业务场景中,经常需要将数据导出存储到excel,而Excel作为一个重要的工具,在数据处理与分析上也起到了至关重要的作用。但是,Excel的操作繁琐、效率低下等问题也制约着我们的工作效率。为了解决这个问题,我们可以使用EasyExcel这个优秀的Java类库,来优化Excel数据的读写,并提高我们的工作效率。
在本文中,我将向大家介绍如何使用EasyExcel的高效技巧,从而更好地搞定Excel繁琐操作。
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便
使用Easyexcel可以不用考虑性能、内存的等因素的情况下,我们可以快速完成Excel的读、写等功能。
16M内存23秒读取75M(46W行25列)的Excel(3.2.1+版本)
官方网站:https://easyexcel.opensource.alibaba.com/
github地址:https://github.com/alibaba/easyexcel
gitee地址:https://gitee.com/easyexcel/easyexcel
/**
* 最简单的读
* 1. 创建excel对应的实体对象 参照{@link DemoData}
*
2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
*
3. 直接读即可
*/
@Test
public void simpleRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}
读取指定sheet:
//读取指定Sheet
List<YourModelName> list = EasyExcel.read(fileName)
.sheet(sheetNo)
.head(YourModelName.class)
.doReadSync();
读取指定的列:
//读取指定列
List<Object> list = EasyExcel.read(fileName)
.sheet(sheetNo)
.headRow(rowNo)
.readRowFilter(new ReadRowFilter() {
@Override
public boolean doFilter(List<Object> list) {
if (list.get(0).equals("某一列的值")) {
return true;
}
return false;
}
})
.doReadSync();
/**
* 最简单的写
* 1. 创建excel对应的实体对象 参照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
*
2. 直接写即可
*/
@Test
public void simpleWrite() {
String fileName = TestFileUtil.getPath() + "write" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}
向指定sheet写入数据:
//向指定Sheet写入数据
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, YourModelName.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo).build();
excelWriter.write(dataList, writeSheet);
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
向指定行写入数据:
//向指定行写入数据
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, YourModelName.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo).build();
excelWriter.write(dataList, writeSheet, new WriteTable()) .relativeHeadRowIndex(rowNo) .doWrite();
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
/**
* 文件下载(失败了会返回一个有部分数据的Excel)
*
* 1. 创建excel对应的实体对象 参照{@link DownloadData}
*
* 2. 设置返回的 参数
*
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}
/**
* 文件上传
* 1. 创建excel对应的实体对象 参照{@link UploadData}
*
2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
*
3. 直接读即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener(uploadDAO)).sheet().doRead();
return "success";
}
在实际工作中,我们也经常需要对Excel文件进行填充。EasyExcel提供了多种填充Excel的方式,包括对象填充、自定义填充等。下面是一个简单的代码示例:
//对象填充
List<YourModelName> dataList = getDataList();
OutputStream outputStream = new FileOutputStream(fileName);
EasyExcel.write(outputStream, YourModelName.class)
.sheet(sheetNo)
.doWrite(dataList);
outputStream.close();
//自定义填充
List<Object> headList = getHeadList();
List<List<Object>> dataList = getDataList();
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName).build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetNo).build();
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.VERTICAL).build();
excelWriter.fill(headList, fillConfig, writeSheet);
excelWriter.fill(dataList, fillConfig, writeSheet);
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
}
如果需要读取大量数据,可以使用异步读取的方式,提高读取效率。
/异步读取
EasyExcel.read(fileName, YourModelName.class, new ReadListener<YourModelName>() {
// 重写方法
}).sheet().doRead();
@ExcelProperty注解用于标记Java对象中的属性是Excel中的第几列。该注解包含三个属性:index、value和converter。
index属性表示Java对象中的属性在Excel中对应的列的索引,默认值为-1,表示自动匹配。如果该值设置为2,则表示该属性与Excel中第3列对应。
value属性表示Excel中该列的表头名称。如果设置了该值,则Excel中该列的表头名称为value的值;如果未设置,则默认为该属性的名称或字段名称。
converter属性表示数据类型转换器,可将Excel文件中的数据进行类型转换。例如,当Excel中的数据为字符串时可以通过设置converter属性将其转换为指定的数据类型。
@ExcelIgnore注解用于标记Java对象中不需要映射为Excel中的列的属性。使用该注解后,EasyExcel将自动忽略该属性,不对其进行映射。
@ExcelPropertyRange注解用于标记Excel文件中某个列的取值范围。该注解包含两个属性:min和max。如果Excel文件中该列的取值范围不符合要求,则EasyExcel会抛出异常。
@ExcelHeadRowNumber注解用于标记Excel文件中表头所在的行号,默认为0,即第一行。如果设置为1,则表示表头在第二行。使用该注解可以灵活地配置表头所在的位置。
定义个java对象映射excel表格:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DepositRechargeExportElement {
@ExcelProperty(value = "充值订单号", index = 0)
private String accountDepositNo;
@ExcelProperty(value = "账户类型", index = 1)
private String accountGroupType;
@ExcelProperty(value = "充值成功时间", index = 2)
private String completeTime;
@ExcelProperty(value = "充值成功金额(元)", index = 3)
private String depositAmount;
@ExcelProperty(value = "充值技术服务费(元)", index = 4)
private String feeAmount;
}
导出核心逻辑:
@Component
@Slf4j
public class DepositRechargeBillExportHandler extends AbstractBillExportHandler {
@Autowired
private BillDetailService billDetailService;
@Autowired
private BillService billService;
@Override
public BillKind getBillKind() {
return BillKind.DEPOSIT_RECHARGE_KIND;
}
@Override
protected long countAll() {
return 0;
}
@Override
protected void doExport(BillExportTask task, File exportTmpFile) throws IOException {
Bill bill = billService.getBillFromMaster(task.getSellerId(), task.getBillId());
if (bill == null || bill.getBillKind() != getBillKind().getCode()) {
log.error("#DepositRechargeBillExportHandler.doExport# warn params: ={}", ObjectMapperUtils.toJSON(task));
return;
}
Pair<Long, Long> begAndEndTime = parseBegAndEndTime(bill.getMonth());
long begTime = begAndEndTime.getLeft();
long endTime = begAndEndTime.getRight();
Stream<AccountDepositBillRecord> excelData =
billDetailService.listAccountDepositBillRecordByStream(task.getSellerId(), task.getSalesCompanyCode(),
AccountGroupKey.MERCHANT_DEPOSIT.getCode(), endTime, begTime);
List<DepositRechargeExportElement> elements = Lists.newArrayList();
LongAdder progress = new LongAdder();
excelData.forEach(item -> {
elements.add(toExportElement(item));
// 进度报告
deltaProgress(task, progress.longValue());
progress.increment();
});
// 写入表格
ExcelWriter writer = EasyExcel.write(exportTmpFile).excelType(ExcelTypeEnum.XLSX).autoCloseStream(true).build();
WriteSheet sheet =
EasyExcel.writerSheet(getBillKind().getDescription()).head(DepositRechargeExportElement.class).build();
// 保证金商家每个月充值记录少,可以直接保存到excel
writer.write(elements, sheet);
// 关闭资源
writer.finish();
}
private DepositRechargeExportElement toExportElement(AccountDepositBillRecord record) {
DepositRechargeExportElement element = new DepositRechargeExportElement();
element.setAccountDepositNo(record.getAccountDepositNo());
element.setAccountGroupType(AccountGroupKey.valueOf(record.getAccountGroupKey()).getDescription());
element.setCompleteTime(TimeUtils.yyyyMMddHHmmssFormat(record.getCompleteTime()));
element.setDepositAmount(MerchantAmountUtil.calculatePennyToBuck(record.getDepositAmount()));
element.setFeeAmount(MerchantAmountUtil.calculatePennyToBuck(record.getFeeAmount()));
return element;
}
}
excel有最大行数限制,如果要导出的数据特别大,可能百万、千万级别的数据。这个时候我们需要进行分表、分sheet等方式导出。
核心逻辑:
OtherPenaltyBillExportHandler extends AbstractBillExportHandler {
@Autowired
private BillDetailService billDetailService;
@Autowired
private BillService billService;
@Override
public BillKind getBillKind() { return BillKind.OTHER_Penalty_KIND; }
@Override
protected long countAll() { return 0; }
@Override
protected void doExport(BillExportTask task, File exportTmpFile)
throws IOException, ExecutionException, InterruptedException {
Bill bill = billService.getBillFromMaster(task.getSellerId() ,task.getBillId());
if(bill == null || bill.getBillKind() != getBillKind().getCode()){
log.error("#OtherPenaltyBillExportHandler.doExport# warn params: ={}", ObjectMapperUtils.toJSON(task));
return;
}
log.info("#OtherPenaltyBillExportHandler.doExport# info params: task={}", ObjectMapperUtils.toJSON(task));
Pair<Long,Long> begAndEndTime = parseBegAndEndTime(bill.getMonth());
long begTime = begAndEndTime.getLeft();
long endTime = begAndEndTime.getRight();
//生成若干个sheet,每个sheet最大100w行,每个表最多200个sheet,最多存2亿条数据
int sheetNo = 0;
ExcelWriter writer = EasyExcel.write(exportTmpFile).excelType(ExcelTypeEnum.XLSX).autoCloseStream(true).build();
//开始写order_tr
WriteSheet sheet = EasyExcel.writerSheet(sheetNo, getBillKind().getDescription() + "_" + sheetNo)
.head(OtherPenaltyExportElement.class).build();
int page = 1;
List<OtherPenaltyRechargeDTO> responseList =
billDetailService.listOtherPenalties(task.getSellerId(),begTime,endTime,page);
if(CollectionUtils.isEmpty(responseList)) {
//如果为空直接写入个空sheet,否则表内容会有问题
writer.write(Lists.newArrayList(),sheet);
}
long count = 0;
long progress = 0;
while(CollectionUtils.isNotEmpty(responseList)){
List<OtherPenaltyExportElement> elements = toExportElement(responseList);
count = count + CollectionUtils.size(responseList);
progress = progress + CollectionUtils.size(responseList);
deltaProgress(task,progress);
//如果超出表格最大行数,新建sheet
if(count > InvoiceIntegerKConf.invoiceBillExportExcelMaxRowLimit.get()) {
if(sheetNo > InvoiceIntegerKConf.invoiceBillExportExcelMaxSheetLimit.get()) {
throw new RuntimeException("超出表格最大sheet数");
}
count = 0;
sheetNo = sheetNo + 1;
sheet = EasyExcel.writerSheet(sheetNo, getBillKind().getDescription() + "_" + sheetNo)
.head(OtherPenaltyExportElement.class).build();
}
writer.write(elements,sheet);
page = page + 1;
responseList = billDetailService.listOtherPenalties(task.getSellerId(),begTime,endTime,page);
}
writer.finish();
}
private List<OtherPenaltyExportElement> toExportElement(List<OtherPenaltyRechargeDTO> records) {
List<OtherPenaltyExportElement> elements = Lists.newArrayList();
records.stream().forEach(item -> {
OtherPenaltyExportElement element = new OtherPenaltyExportElement();
element.setBusinessOrserId(item.getBusinessOrderId());
element.setAccountType(item.getAccountType());
element.setPenaltyTime(TimeUtils.yyyyMMddHHmmssFormat(item.getPenaltyTime()));
element.setPenaltyAmount(MerchantAmountUtil.calculatePennyToBuck(item.getPenaltyAmount()));
element.setPenaltyReason(item.getPenaltyReason());
elements.add(element);
});
return elements;
}
}
本文简单介绍EasyExcel的使用技巧,以及实际工作中总结的最佳实践。无论是读取Excel文件、写入Excel文件还是填充Excel文件,EasyExcel都提供了非常简单、易用且高效的解决方案。