侧边栏壁纸
博主头像
昂洋编程 博主等级

鸟随鸾凤飞腾远,人伴贤良品自高

  • 累计撰写 72 篇文章
  • 累计创建 79 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

基于Apache POI手写Excel模板导出工具

Administrator
2023-11-15 / 0 评论 / 0 点赞 / 63 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2024-06-14,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Apache POI官网
Github链接

为何要写这个工具类

  1. EasyExcel 或者 EasyPOI这两种开源工具类也支持Excel模板的导出,并且功能和处理大数据量导出时有明显优势,但是这两个工具类对多个list占位的替换实现不太友好
  2. 有时候处理完模板占位之后可能还需要一些自定义的操作,EasyExcel支持自定义处理,但是不完全自定义,处理复杂,而EasyPOI不支持自定处理

实现目标

  1. 同时支持单个占位替换和list多行占位替换,并且list可以有多个
  2. 支持完全自定义后续Excel处理

实现步骤

一个Excel模板

参考附件
image-1700011685324

占位符说明

单个常量占位符用 {key},如图中的 {taskNo};

list占位符用 #[key],如图中的 #[list1],注意同一批list占位只需要一个就行,如果有多批list需要多个占位,比如 #[mainList]#[list1]就是两批list要替换,而单独的每一批中都有多行数据,每一行里边又是多个单元格

引入Poi依赖

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.2</version>
        </dependency>

模板类ExcelTemplate

package com.hugo.poi.eladmin;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

/**
 * @author zhangmy
 * @date 2023/11/8 15:47
 * @description
 */
@Slf4j
public class ExcelTemplate {

    /**
     * 工作簿
     */
    private Workbook workbook;

    /**
     * sheet表单
     */
    private Sheet sheet;

    public Workbook getWorkbook() {
        return workbook;
    }

    public Sheet getSheet() {
        return sheet;
    }

    /**
     * 构造函数 -- 在建造器中的build方法中调用此构造函数
     * @param workbook
     * @param sheet
     */
    ExcelTemplate(Workbook workbook, Sheet sheet) {
        this.workbook = workbook;
        this.sheet = sheet;
    }

    public void closeWorkbook() {
        if (workbook != null) {
            try {
                workbook.close();
            } catch (IOException e) {
                log.error("关闭workbook失败:", e);
            }
        }
    }

    /**
     * 获取ExcelTemplateBuilder
     * @return
     */
    public static ExcelTemplate.ExcelTemplateBuilder builder() {
        return new ExcelTemplateBuilder();
    }


    /**
     * 内部构造器类
     */
    public static class ExcelTemplateBuilder {

        /**
         * 内部builder类也必须持有外部相同的属性
         */
        private Workbook workbook;

        /**
         * 内部builder类也必须持有外部相同的属性
         */
        private Sheet sheet;

        /**
         * 无参构造函数,在外部builder方法中会调用
         */
        ExcelTemplateBuilder() {}

        /**
         * 模板文件路径
         * @param templatePath
         * @return
         */
        public ExcelTemplate.ExcelTemplateBuilder workbook(String templatePath) {
            try (InputStream inputStream = Files.newInputStream(new File(templatePath).toPath())) {
                this.workbook = new XSSFWorkbook(inputStream);
            } catch (Exception e) {
                log.error("读取Excel模板错误", e);
            }
            return this;
        }

        public ExcelTemplate.ExcelTemplateBuilder workbook(InputStream inputStream) {
            try {
                this.workbook = new XSSFWorkbook(inputStream);
            } catch (Exception e) {
                log.error("读取Excel模板错误", e);
            }
            return this;
        }

        /**
         * 模板所在工作簿的sheet,默认第一个
         * @return
         */
        public ExcelTemplate.ExcelTemplateBuilder sheet() {
            return sheet(0);
        }

        /**
         * 模板所在工作簿的sheet,指定index
         * @param sheetIndex
         * @return
         */
        public ExcelTemplate.ExcelTemplateBuilder sheet(Integer sheetIndex) {
            this.sheet = this.workbook.getSheetAt(sheetIndex);
            return this;
        }

        /**
         * 最后调用此方法返回ExcelTemplate
         *  调用示例: ExcelTemplate.builder().templatePath(path).sheet().build()
         * @return
         */
        public ExcelTemplate build() {
            return new ExcelTemplate(this.workbook, this.sheet);
        }
    }
}

