POI创建Excel文件

发布于:2025-05-08 ⋅ 阅读:(20) ⋅ 点赞:(0)

1、背景

需求中有需要用户自定义Excel表格表头,然后生成Excel文件,使用EasyExcel更适合生成固定表头的Excel文档,所以此处采用POI原生方式进行开发。文档如下:
在这里插入图片描述

2、创建表格

主要的代码逻辑如下,非主要方法可以在完整代码中找到。

2.1 定义表头对象

根据需求,表头需要制定2级表头,我们先定义一个Excel表头对象。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelModelDto {

    /*** 名称 */
    private String fieldName;
    /*** 提示语 */
    private String comment;
    /*** 类型 */
    private Integer type;
    /*** 背景色 */
    private short backgroundColor;
    /*** 子标题 */
    private List<Child> children;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Child {
        /*** 字段编码 */
        private String fieldCode;
        /*** 字段名称 */
        private String fieldName;
        /*** 提示语 */
        private String comment;
        /*** 类型 */
        private Integer type;
        /*** 下拉框选项 */
        private String[] items;

    }

}

2.2 Excel生成器

创建一个Excel文件生成对象,包含多个属性,其中包括:文件路径、文件名称、是否需要下拉框、文件后缀名、最大文本行数等。

@Slf4j
public class ExcelGenerator {

    private final String localPath;
    private final String sheetName;
    private final String fileName;
    private final String file;
    private final Boolean needItems;
    private final List<ExcelModelDto> data;
    /*** 字段编码集合,从data中解析 */
    private final List<String> fieldCodeList;

    public static final Integer FIRST_ROW = 2;
    public static final Integer LAST_ROW = 65535;
    public static final String FILE_SUFFIX = ".xlsx";
    public static final String PATH_SUFFIX = "/";
    public static final String ITEM_SHEET_NAME = "itemSheet";
    public static final String END_FLAG = "*";
    public static final Integer MAX_CONTENT_ROW_NUMBER = 1002;

    /**
     * 扩展字段sheet页行数记录key值
     */
    public static final String EXTEND_PAGE_ROW_NUMBER_KEY = "extend";

    public ExcelGenerator(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) {
        this(localPath, fileName, sheetName, true, data);
    }

    public ExcelGenerator(String localPath, String fileName, String sheetName, Boolean needItems, List<ExcelModelDto> data) {
        this.localPath = localPath;
        this.fileName = fileName;
        this.sheetName = sheetName;
        this.file = localPath + fileName;
        this.needItems = needItems;
        this.data = data;
        fieldCodeList = this.parseField(data);
    }
}

2.3 创建模板

/**
     * 生成模板
     *
     * @throws IOException 异常
     */
    public void createTemplate() throws IOException {
        this.doCreateSheet(Paths.get(file), sheetName, data);
    }

/**
     * 向Excel文件新增一个新的工作表,并处理表头。
     *
     * @param pathForFile 新工作表将要保存的文件路径。
     * @throws IOException 如果读写文件时发生异常。
     */
    private void doCreateSheet(Path pathForFile, String sheetName, List<ExcelModelDto> data)
            throws IOException {
        long startTime = System.currentTimeMillis();
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = this.getSheetByName(workbook, sheetName, false);
        // 处理Excel表头
        this.dealExcelHeadingCell(workbook, sheet, data);
        // 处理Excel内容单元格,默认都是有二级标题
        this.dealExcelContentCell(workbook, sheet, data);

        // 将inputStream转换为outputStream,并重新写入文件
        try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
            workbook.write(outputStream);
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("创建Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
        }
    }

2.4 处理Excel表头

/**
     * 处理 Excel 表头数据,包括第一行和第二行的标题单元格样式设置、数据填充和合并单元格。
     *
     * @param workbook 工作簿对象
     * @param sheet    主表的工作表对象
     * @param data     表头数据
     */
    private void dealExcelHeadingCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
        // 创建第一行和第二行表头数据,并设置行高
        Row row1 = this.getRow(sheet, 0);
        Row row2 = this.getRow(sheet, 1);
        row1.setHeightInPoints(20);
        row2.setHeightInPoints(20);

        // 已经存在的列号
        int lastCellNum = this.getLastCellNum(sheet, 1);
        int currentCellNum = lastCellNum;
        int startCellNum = lastCellNum;
        int endCellNum;

        for (ExcelModelDto excelModelDto : data) {
            // 一级标题名称
            String firstTitleName = excelModelDto.getFieldName();
            // 一级标题单元格样式
            CellStyle firstTitleCellStyle = this.buildFirstTitleCellStyle(workbook, excelModelDto);
            // 二级标题的单元格样式
            CellStyle secondTitleCellStyle = this.getCellStyle(workbook, IndexedColors.WHITE.getIndex());

            List<ExcelModelDto.Child> children = excelModelDto.getChildren();
            if (children == null || children.size() == 0) {
                continue;
            }

            for (ExcelModelDto.Child child : children) {
                // 处理表头单元格
                this.dealTitleCell(workbook, sheet, child, firstTitleName, firstTitleCellStyle, secondTitleCellStyle, currentCellNum);
                // 处理完后列号加一
                currentCellNum++;
            }
            endCellNum = currentCellNum - 1;

            // POI 版本升级后,合并单元格需要大于一个单元格
            if (startCellNum != endCellNum) {
                CellRangeAddress region = new CellRangeAddress(0, 0, startCellNum, endCellNum);
                sheet.addMergedRegion(region);
            }
            startCellNum = endCellNum + 1;
        }
    }

