easyExcel模板填充数据并合并sheet

最近给了一个新的需求,就是选择多个发货记录后导出发货记录及发货详情,要求导出的Excel格式如下
在这里插入图片描述
一个表格表示一条发货记录,里面的详情是不确定条数的。
因为我之前的其他的导出都是使用easyExcel的模板填充做的,所以我想这个能不能也用模板填充实现。
但存在一个问题就是发货记录是不确定个数的,但模板是写死的。
后面看到一些文章,发现一个思路:可以将多条发货记录使用模板生成多个sheet,然后再将多个sheet合并成一个。

一、首先是实现将一个模板填充多个sheet(参考 https://blog.csdn.net/once10/article/details/119616071)

1、实体对象

TrackingdeliveryExcelVO

package com.hx.admin.erp.response;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;

/**
 * <p>
 * 物流发货单查询返回VO
 * </p>
 *
 * @author wxb
 * @since 2021-10-18
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class TrackingdeliveryExcelVO implements Serializable {

    private static final long serialVersionUID = -2334403249810086977L;
    /**
     * id
     */
    @ApiModelProperty("id")
    private Integer id;

    /**
     * 物流公司名称
     */
    @ApiModelProperty("物流公司名称")
    private String logisticsName;

    /**
     * 物流公司联系人
     */
    @ApiModelProperty("物流公司联系人")
    private String logisticsUserName;

    /**
     * 物流公司联系电话
     */
    @ApiModelProperty("物流公司联系电话")
    private String logisticsPhone;

    /**
     * 物流公司所在区域
     */
    @ApiModelProperty("物流公司所在区域")
    private String logisticsArea;

    /**
     * 保价
     */
    @ApiModelProperty("保价")
    private BigDecimal parcel;

    /**
     * 总件数
     */
    @ApiModelProperty("总件数")
    private Integer snum;

    /**
     * 总运费
     */
    @ApiModelProperty("总运费")
    private BigDecimal totel;

    /**
     * 收货人
     */
    @ApiModelProperty("收货人")
    private String consigneeName;

    /**
     * 收货人电话
     */
    @ApiModelProperty("收货人电话")
    private String consigneeMobile;

    /**
     * 收货地址
     */
    @ApiModelProperty("收货地址")
    private String address;

    /**
     * 详情列表
     */
    @ApiModelProperty("详情列表")
    private List<TrackingdeliveryDetailExcelVO> detailList;

}

TrackingdeliveryDetailExcelVO

package com.hx.admin.erp.response;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.math.BigDecimal;

/**
 * 物流发货单详情查询返回VO
 *
 * @author wxb
 * @since 2021-10-18
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class TrackingdeliveryDetailExcelVO implements Serializable {

    private static final long serialVersionUID = 332783842414319355L;

    /**
     * 发货单ID
     */
    @ApiModelProperty("标识")
    private Integer id;

    /**
     * 工厂-仓库名称
     */
    @ApiModelProperty("工厂-仓库名称")
    private String warehouseName;

    /**
     * 仓库地址
     */
    @ApiModelProperty("仓库地址")
    private String warehouseAddress;

    /**
     * 商品名称
     */
    @ApiModelProperty("商品名称")
    private String goodsName;

    /**
     * 规格
     */
    @ApiModelProperty("规格")
    private String specif;

    /**
     * 销售单位
     */
    @ApiModelProperty("单位")
    private String unit;

    /**
     * 数量
     */
    @ApiModelProperty("数量")
    private BigDecimal num;

    /**
     * 客户名称
     */
    @ApiModelProperty("客户名称")
    private String consigneeName;

}

2、导出的模板(记得将sheet删到只剩一个,不然合并sheet时会有问题)

在这里插入图片描述

3、导出工具类

package com.hx.qypfs.excel.utils;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.hx.qypfs.excel.converter.LocalDateTimeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;

@Slf4j
public class ExcelUtils {