这个工具类有几处注意的地方:

  1. 必须持有Workbook对象和Sheet,因为后续都是基于sheet操作,而sheet来源于workbook;
  2. 工具类采用了建造者模式的思想,将ExcelTemplate的创建改造为有内部的ExcelTemplateBuilder类创建,目标是实现链式创建对象

ExcelTemplate template = ExcelTemplate.builder().workbook(templatePath).sheet().build();;

  1. 别忘记关闭workbook文件流,参考方法closeWorkbook

自定义处理器WorkbookHandler

package com.hugo.poi.eladmin;

import org.apache.poi.ss.usermodel.Workbook;

/**
 * @author zhangmy
 * @date 2023/11/10 10:52
 * @description 自定义workbook处理接口
 */
@FunctionalInterface
public interface WorkbookHandler {

    /**
     * 自定义处理workbook
     * @param workbook workbook
     */
    void handle(Workbook workbook);
}

自定义处理器就一个接口handle,用户在自定义处理的时候可自行实现handle方法,@FunctionalInterface表示功能性接口,到时候我们就可以用箭头函数

工具类ExcelTemplateUtil

package com.hugo.poi.eladmin;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhangmy
 * @date 2023/11/13 15:40
 * @description
 */
public class ExcelTemplateUtil {

    /**
     * workbook对象
     */
    private Workbook workbook;

    /**
     * sheet对象
     */
    private Sheet sheet;

    /**
     * 待替换数据()
     */
    private final Map<String, Object> map;

    /**
     * 合并区域map
     *  key为区域index
     *  value为合并的区域对象
     */
    private Map<Integer, CellRangeAddress> cellRangeAddressMap;

    /**
     *  最大列
     */
    private int maxColumn = 0;

    /**
     * 初始化工具类
     * @param template 模板对象
     * @param map 待填充数据
     */
    public ExcelTemplateUtil(ExcelTemplate template, Map<String, Object> map) {
        this.map = map;
        this.workbook = template.getWorkbook();
        if (this.workbook != null) {
            this.sheet = template.getSheet();
        }
    }

    /**
     * 替换并导出
     * @param savePath 保存路径
     * @param handler 自定义处理
     */
    public void replaceAndExport(String savePath, WorkbookHandler handler) {
        try {
            // 替换模板数据
            replaceModel();
            // 允许用户自定义处理Workbook
            if (handler != null) {
                handler.handle(workbook);
            }
            export(savePath);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            closeWorkbook();
        }
    }

    /**
     * 替换并导出
     * @param savePath 保存路径
     */
    public void replaceAndExport(String savePath) {
        try {
            // 替换模板数据
            replaceModel();
            export(savePath);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            closeWorkbook();
        }
    }
    public void replaceModel(WorkbookHandler handler) {
        replaceModel();
        // 允许用户自定义处理Workbook
        if (handler != null) {
            handler.handle(workbook);
        }
    }

    /**
     * 替换数据
     */
    public void replaceModel() {
        // list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据
        Map<Integer[], List<List<String>>> cellListDataMap = new LinkedHashMap<>();
        // 处理list占位行
        dealListRow(cellListDataMap);
        // 最后填充数据
        fillData(cellListDataMap);
    }