2.5 处理Excel内容单元格样式

/**
     * 格式化内容单元格。
     *
     * @param sheet    工作表对象。
     * @param workbook 工作簿对象。
     */
    private void dealExcelContentCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
        // 获取统一的单元格样式,不用每个单元格获取一个对象,防止对象过多
        CellStyle childCellStyle = this.getContentCellStyle(workbook);
        // 只格式化内容单元格,且有上限
        int maxContentRowNumber = MAX_CONTENT_ROW_NUMBER;
        // 跳过表头,从文本行开始
        for (int rowNumber = 2; rowNumber < maxContentRowNumber; rowNumber++) {
            Row row = sheet.createRow(rowNumber);
            // 列号从0开始
            int cellNumber = 0;
            for (ExcelModelDto excelModelDto : data) {
                List<ExcelModelDto.Child> children = excelModelDto.getChildren();
                for (ExcelModelDto.Child child : children) {
                    String[] items = child.getItems();
                    if (Objects.isNull(items) || items.length == 0) {
                        Cell cell = row.createCell(cellNumber);
                        cell.setCellStyle(childCellStyle);
                    }
                    // 每处理完一个单元格,列号加1
                    cellNumber++;
                }
            }
        }
    }

2.6 处理单个表头

在处理表头过程中,如果items 不为空,则说明此列需要下拉框,数组为供用户选择的下拉内容,防止下拉框内容过大,所以将下拉内容单独生成到一个隐藏的sheet页中,并且使用表达式来表达下拉框内容,设定到单元格中。

/**
     * 处理Excel表格的标题单元格。
     *
     * @param workbook             工作簿对象
     * @param sheet                工作表对象
     * @param child                ExcelModelDto.Child 对象,包含字段名、注释和下拉框选项等信息
     * @param firstTitleName       一级标题名称
     * @param firstTitleCellStyle  一级标题单元格样式
     * @param secondTitleCellStyle 二级标题单元格样式
     * @param index                当前处理的列索引
     */
    private void dealTitleCell(Workbook workbook, Sheet sheet,
                               ExcelModelDto.Child child, String firstTitleName,
                               CellStyle firstTitleCellStyle, CellStyle secondTitleCellStyle,
                               int index) {
        Row row1 = this.getRow(sheet, 0);
        Row row2 = this.getRow(sheet, 1);

        String secondFieldName = child.getFieldName();
        String comment = child.getComment();
        String[] items = child.getItems();

        // 一级表头
        Cell cell1 = row1.createCell(index);
        cell1.setCellValue(firstTitleName);
        cell1.setCellStyle(firstTitleCellStyle);

        // 二级表头,标题如果以* 号结尾,则* 置为红色
        Cell cell2 = row2.createCell(index);
        RichTextString textString = this.parseCellValue(workbook, Font.COLOR_NORMAL, true, secondFieldName);
        cell2.setCellValue(textString);
        cell2.setCellStyle(secondTitleCellStyle);

        // 设置下拉框
        if (items != null && items.length > 0 && needItems) {
            this.appendItems(workbook, sheet, secondTitleCellStyle, secondFieldName, items, index);
        }
        // 设置表头备注
        if (!org.apache.commons.lang.StringUtils.isEmpty(comment)) {
            this.setComment(sheet, cell2, comment);
        }
        // 根据字段长度自动调整列的宽度
        sheet.setColumnWidth(index, 100 * 50);
    }

/**
     * 在指定的工作簿和工作表中追加枚举类型的项,并设置公式引用。
     *
     * @param workbook        工作簿对象
     * @param sheet           工作表对象
     * @param childCellStyle  子单元格样式
     * @param secondTitleName 第二级标题名称
     * @param items           枚举类型的项数组
     * @param index           当前项在总体中的索引位置
     */
    private void appendItems(Workbook workbook, Sheet sheet, CellStyle childCellStyle, String secondTitleName, String[] items, int index) {
        // 如果有序列单元格,则创建一个sheet页,来保存所有的枚举类型,同时隐藏该sheet页
        Sheet itemsSheet = this.getSheetByName(workbook, ITEM_SHEET_NAME, true);
        // 追加sheet的时候,需要看隐藏sheet的列已经到哪一列了,避免追加时将原有隐藏列覆盖掉
        int existItemCell = this.getLastCellNum(itemsSheet, 0);
        // 将枚举数组写入到独立的sheet页中,同时设置表头格式
        String formula = this.writeItems(itemsSheet, childCellStyle, secondTitleName, existItemCell, items);
        // 设置公式到模板的sheet页中,格式化后的最终公式为
        // =itemSheet!$B$1:$B$88
        // 表明该单元格引用的是 itemSheet sheet页中 B1~B88的数据
        formula = String.format("=%s!%s", ITEM_SHEET_NAME, formula);
        this.setItems(sheet, formula, FIRST_ROW, LAST_ROW, index, index);
    }