    /**
     * 模板填充导出
     * 这里的逻辑是将列表中的数据分表生成多个sheet表,再将多个sheet合并成一个
     *
     * @param list             列表数据
     * @param excelPath        要导出的表格的路径
     * @param excelName        要导出的表格名称
     * @param templateName     templates目录下的模板名称(ex: 模板.xlsx)
     * @param <T>
     */
    public static <T> void downloadExcelTemplate(List<TrackingdeliveryExcelVO> list,
                                                 String excelPath,
                                                 String excelName,
                                                 String templateName) throws Exception {

        Assert.isTrue(StrUtil.isNotEmpty(excelName), "表格名称不能为空");
        Assert.isTrue(StrUtil.isNotEmpty(templateName), "模板名称不能为空");
        Assert.isTrue(templateName.endsWith(".xlsx"), "模板名称后缀名不为.xlsx");

        ExcelWriter excelWriter = null;

        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             // 模板文件 使用这种方式才不会在打包成jar后获取不到resources中的模板资源
             InputStream resourceAsStream = ExcelUtils.class.getResourceAsStream(StrUtil.format("/templates/{}", templateName))) {

            File file = new File(excelPath);
            while (!file.exists()) {
                file.mkdirs();
            }
            int sheetNum = list.size();
            // 原模板只有一个sheet,通过poi复制出需要的sheet个数的模板
            XSSFWorkbook xssWorkbook = new XSSFWorkbook(resourceAsStream);
            // 设置模板的第一个sheet的名称
            xssWorkbook.setSheetName(0, "sheet1");
            for (int i = 1; i < sheetNum; i++) {
                //复制模板,得到第i个sheet
                int num = i + 1;
                xssWorkbook.cloneSheet(0, "sheet" + num);
            }

            //写到流里
            xssWorkbook.write(bos);
            byte[] bArray = bos.toByteArray();
            // 不需要调用close()关闭流,在调用ExcelWriter.finish()会被关闭
            InputStream is = new ByteArrayInputStream(bArray);
            FileOutputStream fileOutputStream = new FileOutputStream(StrUtil.format("{}{}{}", excelPath, File.separator, excelName));

            // 读取模板并进行填充
            excelWriter = EasyExcel.write(fileOutputStream).registerConverter(new LocalDateTimeConverter()).withTemplate(is).build();
            Assert.notNull(excelWriter, "模板文件不存在");
            // 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            // 循环插入每个sheet
            for (int i = 0; i < sheetNum; i++) {
                int num = i + 1;
                WriteSheet writeSheet = EasyExcel.writerSheet("sheet" + num).build();
                excelWriter.fill(list.get(i).getDetailList(), fillConfig, writeSheet);
                excelWriter.fill(list.get(i), writeSheet);
            }
            // 这里必须要写finish,不然只会导出空文件
            excelWriter.finish();
        } finally {
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }
}

这样就基本能将列表数据导出成一个有多个sheet的Excel表格

4、导出效果:

在这里插入图片描述
在这里插入图片描述