    /**
     * 填充常量和list
     * @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据(外层list为多行,里层list为单行多个单元格的值)
     */
    private void fillData(Map<Integer[], List<List<String>>> cellListDataMap) {
        // 填充数据之前重新读取表格单元格合并情况
        readCellRangeAddress();
        int lastRowNum = sheet.getLastRowNum();
        // 遍历每一行,查找占位符
        for (int i = 0; i < lastRowNum; i++) {
            // 当前行
            Row row = sheet.getRow(i);
            if (row == null) continue;
            if (row.getLastCellNum() > maxColumn) {
                maxColumn = row.getLastCellNum();
            }
            // 首个单元格
            int firstCellNum = row.getFirstCellNum();
            // 最后一个单元格
            int lastCellNum = row.getLastCellNum();

            // 遍历每一个单元格,查找list占位符
            for (int j = firstCellNum; j < lastCellNum; j++) {
                // 当前单元格
                Cell cell = row.getCell(j);
                if (cell == null) continue;
                // 只读取cellType为String的
                if (cell.getCellType() != CellType.STRING) continue;
                // 占位符字符
                String placeHolderStr = cell.getStringCellValue().trim();
                // 常量替换
                if (placeHolderStr.startsWith("{") && placeHolderStr.endsWith("}")) {
                    // 真正的占位符
                    String placeHolder = placeHolderStr.substring(1, placeHolderStr.length() - 1);
                    if (map.containsKey(placeHolder)) {
                        // 替换为值
                        setCellValue(cell, map.get(placeHolder));
                        // 自动设置高度
//                        autoSetCellHeightByText(cell, map.get(placeHolder).toString());
                    }
                // list替换
                } else if (placeHolderStr.startsWith("#[") && placeHolderStr.endsWith("]")) {
                    // 真正的占位符
                    String placeHolder = placeHolderStr.substring(2, placeHolderStr.length() - 1);
                    if (map.containsKey(placeHolder)) {
                        // 清空值
                        cell.setCellValue("");
                    }
                }
            }
        }
        readCellRangeAddress();
        fillListData(cellListDataMap);
    }

    /**
     * 处理list占位符
     *  1.查找list占位符标签
     *  2.复制出需要的行数
     * @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据(外层list为多行,里层list为单行多个单元格的值)
     */
    private void dealListRow(Map<Integer[], List<List<String>>> cellListDataMap) {
        // 读取单元格合并区域情况
        this.readCellRangeAddress();
        int lastRowNum = sheet.getLastRowNum();
        // 遍历每一行,查找占位符
        for (int i = 0; i < lastRowNum; i++) {
            // 当前行
            Row row = sheet.getRow(i);
            if (row == null) continue;
            if (row.getLastCellNum() > maxColumn) {
                maxColumn = row.getLastCellNum();
            }
            // 首个单元格
            int firstCellNum = row.getFirstCellNum();
            // 最后一个单元格
            int lastCellNum = row.getLastCellNum();

            // 遍历每一个单元格,查找list占位符
            for (int j = firstCellNum; j < lastCellNum; j++) {
                // 当前单元格
                Cell cell = row.getCell(j);
                if (cell == null) continue;
                // 只读取cellType为String的
                if (cell.getCellType() != CellType.STRING) continue;
                // 占位符字符
                String placeHolderStr = cell.getStringCellValue().trim();
                if (placeHolderStr.startsWith("#[") && placeHolderStr.endsWith("]")) {
                    // 真正的占位符
                    String placeHolder = placeHolderStr.substring(2, placeHolderStr.length() - 1);
                    // 占位符待替换的数据值
                    @SuppressWarnings("unchecked")
                    List<List<String>> cellDataList = (List<List<String>>)map.get(placeHolder);
                    // 记录此单元格待替换数据
                    cellListDataMap.put(new Integer[]{j, i}, cellDataList);

                    // 新行的行号
                    int r = i;
                    if (cellDataList != null && !cellDataList.isEmpty()) {
                        // 往下移动待插入数据总数-1行,占位符行默认要使用
                        sheet.shiftRows(r + 1, sheet.getLastRowNum(), cellDataList.size() -1, true, false);
                        // 向下移动之后重置最后一行行号
                        lastRowNum = sheet.getLastRowNum();
                        // 跳过遍历新增的行
                        i = i + cellDataList.size() - 1;

                    }
                    if (cellDataList != null && !cellDataList.isEmpty()) {
                        // 待插入行数据处理
                        for (int n = 0; n < cellDataList.size() - 1; n++) {
                            r++;
                            Row newRow = sheet.createRow(r);
                            // 复制行 -- 包含样式的复制
                            copyRow(row, newRow);
                        }
                    }
                    if (cellDataList != null && !cellDataList.isEmpty()) {
                        readCellRangeAddress();
                    }
                }
            }
        }
    }

    /**
     * 导出
     * @param savePath 保存文件路径
     */
    public void export(String savePath) {
        try (FileOutputStream out = new FileOutputStream(savePath)){
            workbook.write(out);
            out.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            closeWorkbook();
        }
    }

    /**
     * 关闭工作簿(很重要别忘记了)
     */
    public void closeWorkbook() {
        if (workbook != null) {
            try {
                workbook.close();
            } catch (IOException e) {
                System.out.println("关闭workbook失败:" + e);
            }
        }
    }