3、追加sheet

有些需要在已有的Excel文档中追加新的sheet表格内容,效果如下:

在这里插入图片描述

/**
     * 在指定的 Excel 文件中添加一个新的工作表,并填充数据。
     *
     * @param sheetName 新工作表的名称
     * @param data      要填充的数据列表
     * @throws IOException 如果在操作文件时发生了 I/O 错误
     */
    public void appendSheet(String sheetName, List<ExcelModelDto> data) throws IOException {
        long startTime = System.currentTimeMillis();

        // 路径不存在则创建,保证路径是存在的
        Path pathForLocalPath = Paths.get(localPath);
        boolean existPath = Files.exists(pathForLocalPath);
        if (!existPath) {
            Files.createDirectories(pathForLocalPath);
        }

        // 如果文件不存在,则走创建sheet逻辑
        Path pathForFile = Paths.get(file);
        if (!Files.exists(pathForFile)) {
            this.doCreateSheet(pathForFile, sheetName, data);
            return;
        }

        // 如果文件存在则走追加sheet逻辑
        try (InputStream inputStream = Files.newInputStream(pathForFile)) {
            this.doAppendSheet(inputStream, pathForFile, sheetName, data);
            long endTime = System.currentTimeMillis();
            log.info("追加Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
        } catch (Exception e) {
            log.error("追加Excel模板文件失败!", e);
            throw new BizException(e);
        }
    }

/**
     * 向Excel文件追加一个新的工作表,并处理表头。
     *
     * @param inputStream Excel文件的输入流。
     * @param pathForFile 新工作表将要保存的文件路径。
     * @throws IOException 如果读写文件时发生异常。
     */
    private void doAppendSheet(InputStream inputStream, Path pathForFile, String sheetName, List<ExcelModelDto> data)
            throws IOException {
        Workbook workbook = new XSSFWorkbook(inputStream);
        Sheet sheet = this.getSheetByName(workbook, sheetName, false);
        // 处理Excel表头
        this.dealExcelHeadingCell(workbook, sheet, data);
        // 处理Excel内容单元格,默认都是有二级标题
        this.dealExcelContentCell(workbook, sheet, data);

        // 将inputStream转换为outputStream,并重新写入文件
        try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
            IOUtils.copy(inputStream, outputStream);
            workbook.write(outputStream);
        }
    }

4、静态工具

每次使用都需要new一个对象来创建Excel文件,所以创建一个静态工具类,来通过静态方法实现文档的创建与追加。

public class ExcelGeneratorExecutors {

    /**
     * 创建 Excel 模板文件。
     *
     * @param localPath 本地路径
     * @param fileName  文件名
     * @param sheetName 工作表名称
     * @param data      数据列表
     * @throws IOException 如果创建模板文件失败
     */
    public static void createTemplate(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) throws IOException {
        ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
        excelGenerator.createTemplate();
    }

    /**
     * 在指定路径的Excel文件中追加一个新的工作表,并填充数据。
     *
     * @param localPath Excel文件的本地路径。
     * @param fileName  Excel文件的名称。
     * @param sheetName 新增工作表的名称。
     * @param data      填充到新增工作表的数据。
     * @throws IOException 如果在追加工作表或填充数据时发生I/O错误。
     */
    public static void appendSheet(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) throws IOException {
        ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
        excelGenerator.appendSheet(sheetName, data);
    }

}

5、单元测试