二、多个sheet合并成一个(参考https://blog.csdn.net/weixin_44009447/article/details/116708514)

1、合并sheet工具类

package com.hx.qypfs.excel.utils;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFSheet;

/**
 * 来源 https://blog.csdn.net/weixin_44009447/article/details/116708514
 *
 * @Author csdn 博主:Keson Z
 * @Description POI导出excel工具类
 * @Date 17:16 2021/5/11
 * @Param
 * @return
 **/
public class POIUtil {
    /**
     * @return void
     * @Author csdn 博主:Keson Z
     * @Description 拷贝sheet(表)
     * @Date 17:16 2021/5/11
     * @Param [targetSheet, sourceSheet, targetWork, sourceWork, startRow, cellRangeAddressExcelVoList]
     **/
    public static void copySheet(Sheet targetSheet, XSSFSheet sourceSheet,
                                 Workbook targetWork, Workbook sourceWork, int startRow) {
        if (targetSheet == null || sourceSheet == null || targetWork == null || sourceWork == null) {
            throw new IllegalArgumentException("调用PoiUtil.copySheet()方法时,targetSheet、sourceSheet、targetWork、sourceWork都不能为空,故抛出该异常!");
        }

        // 设置单元格默认宽度
        targetSheet.setDefaultColumnWidth(25);
        // 复制源表中的行
        for (int i = sourceSheet.getFirstRowNum(); i <= sourceSheet.getLastRowNum(); i++) {
            Row sourceRow = sourceSheet.getRow(i);
            // 创建新的row
            Row targetRow = targetSheet.createRow(i + startRow);
            if (sourceRow != null) {
                copyRow(targetRow, sourceRow,
                        targetWork, sourceWork);
            }
        }
    }


    /**
     * @return void
     * @Author csdn 博主:Keson Z
     * @Description 拷贝row(行)
     * @Date 17:17 2021/5/11
     * @Param [targetRow, sourceRow, targetWork, sourceWork]
     **/
    public static void copyRow(Row targetRow, Row sourceRow,
                               Workbook targetWork, Workbook sourceWork) {
        if (targetRow == null || sourceRow == null || targetWork == null || sourceWork == null) {
            throw new IllegalArgumentException("调用PoiUtil.copyRow()方法时,targetRow、sourceRow、targetWork、sourceWork、targetPatriarch都不能为空,故抛出该异常!");
        }

        //设置行高
        targetRow.setHeight(sourceRow.getHeight());

        for (int i = sourceRow.getFirstCellNum(); i < sourceRow.getLastCellNum(); i++) {
            Cell sourceCell = sourceRow.getCell(i);
            Cell targetCell = null;
            if (sourceCell != null) {
                if (targetCell == null) {
                    targetCell = targetRow.createCell(i);
                }
                //拷贝单元格,包括内容和样式
                copyCell(targetCell, sourceCell, targetWork, sourceWork);
            }
        }
    }

    /**
     * @return void
     * @Author csdn 博主:Keson Z
     * @Description 拷贝cell(单元格)
     * @Date 17:18 2021/5/11
     * @Param [targetCell, sourceCell, targetWork, sourceWork]
     **/
    public static void copyCell(Cell targetCell, Cell sourceCell, Workbook targetWork, Workbook sourceWork) {
        if (targetCell == null || sourceCell == null || targetWork == null || sourceWork == null) {
            throw new IllegalArgumentException("调用PoiUtil.copyCell()方法时,targetCell、sourceCell、targetWork、sourceWork都不能为空,故抛出该异常!");
        }

        CellStyle targetCellStyle = targetWork.createCellStyle();
        // 拷贝样式
        targetCellStyle.cloneStyleFrom(sourceCell.getCellStyle());
        targetCell.setCellStyle(targetCellStyle);
        try {
            targetCell.setCellValue(sourceCell.getStringCellValue());
        } catch (Exception e) {
            targetCell.setCellValue(sourceCell.getNumericCellValue());
        }
    }
}

2、导出工具类(和上面导出工具类是同一个,只是添加了部分合并sheet的代码)

package com.hx.qypfs.excel.utils;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.hx.qypfs.excel.converter.LocalDateTimeConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.*;

@Slf4j
public class ExcelUtils {

    /**
     * 模板填充导出
     * 这里的逻辑是将列表中的数据分表生成多个sheet表,再将多个sheet合并成一个
     *
     * @param list             列表数据
     * @param excelPath        要导出的表格的路径
     * @param excelName        要导出的表格名称
     * @param templateName     templates目录下的模板名称(ex: 模板.xlsx)
     * @param <T>
     */
    public static <T> void downloadExcelTemplate(List<TrackingdeliveryExcelVO> list,
                                                 String excelPath,
                                                 String excelName,
                                                 String templateName) throws Exception {

        Assert.isTrue(StrUtil.isNotEmpty(excelName), "表格名称不能为空");
        Assert.isTrue(StrUtil.isNotEmpty(templateName), "模板名称不能为空");
        Assert.isTrue(templateName.endsWith(".xlsx"), "模板名称后缀名不为.xlsx");

        ExcelWriter excelWriter = null;

        try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
             // 模板文件 使用这种方式才不会在打包成jar后获取不到resources中的模板资源
             InputStream resourceAsStream = ExcelUtils.class.getResourceAsStream(StrUtil.format("/templates/{}", templateName))) {

            File file = new File(excelPath);
            while (!file.exists()) {
                file.mkdirs();
            }
            int sheetNum = list.size();
            // 原模板只有一个sheet,通过poi复制出需要的sheet个数的模板
            XSSFWorkbook xssWorkbook = new XSSFWorkbook(resourceAsStream);
            // 设置模板的第一个sheet的名称
            xssWorkbook.setSheetName(0, "sheet1");
            for (int i = 1; i < sheetNum; i++) {
                //复制模板,得到第i个sheet
                int num = i + 1;
                xssWorkbook.cloneSheet(0, "sheet" + num);
            }

            //写到流里
            xssWorkbook.write(bos);
            byte[] bArray = bos.toByteArray();
            // 不需要调用close()关闭流,在调用ExcelWriter.finish()会被关闭
            InputStream is = new ByteArrayInputStream(bArray);
            FileOutputStream fileOutputStream = new FileOutputStream(StrUtil.format("{}{}{}", excelPath, File.separator, excelName));
		
            // 读取模板并进行填充
            excelWriter = EasyExcel.write(fileOutputStream).registerConverter(new LocalDateTimeConverter()).withTemplate(is).build();
            Assert.notNull(excelWriter, "模板文件不存在");
            // 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            // 循环插入每个sheet
            for (int i = 0; i < sheetNum; i++) {
                int num = i + 1;
                WriteSheet writeSheet = EasyExcel.writerSheet("sheet" + num).build();
                excelWriter.fill(list.get(i).getDetailList(), fillConfig, writeSheet);
                excelWriter.fill(list.get(i), writeSheet);
            }
			// ======================== 新添加代码 start ==========================
            // 合并多个sheet
            Workbook sourceWorkbook = excelWriter.writeContext().writeWorkbookHolder().getWorkbook();
            Workbook targeWorkbook = new SXSSFWorkbook();
            Workbook workbook1 = ExcelUtils.mergeWorkSheet(targeWorkbook, sourceWorkbook, sheetNum);
            // 合并单元格并设置标题
            mergedCellAndSetTitle(list, workbook1);
            excelWriter.writeContext().writeWorkbookHolder().setWorkbook(workbook1);
            // ======================== 新添加代码 end ==========================

            // 这里必须要写finish,不然只会导出空文件
            excelWriter.finish();
        } finally {
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }
// ======================== 新添加代码 start ==========================
    /**
     * @Description 合并sheet
     * @Date 10:39 2021/5/11
     * @Param [targetWorkbook, sourceWorkbook]
     * @return org.apache.poi.ss.usermodel.Workbook
     **/
    public static Workbook mergeWorkSheet(Workbook targetWorkbook, Workbook sourceWorkbook,int size){
        try{
            //表合并后新表名称
            Sheet targetSheet = targetWorkbook.createSheet("sheet1");
            int secondSourceSheetLen = 0;
            SXSSFWorkbook sw = (SXSSFWorkbook) sourceWorkbook;
            for (int i = 0; i < size; i++) {
                XSSFSheet sheetAt1 = sw.getXSSFWorkbook().getSheetAt(i);
                POIUtil.copySheet(targetSheet, sheetAt1, targetWorkbook, sourceWorkbook,secondSourceSheetLen);
                secondSourceSheetLen += sheetAt1.getPhysicalNumberOfRows();
            }
            return targetWorkbook;
        }catch (Exception e){
            log.error("Workbook合并出错",e);
            return null;
        }
    }
    /**
     * 合并单元格并设置标题
     *
     * @param list
     * @param workbook1
     * @return void
     * @author weixiaobin
     * @date 2021/10/19 9:44
     */
    private static void mergedCellAndSetTitle(List<TrackingdeliveryExcelVO> list, Workbook workbook1) {
        Sheet sheetAt = workbook1.getSheetAt(0);
        //
        CellStyle cellStyle = workbook1.createCellStyle();
        // 居中
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        // 字体
        Font font = workbook1.createFont();
        font.setFontName("宋体");
        font.setBold(true);
        font.setFontHeightInPoints((short) 28);
        cellStyle.setFont(font);
        // 添加标题
        sheetAt.addMergedRegion(new CellRangeAddress(0, 0, 1, 8));
        Cell cell = sheetAt.createRow(0).createCell(1);
        cell.setCellValue("物流发货记录");
        cell.setCellStyle(cellStyle);
        // 修改固定列宽
        sheetAt.setColumnWidth(6,  7 * 256);
        sheetAt.setColumnWidth(7,  14 * 256);

        int initRow = 2;
        for (int i = 0; i < list.size(); i++) {
            // 当前详情列表大小
            int currentSize = list.get(i).getDetailList().size();
            if (0 != i) {
                int nextSize = list.get(i - 1).getDetailList().size();
                initRow = initRow + 8 + nextSize;
            }
            sheetAt.addMergedRegion(new CellRangeAddress(initRow, initRow, 1, 3));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow, initRow, 4, 5));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow, initRow, 6, 7));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow + 1, initRow + 1, 1, 3));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow + 3 + currentSize, initRow + 3 + currentSize, 1, 2));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow + 3 + currentSize, initRow + 3 + currentSize, 6, 7));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow + 4 + currentSize, initRow + 4 + currentSize, 1, 2));
            sheetAt.addMergedRegion(new CellRangeAddress(initRow + 4 + currentSize, initRow + 4 + currentSize, 4, 8));
        }
    }
    // ======================== 新添加代码 end ==========================
}

3、效果

在这里插入图片描述

https://blog.csdn.net/once10/article/details/119616071 (一个模板动态多个sheet填充)
https://blog.csdn.net/weixin_44009447/article/details/116708514(多个sheet合并成一个)