    /**
     * 填充list占位符数据
     * @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据
     */
    private void fillListData(Map<Integer[], List<List<String>>> cellListDataMap) {
        if (cellListDataMap != null && !cellListDataMap.isEmpty()) {
            // 遍历每一个list占位符
            for (Map.Entry<Integer[], List<List<String>>> entry : cellListDataMap.entrySet()) {
                // 单元格坐标 [列,行]
                Integer[] colRow = entry.getKey();
                // 行index
                int rowIndex = colRow[1];
                // 数据
                List<List<String>> dataList = entry.getValue();
                if (dataList != null && !dataList.isEmpty()) {
                    // 遍历多行数据
                    for (List<String> list : dataList) {
                        // dataList中第n个数据就是rowIndex + n所在行的数据
                        Row row = sheet.getRow(rowIndex);
                        if (row == null) break;
                        // 列index
                        int c = colRow[0];
                        // 遍历第rowIndex + n行数据,填充每一个单元格
                        for (String s : list) {
                            Cell cell = row.getCell(c);
                            if (cell == null) break;
                            // 设置单元格的值
                            String cellValue = s != null ? s : "";
                            cell.setCellValue(cellValue);
                            // 查询单元格是否合并
                            CellRangeAddress mergedRegion = getMergedRegion(cell);
                            // 遍历下一个单元格
                            c++;
                            if (mergedRegion != null) {
                                // 合并了单元格,则跳过合并单元格的遍历
                                c += mergedRegion.getLastColumn() - mergedRegion.getFirstColumn();
                            }
                        }
                        rowIndex++;
                    }
                }
            }
        }
    }

