搞定 Excel 繁琐操作:轻松掌握 EasyExcel 的使用技巧

在一些导出业务场景中,经常需要将数据导出存储到excel,而Excel作为一个重要的工具,在数据处理与分析上也起到了至关重要的作用。但是,Excel的操作繁琐、效率低下等问题也制约着我们的工作效率。为了解决这个问题,我们可以使用EasyExcel这个优秀的Java类库,来优化Excel数据的读写,并提高我们的工作效率。

在本文中,我将向大家介绍如何使用EasyExcel的高效技巧,从而更好地搞定Excel繁琐操作。

EasyExcel简介

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+版本)
搞定 Excel 繁琐操作:轻松掌握 EasyExcel 的使用技巧_第1张图片
官方网站:https://easyexcel.opensource.alibaba.com/
github地址:https://github.com/alibaba/easyexcel
gitee地址:https://gitee.com/easyexcel/easyexcel

快速使用

读excel

    /**
     * 最简单的读
     * 

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();

写excel

 /**
     * 最简单的写
     * 

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();
    }
}

Web上传下载

 /**
     * 文件下载(失败了会返回一个有部分数据的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

在实际工作中,我们也经常需要对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();

最佳实践

1. @ExcelProperty注解

@ExcelProperty注解用于标记Java对象中的属性是Excel中的第几列。该注解包含三个属性:index、value和converter。

index属性表示Java对象中的属性在Excel中对应的列的索引,默认值为-1,表示自动匹配。如果该值设置为2,则表示该属性与Excel中第3列对应。

value属性表示Excel中该列的表头名称。如果设置了该值,则Excel中该列的表头名称为value的值;如果未设置,则默认为该属性的名称或字段名称。

converter属性表示数据类型转换器,可将Excel文件中的数据进行类型转换。例如,当Excel中的数据为字符串时可以通过设置converter属性将其转换为指定的数据类型。

2. @ExcelIgnore注解

@ExcelIgnore注解用于标记Java对象中不需要映射为Excel中的列的属性。使用该注解后,EasyExcel将自动忽略该属性,不对其进行映射。

3. @ExcelPropertyRange注解

@ExcelPropertyRange注解用于标记Excel文件中某个列的取值范围。该注解包含两个属性:min和max。如果Excel文件中该列的取值范围不符合要求,则EasyExcel会抛出异常。

4. @ExcelHeadRowNumber注解

@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等方式导出。
核心逻辑:

  1. 设定每个sheet最大行数和每个表的最大sheet数量
  2. 执行写入的时候,判断是否超过对应的限制,如果超过新建
  3. 最后将生成的excel文件压缩存储成一个文件
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都提供了非常简单、易用且高效的解决方案。

你可能感兴趣的:(Java篇,excel,EasyExcel,Java,导出,表格)