@Test
    public void testGenerate() {
        String localPath = "D:\\mytmp\\template\\";
        String dateTime = DateUtils.format(new Date(), DateUtils.DATE_FORMAT_COMMENT_2);
        String fileName = String.format("生成模板-%s.xlsx", dateTime);
        String sheetName = "测试";
        List<ExcelModelDto> data = this.buildExcelModelDtoList();
        ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
        try {
            excelGenerator.createTemplate();

            List<ExcelModelDto> data2 = this.buildExcelModelDtoList2();
            excelGenerator.appendSheet("自定义sheet", data);
            excelGenerator.appendSheet("自定义sheet2", data2);
            excelGenerator.appendSheet("自定义sheet3", data2);
            log.info("模板文件生成,名称为:{}", fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testGenerate2() {
        String localPath = "D:\\mytmp\\template\\";
        String dateTime = DateUtils.format(new Date(), DateUtils.DATE_FORMAT_COMMENT_2);
        String fileName = String.format("生成模板-%s.xlsx", dateTime);
        String sheetName = "测试";
        List<ExcelModelDto> data = this.buildExcelModelDtoList();
        try {
            ExcelGeneratorExecutors.createTemplate(localPath, fileName, sheetName, data);
            ExcelGeneratorExecutors.appendSheet(localPath, fileName, sheetName, data);
            ExcelGeneratorExecutors.appendSheet(localPath, fileName, "自定义sheet3", data);
            log.info("模板文件生成,名称为:{}", fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

public List<ExcelModelDto> buildExcelModelDtoList() {
        List<ExcelModelDto> data = new ArrayList<>();
        ExcelModelDto excelModelDto = new ExcelModelDto();
        excelModelDto.setFieldName("电器");
        excelModelDto.setComment("song");
        excelModelDto.setType(2);
        excelModelDto.setBackgroundColor((short) 2);

        List<ExcelModelDto.Child> children = new ArrayList<>();
        ExcelModelDto.Child child1 = new ExcelModelDto.Child();
        child1.setComment("类目1");
        child1.setFieldCode("category");
        child1.setFieldName("类目1");
        List<String> list1 = Lists.newArrayList("冰箱", "洗衣机", "空调");
        child1.setItems(list1.toArray(new String[0]));

        ExcelModelDto.Child child2 = new ExcelModelDto.Child();
        child2.setComment("数量1");
        child2.setFieldCode("qty");
        child2.setFieldName("数量1");
        List<String> list2 = Lists.newArrayList("1", "2", "3");
        child2.setItems(list2.toArray(new String[0]));

        ExcelModelDto.Child child3 = new ExcelModelDto.Child();
        child3.setComment("文本内容");
        child3.setFieldCode("textValue");
        child3.setFieldName("文本内容");

        children.add(child1);
        children.add(child2);
        children.add(child3);
        excelModelDto.setChildren(children);
        data.add(excelModelDto);
        return data;
    }

    public List<ExcelModelDto> buildExcelModelDtoList2() {
        List<ExcelModelDto> data = new ArrayList<>();

        ExcelModelDto excelModelDto0 = new ExcelModelDto();
        excelModelDto0.setFieldName("商家运单号");
        excelModelDto0.setComment("商家运单号");
        excelModelDto0.setType((int) IndexedColors.TURQUOISE1.getIndex());
        excelModelDto0.setBackgroundColor(IndexedColors.TURQUOISE1.getIndex());

        ExcelModelDto.Child child0 = new ExcelModelDto.Child();
        child0.setComment("关联第一个sheet页的商家运单号");
        child0.setFieldCode("orderNo");
        child0.setFieldName("商家运单号*");

        List<ExcelModelDto.Child> children0 = new ArrayList<>();
        children0.add(child0);
        excelModelDto0.setChildren(children0);

        ExcelModelDto excelModelDto = new ExcelModelDto();
        excelModelDto.setFieldName("购买电器");
        excelModelDto.setComment("song");
        excelModelDto.setType((int) IndexedColors.TURQUOISE1.getIndex());
        excelModelDto.setBackgroundColor(IndexedColors.TURQUOISE1.getIndex());


        ExcelModelDto.Child child1 = new ExcelModelDto.Child();
        child1.setComment("类目");
        child1.setFieldCode("category");
        child1.setFieldName("类目");
        List<String> list1 = Lists.newArrayList("冰箱", "洗衣机", "空调");
        child1.setItems(list1.toArray(new String[0]));

        ExcelModelDto.Child child2 = new ExcelModelDto.Child();
        child2.setComment("数量");
        child2.setFieldCode("qty");
        child2.setFieldName("数量");
        //List<String> list2 = Lists.newArrayList("1", "2", "3");
        //child2.setItems(list2.toArray(new String[0]));

        List<ExcelModelDto.Child> children = new ArrayList<>();
        children.add(child1);
        children.add(child2);
        excelModelDto.setChildren(children);

        data.add(excelModelDto0);
        data.add(excelModelDto);
        return data;
    }

6、完整代码示例

@Slf4j
public class ExcelGenerator {

    private final String localPath;
    private final String sheetName;
    private final String fileName;
    private final String file;
    private final Boolean needItems;
    private final List<ExcelModelDto> data;
    /*** 字段编码集合,从data中解析 */
    private final List<String> fieldCodeList;

    public static final Integer FIRST_ROW = 2;
    public static final Integer LAST_ROW = 65535;
    public static final String FILE_SUFFIX = ".xlsx";
    public static final String PATH_SUFFIX = "/";
    public static final String ITEM_SHEET_NAME = "itemSheet";
    public static final String END_FLAG = "*";
    public static final Integer MAX_CONTENT_ROW_NUMBER = 1002;

    /**
     * 扩展字段sheet页行数记录key值
     */
    public static final String EXTEND_PAGE_ROW_NUMBER_KEY = "extend";

    public ExcelGenerator(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) {
        this(localPath, fileName, sheetName, true, data);
    }

    public ExcelGenerator(String localPath, String fileName, String sheetName, Boolean needItems, List<ExcelModelDto> data) {
        this.localPath = localPath;
        this.fileName = fileName;
        this.sheetName = sheetName;
        this.file = localPath + fileName;
        this.needItems = needItems;
        this.data = data;
        fieldCodeList = this.parseField(data);
    }

    /**
     * 创建对象时,将ExcelModel中的字段按顺序排好,保存到List中
     *
     * @param data 入参
     * @return 返回值
     */
    public List<String> parseField(List<ExcelModelDto> data) {
        List<String> fieldCodeList = new ArrayList<>();
        for (ExcelModelDto modelDto : data) {
            List<ExcelModelDto.Child> children = modelDto.getChildren();
            for (ExcelModelDto.Child child : children) {
                String fieldCode = child.getFieldCode();
                fieldCodeList.add(fieldCode);
            }
        }
        return fieldCodeList;
    }

    /**
     * 生成模板
     *
     * @throws IOException 异常
     */
    public void createTemplate() throws IOException {
        this.doCreateSheet(Paths.get(file), sheetName, data);
    }

    /**
     * 在指定的 Excel 文件中添加一个新的工作表,并填充数据。
     *
     * @param sheetName 新工作表的名称
     * @param data      要填充的数据列表
     * @throws IOException 如果在操作文件时发生了 I/O 错误
     */
    public void appendSheet(String sheetName, List<ExcelModelDto> data) throws IOException {
        long startTime = System.currentTimeMillis();

        // 路径不存在则创建,保证路径是存在的
        Path pathForLocalPath = Paths.get(localPath);
        boolean existPath = Files.exists(pathForLocalPath);
        if (!existPath) {
            Files.createDirectories(pathForLocalPath);
        }

        // 如果文件不存在,则走创建sheet逻辑
        Path pathForFile = Paths.get(file);
        if (!Files.exists(pathForFile)) {
            this.doCreateSheet(pathForFile, sheetName, data);
            return;
        }

        // 如果文件存在则走追加sheet逻辑
        try (InputStream inputStream = Files.newInputStream(pathForFile)) {
            this.doAppendSheet(inputStream, pathForFile, sheetName, data);
            long endTime = System.currentTimeMillis();
            log.info("追加Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
        } catch (Exception e) {
            log.error("追加Excel模板文件失败!", e);
            throw new BizException(e);
        }
    }

    /**
     * 向Excel文件新增一个新的工作表,并处理表头。
     *
     * @param pathForFile 新工作表将要保存的文件路径。
     * @throws IOException 如果读写文件时发生异常。
     */
    private void doCreateSheet(Path pathForFile, String sheetName, List<ExcelModelDto> data)
            throws IOException {
        long startTime = System.currentTimeMillis();
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = this.getSheetByName(workbook, sheetName, false);
        // 处理Excel表头
        this.dealExcelHeadingCell(workbook, sheet, data);
        // 处理Excel内容单元格,默认都是有二级标题
        this.dealExcelContentCell(workbook, sheet, data);

        // 将inputStream转换为outputStream,并重新写入文件
        try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
            workbook.write(outputStream);
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("创建Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
        }
    }

    /**
     * 向Excel文件追加一个新的工作表,并处理表头。
     *
     * @param inputStream Excel文件的输入流。
     * @param pathForFile 新工作表将要保存的文件路径。
     * @throws IOException 如果读写文件时发生异常。
     */
    private void doAppendSheet(InputStream inputStream, Path pathForFile, String sheetName, List<ExcelModelDto> data)
            throws IOException {
        Workbook workbook = new XSSFWorkbook(inputStream);
        Sheet sheet = this.getSheetByName(workbook, sheetName, false);
        // 处理Excel表头
        this.dealExcelHeadingCell(workbook, sheet, data);
        // 处理Excel内容单元格,默认都是有二级标题
        this.dealExcelContentCell(workbook, sheet, data);

        // 将inputStream转换为outputStream,并重新写入文件
        try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
            IOUtils.copy(inputStream, outputStream);
            workbook.write(outputStream);
        }
    }

    /**
     * 处理 Excel 表头数据,包括第一行和第二行的标题单元格样式设置、数据填充和合并单元格。
     *
     * @param workbook 工作簿对象
     * @param sheet    主表的工作表对象
     * @param data     表头数据
     */
    private void dealExcelHeadingCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
        // 创建第一行和第二行表头数据,并设置行高
        Row row1 = this.getRow(sheet, 0);
        Row row2 = this.getRow(sheet, 1);
        row1.setHeightInPoints(20);
        row2.setHeightInPoints(20);

        // 已经存在的列号
        int lastCellNum = this.getLastCellNum(sheet, 1);
        int currentCellNum = lastCellNum;
        int startCellNum = lastCellNum;
        int endCellNum;

        for (ExcelModelDto excelModelDto : data) {
            // 一级标题名称
            String firstTitleName = excelModelDto.getFieldName();
            // 一级标题单元格样式
            CellStyle firstTitleCellStyle = this.buildFirstTitleCellStyle(workbook, excelModelDto);
            // 二级标题的单元格样式
            CellStyle secondTitleCellStyle = this.getCellStyle(workbook, IndexedColors.WHITE.getIndex());

            List<ExcelModelDto.Child> children = excelModelDto.getChildren();
            if (children == null || children.size() == 0) {
                continue;
            }

            for (ExcelModelDto.Child child : children) {
                // 处理表头单元格
                this.dealTitleCell(workbook, sheet, child, firstTitleName, firstTitleCellStyle, secondTitleCellStyle, currentCellNum);
                // 处理完后列号加一
                currentCellNum++;
            }
            endCellNum = currentCellNum - 1;

            // POI 版本升级后,合并单元格需要大于一个单元格
            if (startCellNum != endCellNum) {
                CellRangeAddress region = new CellRangeAddress(0, 0, startCellNum, endCellNum);
                sheet.addMergedRegion(region);
            }
            startCellNum = endCellNum + 1;
        }
    }

    /**
     * 格式化内容单元格。
     *
     * @param sheet    工作表对象。
     * @param workbook 工作簿对象。
     */
    private void dealExcelContentCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
        // 获取统一的单元格样式,不用每个单元格获取一个对象,防止对象过多
        CellStyle childCellStyle = this.getContentCellStyle(workbook);
        // 只格式化内容单元格,且有上限
        int maxContentRowNumber = MAX_CONTENT_ROW_NUMBER;
        // 跳过表头,从文本行开始
        for (int rowNumber = 2; rowNumber < maxContentRowNumber; rowNumber++) {
            Row row = sheet.createRow(rowNumber);
            // 列号从0开始
            int cellNumber = 0;
            for (ExcelModelDto excelModelDto : data) {
                List<ExcelModelDto.Child> children = excelModelDto.getChildren();
                for (ExcelModelDto.Child child : children) {
                    String[] items = child.getItems();
                    if (Objects.isNull(items) || items.length == 0) {
                        Cell cell = row.createCell(cellNumber);
                        cell.setCellStyle(childCellStyle);
                    }
                    // 每处理完一个单元格,列号加1
                    cellNumber++;
                }
            }
        }
    }

    /**
     * 处理Excel表格的标题单元格。
     *
     * @param workbook             工作簿对象
     * @param sheet                工作表对象
     * @param child                ExcelModelDto.Child 对象,包含字段名、注释和下拉框选项等信息
     * @param firstTitleName       一级标题名称
     * @param firstTitleCellStyle  一级标题单元格样式
     * @param secondTitleCellStyle 二级标题单元格样式
     * @param index                当前处理的列索引
     */
    private void dealTitleCell(Workbook workbook, Sheet sheet,
                               ExcelModelDto.Child child, String firstTitleName,
                               CellStyle firstTitleCellStyle, CellStyle secondTitleCellStyle,
                               int index) {
        Row row1 = this.getRow(sheet, 0);
        Row row2 = this.getRow(sheet, 1);

        String secondFieldName = child.getFieldName();
        String comment = child.getComment();
        String[] items = child.getItems();

        // 一级表头
        Cell cell1 = row1.createCell(index);
        cell1.setCellValue(firstTitleName);
        cell1.setCellStyle(firstTitleCellStyle);

        // 二级表头,标题如果以* 号结尾,则* 置为红色
        Cell cell2 = row2.createCell(index);
        RichTextString textString = this.parseCellValue(workbook, Font.COLOR_NORMAL, true, secondFieldName);
        cell2.setCellValue(textString);
        cell2.setCellStyle(secondTitleCellStyle);

        // 设置下拉框
        if (items != null && items.length > 0 && needItems) {
            this.appendItems(workbook, sheet, secondTitleCellStyle, secondFieldName, items, index);
        }
        // 设置表头备注
        if (!org.apache.commons.lang.StringUtils.isEmpty(comment)) {
            this.setComment(sheet, cell2, comment);
        }
        // 根据字段长度自动调整列的宽度
        sheet.setColumnWidth(index, 100 * 50);
    }

    /**
     * 设置单元格下拉框
     * 下拉框引用单独一个sheet页中的数据
     *
     * @param sheet    sheet
     * @param formula  公式
     * @param firstRow 起始行
     * @param lastRow  结束行
     * @param firstCol 起始列
     * @param lastCol  结束列
     */
    public void setItems(Sheet sheet, String formula, int firstRow, int lastRow, int firstCol, int lastCol) {
        CellRangeAddressList addressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
        DataValidationHelper helper = sheet.getDataValidationHelper();
        DataValidationConstraint constraint = helper.createFormulaListConstraint(formula);
        DataValidation validation = helper.createValidation(constraint, addressList);
        validation.setShowErrorBox(true);
        sheet.addValidationData(validation);
    }

    /**
     * 设置单元格备注信息
     *
     * @param sheet      sheet
     * @param cell       单元格
     * @param textString 提示信息
     */
    public void setComment(Sheet sheet, Cell cell, String textString) {
        Drawing<?> drawing = sheet.createDrawingPatriarch();
        CreationHelper factory = sheet.getWorkbook().getCreationHelper();

        // 设置提示框大小,默认根据 提示信息的大小来确认提示框高度
        /// ClientAnchor anchor = factory.createClientAnchor();
        textString = StringUtils.defaultIfBlank(textString, "");
        int length = textString.length();
        int row2 = length / 25 + 6;
        // (int dx1, int dy1, int dx2, int dy2, short col1, int row1, short col2, int row2)
        // 前四个参数是坐标点,后四个参数是编辑和显示批注时的大小.
        ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) 4, 2, (short) 6, row2);
        Comment comment = drawing.createCellComment(anchor);
        RichTextString str = factory.createRichTextString(textString);
        comment.setString(str);
        comment.setAuthor("Auto+");
        // 以上参数不设置时会有默认值,当一个被重复设置批注时会报错
        // Multiple cell comments in one cell are not allowed
        // 故在设置批注前检查锚点位置有无批注,有的话移除
        if (cell.getCellComment() != null) {
            cell.removeCellComment();
        }
        cell.setCellComment(comment);
    }

    /**
     * 获取单元格样式对象
     *
     * @param workbook        工作簿
     * @param backGroundColor 背景色
     * @return 返回样式对象
     */
    public CellStyle getCellStyle(Workbook workbook, short backGroundColor) {
        CellStyle cellStyle = workbook.createCellStyle();
        CreationHelper createHelper = workbook.getCreationHelper();
        cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));

        // IndexedColors.YELLOW.getIndex()
        cellStyle.setFillForegroundColor(backGroundColor);
        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 水平居中
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        // 垂直居中
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);

        // 设置边框及颜色
        cellStyle.setBorderTop(BorderStyle.DOUBLE);
        cellStyle.setBorderBottom(BorderStyle.DOUBLE);
        cellStyle.setBorderLeft(BorderStyle.DOUBLE);
        cellStyle.setBorderRight(BorderStyle.DOUBLE);
        cellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());
        cellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
        cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex());
        cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());

        return cellStyle;
    }

    /**
     * 向sheet中写入 序列内容
     *
     * @param sheet     sheet
     * @param cellStyle 单元格格式,表头格式
     * @param itemsType 序列类型
     * @param col       列号
     * @param items     序列数组
     * @return 返回坐标
     */
    protected String writeItems(Sheet sheet, CellStyle cellStyle, String itemsType, int col, String[] items) {
        // 第一行为表头数据
        Row row = sheet.getRow(0);
        if (row == null) {
            row = sheet.createRow(0);
        }
        Cell cell = row.createCell(col);
        // 获取单元格列所对应的字母,即 0=A,1=B ...
        String columnLetter = CellReference.convertNumToColString(col);
        cell.setCellValue(itemsType);
        cell.setCellStyle(cellStyle);
        int length = items.length;
        for (int i = 0; i < length; i++) {
            Row itemRow = sheet.getRow(i + 1);
            if (itemRow == null) {
                itemRow = sheet.createRow(i + 1);
            }
            Cell itemRowCell = itemRow.createCell(col);
            itemRowCell.setCellValue(items[i]);
        }
        // 格式化后的公式坐标为  $B$1:$B$88
        return String.format("$%s$%s:$%s$%s", columnLetter, 2, columnLetter, items.length + 1);
    }

    /**
     * 格式化单元格字体样式
     *
     * @param workbook  工作簿
     * @param fontColor 字体颜色
     * @param isBold    是否加粗
     * @param value     单元格值
     */
    public RichTextString parseCellValue(Workbook workbook, short fontColor, boolean isBold, String value) {
        value = StringUtils.defaultIfBlank(value, "");
        XSSFRichTextString textString = new XSSFRichTextString(value);
        Font font1 = getFontStyle(workbook, fontColor, isBold);
        if (StringUtils.isNotBlank(value)) {
            int length = value.length();
            // 如果内容是以 * 号结尾的,则将 * 号置为红色,默认黑色
            if (value.endsWith(END_FLAG)) {
                int point = length - 1;
                textString.applyFont(0, point, font1);
                Font font2 = getFontStyle(workbook, Font.COLOR_RED, isBold);
                textString.applyFont(point, length, font2);
            } else {
                textString.applyFont(0, length, font1);
            }
        }
        return textString;
    }

    /**
     * 获取字体样式
     *
     * @param workbook  工作簿
     * @param fontColor 字体颜色
     * @param isBold    是否加粗
     * @return 返回值
     */
    public Font getFontStyle(Workbook workbook, short fontColor, boolean isBold) {
        Font font = workbook.createFont();
        font.setColor(fontColor);
        if (isBold) {
            font.setBold(true);
        }
        font.setFontName("宋体");
        // 字体大小
        font.setFontHeightInPoints((short) 10);
        return font;
    }

    /**
     * 获取指定行在给定工作表中的最后一个单元格的索引。
     *
     * @param sheet  工作表对象
     * @param rowNum 行号(从0开始计数)
     * @return 最后一个单元格的索引,若行不存在则返回0
     */
    private int getLastCellNum(Sheet sheet, int rowNum) {
        int existCell = 0;
        // 指定sheet页不为空,则获取已经有多少列
        Row row = sheet.getRow(rowNum);
        if (Objects.nonNull(row)) {
            existCell = row.getLastCellNum();
            // 如果不存在返回的是-1,业务上从0开始计算
            if (existCell < 0) {
                existCell = 0;
            }
        }
        return existCell;
    }

    /**
     * 获取或创建指定名称的工作表并将其隐藏。
     *
     * @param workbook 工作簿对象
     * @return 指定名称的工作表对象
     */
    private Sheet getSheetByName(Workbook workbook, String sheetName, boolean hide) {
        Sheet itemsSheet = workbook.getSheet(sheetName);
        // 指定sheet页为空则创建
        if (Objects.isNull(itemsSheet)) {
            itemsSheet = workbook.createSheet(sheetName);
            int sheetIndex = workbook.getSheetIndex(sheetName);
            workbook.setSheetHidden(sheetIndex, hide);
        }
        return itemsSheet;
    }

    /**
     * 根据行号获取或创建指定Sheet中的Row对象。
     *
     * @param sheet  要操作的Sheet对象。
     * @param rowNum 需要获取或创建的行号。
     * @return 指定行号的Row对象。
     */
    private Row getRow(Sheet sheet, int rowNum) {
        Row row = sheet.getRow(rowNum);
        if (Objects.isNull(row)) {
            row = sheet.createRow(rowNum);
        }
        return row;
    }

    /**
     * 构建第一行标题单元格样式。
     *
     * @param workbook      工作簿对象。
     * @param excelModelDto Excel模型数据传输对象。
     * @return 第一行标题单元格样式。
     */
    private CellStyle buildFirstTitleCellStyle(Workbook workbook, ExcelModelDto excelModelDto) {
        // 根据字段类型来获取背景色
        short backGroundColor = excelModelDto.getBackgroundColor();
        CellStyle cellStyle = this.getCellStyle(workbook, backGroundColor);
        Font font = this.getFontStyle(workbook, Font.COLOR_NORMAL, true);
        cellStyle.setFont(font);
        return cellStyle;
    }

    /**
     * 在指定的工作簿和工作表中追加枚举类型的项,并设置公式引用。
     *
     * @param workbook        工作簿对象
     * @param sheet           工作表对象
     * @param childCellStyle  子单元格样式
     * @param secondTitleName 第二级标题名称
     * @param items           枚举类型的项数组
     * @param index           当前项在总体中的索引位置
     */
    private void appendItems(Workbook workbook, Sheet sheet, CellStyle childCellStyle, String secondTitleName, String[] items, int index) {
        // 如果有序列单元格,则创建一个sheet页,来保存所有的枚举类型,同时隐藏该sheet页
        Sheet itemsSheet = this.getSheetByName(workbook, ITEM_SHEET_NAME, true);
        // 追加sheet的时候,需要看隐藏sheet的列已经到哪一列了,避免追加时将原有隐藏列覆盖掉
        int existItemCell = this.getLastCellNum(itemsSheet, 0);
        // 将枚举数组写入到独立的sheet页中,同时设置表头格式
        String formula = this.writeItems(itemsSheet, childCellStyle, secondTitleName, existItemCell, items);
        // 设置公式到模板的sheet页中,格式化后的最终公式为
        // =itemSheet!$B$1:$B$88
        // 表明该单元格引用的是 itemSheet sheet页中 B1~B88的数据
        formula = String.format("=%s!%s", ITEM_SHEET_NAME, formula);
        this.setItems(sheet, formula, FIRST_ROW, LAST_ROW, index, index);
    }

    /**
     * 获取单元格样式对象
     *
     * @param workbook 工作簿
     * @return 返回样式对象
     */
    public CellStyle getContentCellStyle(Workbook workbook) {
        CellStyle cellStyle = workbook.createCellStyle();
        CreationHelper createHelper = workbook.getCreationHelper();
        cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));

        // 背景色为纯色
        cellStyle.setFillPattern(FillPatternType.NO_FILL);

        // 设置单元格格式为文本格式
        DataFormat format = workbook.createDataFormat();
        cellStyle.setDataFormat(format.getFormat("@"));
        return cellStyle;
    }

}

网站公告

今日签到

点亮在社区的每一天
去签到