    /**
     * 设置单元格的值
     * @param cell 单元格对象
     * @param value 值
     */
    private void setCellValue(Cell cell, Object value) {
        // 根据值的类型来设置不同类型的单元格值
        if (value == null) {
            cell.setCellValue("");
        }
        if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof Integer) {
            cell.setCellValue((Integer) value);
        } else if (value instanceof Double) {
            cell.setCellValue((Double) value);
        } else if (value instanceof Date) {
            cell.setCellValue((Date) value);
        } else if (value != null) {
            // 对于其他所有类型,调用toString()
            cell.setCellValue(value.toString());
        }
    }

    /**
     * 根据文本值自动设置单元格的高度
     * @param cell 单元格对象
     * @param text 文本值
     */
    private void autoSetCellHeightByText(Cell cell, String text) {
        // 获取合并区域
        CellRangeAddress mergedRegion = getMergedRegion(cell);
        if (mergedRegion == null) {
            // 获取宽度(像素)
            float columnWidthInPixels = sheet.getColumnWidthInPixels(cell.getColumnIndex());
            // 将文本以换\n分割
            String[] texts = text.split("\n");
            int rows = 0;
            for (String content : texts) {
                // 获取文本像素
                float realPx = getLengthByContent(content);
                int row = (int) Math.ceil(realPx / columnWidthInPixels);
                rows += row;
            }
        } else {
            int firstRow = mergedRegion.getFirstRow();
            int lastRow = mergedRegion.getLastRow();
            int firstColumn = mergedRegion.getFirstColumn();
            int lastColumn = mergedRegion.getLastColumn();

            float columnWidthInPixels = 0f;
            for (int i = firstColumn; i <= lastColumn; i++) {
                columnWidthInPixels += sheet.getColumnWidthInPixels(i);
            }

            String[] texts = text.split("\r\n");
            short rows = 0;
            for (String content : texts) {
                float realPx = getLengthByContent(content);
                int row = (int) Math.ceil(realPx / columnWidthInPixels);
                rows += row;
            }

            if (firstRow == lastRow) {
                // 同一行
                float heightInPoints = 18.65f;
                if (rows > 1) {
                    cell.getCellStyle().setWrapText(true);
                }
                cell.getRow().setHeightInPoints((int) (heightInPoints * rows - (rows - 1) * 0.15 * heightInPoints));
            }
        }
    }

    /**
     * 获取文本像素
     *      汉字13.72像素
     *      大写英文 9.80像素
     *      小写 5.69像素
     * @param content 文本内容
     * @return 像素
     */
    private float getLengthByContent(String content) {
        float length=0;
        for(int i=0;i<content.length();i++){
            char c=content.charAt(i);
            if(c>='A' && c<='Z'){
                length+=5.69;
            }else if(c>='\0' && c<='\255'){
                length+=9.80;
            }else{
                length+=13.72;
            }
        }
        return length;
    }

    /**
     * 内部方法 -- 获取单元格的值
     * @param cell
     * @return
     */
    private Object getCellValue(Cell cell) {
        if (cell != null) {
            switch (cell.getCellType()) {
                case NUMERIC:
                    return cell.getNumericCellValue();
                case STRING:
                    return cell.getStringCellValue();
                case BOOLEAN:
                    return cell.getBooleanCellValue();
                case ERROR:
                    return cell.getErrorCellValue();
            }
        }
        return null;
    }

    /**
     * 内部方法 -- 行复制功能(包含数据和样式)
     * @param row 原行
     * @param newRow 新行
     */
    private void copyRow(Row row, Row newRow) {
        if (row == null || newRow == null) return;
        // 遍历行的每一个单元格
        for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
            // 读取要复制的单元格
            Cell cell = row.getCell(i);
            if (cell == null) break;
            Cell newCell;
            // 读取待复制单元格的单元格合并情况
            CellRangeAddress mergedRegion = getMergedRegion(cell);
            if (mergedRegion == null) {
                newCell = newRow.createCell(i);
                // 复制样式
                copyCellStyle(cell.getCellStyle(), newCell.getCellStyle());
            } else {
                // 看合并了几个-就复制几个
                if (mergedRegion.getFirstRow() == mergedRegion.getLastRow()) {
                    // 确定合并了同一行-合并了n个格
                    int n = mergedRegion.getLastColumn() - mergedRegion.getFirstColumn();
                    for (int j = mergedRegion.getFirstColumn(); j <= mergedRegion.getLastColumn(); j++) {
                        newCell = newRow.createCell(i);
                        if (j == mergedRegion.getFirstColumn()) {
                            // 复制样式
                            copyCellStyle(cell.getCellStyle(), newCell.getCellStyle());
                        }
                    }
//                    System.out.println("插入新行--合并单元格:第" + (newRow.getRowNum() + 1) + "行,第" + (i + 1) + "~" + (i + n + 1) + "列");
                    // 合并单元格
                    mergedRegion(newRow.getRowNum(), i, i + n);
                    // 已合并的单元格不再遍历
                    i = i + n;
                }
            }
        }
    }

    /**
     * 进行合并单元格 合并同行的区间格
     * @param firstRow 合并开始行,这里只有一行的合并
     * @param firstCol 合并开始列
     * @param lastCol 合并结束列
     */
    private void mergedRegion(int firstRow, int firstCol,int lastCol) {
        CellRangeAddress region = new CellRangeAddress(firstRow, firstRow, firstCol, lastCol);
        sheet.addMergedRegion(region);
    }

    /**
     * 内部方法 -- 样式复制
     * @param cellStyle 原单元格样式
     * @param newCellStyle 新单元格样式
     */
    private void copyCellStyle(CellStyle cellStyle, CellStyle newCellStyle) {
        if (cellStyle == null || newCellStyle == null) return;
        newCellStyle.cloneStyleFrom(cellStyle);
    }

    /**
     * 获取单元格的合并情况
     * @param cell
     * @return
     */
    private CellRangeAddress getMergedRegion(Cell cell) {
        return cellRangeAddressMap.get(cell.getRowIndex() * 100000 + cell.getColumnIndex());
    }

    /**
     * 读取单元格合并区域情况
     *  注意在不断替换占位符的过程中,合并区域会不断变化
     */
    private void readCellRangeAddress() {
        if (cellRangeAddressMap == null) {
            cellRangeAddressMap = new LinkedHashMap<>();
        } else {
            cellRangeAddressMap.clear();
        }
        // 遍历合并的区域
        for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
            // 当前合并区域
            CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
            // 合并区域-第一行
            int firstRow = cellRangeAddress.getFirstRow();
            // 合并区域-最后一行
            int lastRow = cellRangeAddress.getLastRow();
            // 合并区域-第一列
            int firstColumn = cellRangeAddress.getFirstColumn();
            // 合并区域-最后一列
            int lastColumn = cellRangeAddress.getLastColumn();
//            System.out.println("当前的合并区域为:第" + (firstRow + 1) + "~" + (lastRow + 1) + "行,第" + (firstColumn + 1) + "~" + (lastColumn + 1) + "列");
            // 组装区域合并情况map
            for (int r = firstRow; r <= lastRow; r++) {
                for (int c = firstColumn; c <= lastColumn; c++) {
                    cellRangeAddressMap.put(r * 100000 + c, cellRangeAddress);
                }
            }
        }
    }

    /**
     * 设置合并单元格外边框
     */
    public void setMergedCellBoarder() {
        for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (row != null) {
                for (int colNum = 0; colNum < row.getLastCellNum(); colNum++) {
                    Cell cell = row.getCell(colNum);
                    if (cell != null) {
                        // 读取原单元格样式
                        CellStyle originalStyle = cell.getCellStyle();
                        // 判断当前单元格是否是合并单元格
                        CellRangeAddress cellRangeAddress = isMergedRegion(rowNum, colNum);
                        if (cellRangeAddress != null) {
                            RegionUtil.setBorderTop(BorderStyle.THIN, cellRangeAddress, sheet);
                            RegionUtil.setBorderBottom(BorderStyle.THIN, cellRangeAddress, sheet);
                            RegionUtil.setBorderLeft(BorderStyle.THIN, cellRangeAddress, sheet);
                            RegionUtil.setBorderRight(BorderStyle.THIN, cellRangeAddress, sheet);
                        }
                        // 设置原格式
                        cell.setCellStyle(originalStyle);
                    }
                }
            }
        }
    }

    /**
     * 判断是否是合并单元格并返回合并单元格区域
     * @param row 行index
     * @param column 列index
     * @return 合并区域
     */
    private CellRangeAddress isMergedRegion(int row, int column) {
        for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) {
            if (mergedRegion.isInRange(row, column)) {
                return mergedRegion;
            }
        }
        return null;
    }
}

工具类说明:

  1. 核心处理逻辑在 replaceModel()方法里边;
  2. 有一个 readCellRangeAddress()方法,是用来读取合并区域情况的,这个方法调用的比较频繁,开始的时候要调用一次,中间插入了新行或者手动合并了单元格都需要重新读取,最后在填充数据之前也需要重新读取一次;
  3. 对于单个list占位,首先根据要填充数据list的size插入指定行数,这个插入新行对应在POI中是需要先 sheet.shiftRows(r + 1, sheet.getLastRowNum(), cellDataList.size() -1, true, false);,然后 sheet.createRow(r);
  4. 插入行之后为了保证下移过程中的数据和样式保持不变,需要对插入的行的样式复制成倍复制对象的样式,参考 copyRow(row, newRow);方法,++这一点很重要(★★★★★)++,否则的话会出现数据和样式的丢失
  5. 填充list数据的时候注意跳过合并单元格的数据填充;
  6. 自定义的处理handler可以根据需要调整处理的位置,我这里是在最后导出excel之前进行的处理,参考 replaceAndExport方法;
  7. 在导出表格的方法里记得要关闭workbook,参考 export方法。

测试

package com.hugo.poi.eladmin;

import com.alibaba.excel.util.MapUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author zhangmy
 * @date 2023/11/13 18:02
 * @description
 */
public class ExcelTemplateUtilTest {

    public static void main(String[] args) {
        // 模板文件名
        String templatePath = "C:\\Users\\Administrator\\Desktop\\easyExcel测试\\v2\\Pooling任务单-是v2.xlsx";
        // 导出文件名
        String saveFileName = "C:\\Users\\Administrator\\Desktop\\easyExcel测试\\v2\\Pooling任务单-是v2" + System.currentTimeMillis() + ".xlsx";

        ExcelTemplate template = ExcelTemplate.builder().workbook(templatePath).sheet().build();
        // 测试数据
        Map<String, Object> dataMap = buildData();
        // 替换模板、自定义处理并导出
        ExcelTemplateUtil excelTemplateUtil = new ExcelTemplateUtil(template, dataMap);
        excelTemplateUtil.replaceAndExport(saveFileName, workbook -> {
            // 获取第一个Sheet
            Sheet sheet = template.getSheet();
            // 第2~最后一行行高设置为18
            int startRow = 2;
            int lastRow = sheet.getLastRowNum();
            for (int i = startRow; i <= lastRow; i++) {
                Row row = sheet.getRow(i);
                if (row == null) {
                    row = sheet.createRow(i); // 如果行不存在,则创建
                }
                row.setHeightInPoints(18); // 设置行高为18点
            }
            // 设置合并单元格外边框
            excelTemplateUtil.setMergedCellBoarder();
        });
    }

    /**
     * 组装测试数据
     * @return
     */
    private static Map<String, Object> buildData() {
        // 组装填充数据 -- 简单填充(占位符用{xxx})
        Map<String, Object> map = MapUtils.newHashMap();
        map.put("taskNo", "pooling-第28批-nextseq 550AR(2)-TPNB500543AR");
        map.put("createTime", "2023-03-17 09:51:59");
        map.put("phixK", "Phix");
        map.put("libMer", "1.7pm");
        map.put("rsb", 25.3);
        map.put("changeLib", 476.0);
        map.put("rsb2", 990);
        map.put("mainPoolingV", 37.3471);
        map.put("mainPoolingMer", 3.53);
        map.put("mainPoolingData", 1.2);
        map.put("mainPoolingTheoryAbout", "0.8ng/μl");
        map.put("mainPoolingTheoryRange", "0.64~0.96ng/μl");
        map.put("pooling1V", 405.82);
        map.put("pooling1Mer", 2.67);
        map.put("pooling1Data", 0.9);
        map.put("pooling1TheoryAbout", "0.61ng/μl");
        map.put("pooling1TheoryRange", "0.49~0.73ng/μl");
        map.put("pooling2V", 2.93);
        map.put("pooling2Mer", 122.92);
        map.put("pooling2Data",0.3);
        map.put("pooling2TheoryAbout", "27.96ng/μl");
        map.put("pooling2TheoryRange", "22.37~33.56ng/μl");

        // 组装填充数据 -- 列表填充(占位符用#{}) -- 主Pooling
        List<List<String>> mainPoolingList = new ArrayList<>();
        mainPoolingList.add(new ArrayList<String>() {
            {
                add(1 + "");
                add("pooling1");
                add("37.08");
            }
        });
        mainPoolingList.add(new ArrayList<String>() {
            {
                add(2 + "");
                add("pooling2");
                add("0.27");
            }
        });
        mainPoolingList.add(new ArrayList<String>() {
            {
                add(3 + "");
                add("pooling3");
                add("18");
            }
        });
        map.put("mainList", mainPoolingList);

        // 组装填充数据 -- 列表填充(占位符用{}) -- Pooling1
        List<List<String>> pooling1List = new ArrayList<>();
        pooling1List.add(new ArrayList<String>() {
            {
                add("B1-06A");
                add("BY220095");
                add("测试");
                add("42.76");
            }
        });
        pooling1List.add(new ArrayList<String>() {
            {
                add("B1-06B");
                add("BY220095");
                add("测试");
                add("7.24");
            }
        });
        pooling1List.add(new ArrayList<String>() {
            {
                add("B1-06c");
                add("BY2200956");
                add("测试");
                add("15.55");
            }
        });
        map.put("list1", pooling1List);

        return map;
    }
}

image-1700020420101

总结

  1. 大的步骤分为三步:
  • 读取list占位符
  • 根据待填充的数据容量大小插入指定数量的空白行
  • 填充常量占位和list占位的值
  1. 还有两个核心的点:
  • 要保证在每一轮修改excel单元格样式之前要先读取合并区域的情况,比如插入空白行之前(多个list占位就会有多次插入空白行,每次都需要读取之前的合并区域情况),在填充数据之前也需要,因为list占位填充的的单元格也可能有合并单元格,参考readCellRangeAddress(方法)
  • 逻辑不要搞错了,我之前尝试想读取每一行每一个单元格,查找到占位符就直接替换或者插入新行再替换,这样出来的效果是不对的,一定是要先读取整个表格的布局,然后找到对应的占位符,需要上下移动就上下移动,然后把空白行创建好,最后再填充数据
  1. 最后一个需要注意的地方:

不要直接在变量行或者单元格对象的时候修改行或者单元格,那样会并行修改异常,应该用int i,j的方式,根据i,j去获取行、列。

0

评论区