基于 Apache POI 5.2.5 构建高效 Excel 工具类:从零到生产级实践

文章目录

    • [一、为什么选择 Apache POI?](#一、为什么选择 Apache POI?)
    • 二、工具类设计目标
    • 三、核心功能详解
      • [1. 创建并填充数据表](#1. 创建并填充数据表)
      • [2. 支持按"列"组织的数据结构](#2. 支持按“列”组织的数据结构)
      • [3. 防止科学计数法:设置文本格式](#3. 防止科学计数法:设置文本格式)
      • [4. 内置常用数据验证规则](#4. 内置常用数据验证规则)
      • [5. 实现跨 Sheet 数据联动(VLOOKUP)](#5. 实现跨 Sheet 数据联动(VLOOKUP))
      • [6. 获取 Excel 总行数(用于导入预判)](#6. 获取 Excel 总行数(用于导入预判))
    • 四、完整使用示例
    • 五、完整excel工具类

在企业级 Java 开发中,Excel 文件的导入导出是高频需求。无论是报表生成、批量数据处理还是用户上传模板,都需要一个稳定、灵活且功能丰富的 Excel 操作工具。

本文将基于 Apache POI 5.2.5 版本,结合实际项目经验,分享一个生产可用的 Excel 工具类(ApachePoiUtils) 的设计思路与核心实现,并深入解析其关键特性,帮助你快速构建高质量的 Excel 处理能力。

一、为什么选择 Apache POI?

Apache POI 是 Java 领域最主流的 Office 文档操作库,支持 .xls 和 .xlsx 格式。它提供了对 Excel 的精细控制,适用于复杂场景。

✅ 优势:

完全开源免费

支持读写 .xlsx (XSSF) 和 .xls (HSSF)

可精确控制单元格样式、公式、数据验证等

社区活跃,版本迭代稳定

⚠️ 注意事项(POI 5.2.5):

使用 XSSF 模式时内存消耗较高,大数据量建议配合 SXSSF 或流式读取

依赖较多,需注意版本兼容性(推荐使用 BOM 管理)

xml 复制代码
<!-- Maven 引入 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

二、工具类设计目标

我们期望这个工具类能解决以下常见痛点:

为此,我们封装了 ApachePoiUtils 类,具备如下能力:

✅ 创建带样式的 Sheet

✅ 支持 Map/列表结构数据填充

✅ 设置文本格式防止精度丢失

✅ 内置常用字段验证(手机号、身份证、车牌号、姓名)

✅ 支持下拉选择与跨表联动

✅ 自动计算行高列宽

三、核心功能详解

1. 创建并填充数据表

方法签名:

java 复制代码
public static Sheet createSheetAndSetValue(
    List<String> titleNameList, 
    List<List<String>> dataList,
    String dataSheetName, 
    Workbook workbook)

功能说明:

自动生成标题行,加粗 + 背景色 + 居中

设置默认字体大小为 14pt

每行高度设为 300,标题行 400

所有列默认设置为文本格式,避免数字被自动转换

列宽统一设置为 28 * 256 单位(约 28 字符宽度)

示例调用:

java 复制代码
Workbook wb = new XSSFWorkbook();
List<String> titles = Arrays.asList("姓名", "电话", "身份证");
List<List<String>> data = Arrays.asList(
    Arrays.asList("张三", "13812345678", "51010419900307XXXX")
);
Sheet sheet = ApachePoiUtils.createSheetAndSetValue(titles, data, "用户信息", wb);

2. 支持按"列"组织的数据结构

有时数据是以列为单位存储的,比如前端传来的 JSON 结构:

json 复制代码
{
  "names": ["张三", "李四"],
  "phones": ["138...", "139..."]
}

为此提供专用方法:

java 复制代码
public static Sheet createSheetAndSetValueByCol(
    List<String> titleNameList, 
    List<List<String>> colDataList,
    String dataSheetName, 
    Workbook workbook)

该方法会自动转置数据,同时支持交替背景色提升可读性。

3. 防止科学计数法:设置文本格式

这是最容易被忽视的问题!当导出长数字(如身份证、手机号)时,Excel 默认将其识别为数值类型,导致显示异常或精度丢失。

解决方案:设置列样式为文本格式 @

java 复制代码
CellStyle textStyle = workbook.createCellStyle();
textStyle.setDataFormat(createHelper.createDataFormat().getFormat("@"));
currentSheet.setDefaultColumnStyle(colIndex, textStyle);

我们在所有数据列上都应用了此样式,确保内容原样展示。

4. 内置常用数据验证规则

通过 DataValidation 实现 Excel 原生校验功能,提升用户体验。

(1)手机号验证

校验逻辑:

必须为 11 位

必须以 1 开头

全部由数字组成

java 复制代码
String phoneFormula = "AND(LEN(A2)=11, ISNUMBER(VALUE(A2)), LEFT(A2,1)=\"1\")";

(2)身份证号验证

综合判断:

长度 18 位

前17位为数字

最后一位为数字或 X

出生日期 ≤ 当前日期

java 复制代码
=AND(
  LEN(A2)=18,
  ISNUMBER(VALUE(LEFT(A2,17))),
  OR(ISNUMBER(VALUE(RIGHT(A2,1))),UPPER(RIGHT(A2,1))="X"),
  DATEVALUE(TEXT(MID(A2,7,8),"0000-00-00"))<=TODAY()
)

(3)车牌号验证(蓝牌 & 新能源绿牌)

支持两种格式:

蓝牌:京A12345(7位)

绿牌:京AB12345(8位)

首字符必须为中文(通过 LENB(LEFT(A2,1))=2 判断双字节)

java 复制代码
=OR(
  AND(LEN(A2)=7, LENB(LEFT(A2,1))=2, CODE(MID(A2,2,1))>=65),
  AND(LEN(A2)=8, LENB(LEFT(A2,1))=2, CODE(MID(A2,2,1))>=65)
)

(4)姓名验证(仅限中文)

限制条件:

2~15 个字符

全部为中文(双字节)

不含空格或特殊符号

java 复制代码
=AND(
  LEN(A2)>=2, LEN(A2)<=15,
  LENB(A2)=LEN(A2)*2,
  EXACT(A2,CLEAN(A2))
)

(5)下拉框选择

常用于性别、状态等枚举字段:

java 复制代码
setDropdownValidation(sheet, "\"男,女\"", 2, 2); // C列只能选"男"或"女"

也可引用其他 Sheet 的区域:

java 复制代码
setDropdownValidation(sheet, "地区表!$A$2:$A$100", 3, 3);

5. 实现跨 Sheet 数据联动(VLOOKUP)

这是高级功能,可用于省市区三级联动、部门-负责人关联等。

思路:

在辅助 Sheet 中维护映射关系(如:部门 → 负责人)

主表中使用 VLOOKUP 公式动态获取结果

java 复制代码
ApachePoiUtils.setDataLinkage(mainSheet, 0, 1, "部门映射表");

生成的公式示例:

java 复制代码
=IFERROR(VLOOKUP(A2, 部门映射表!$A$2:$B$100000, 2, FALSE), "")

6. 获取 Excel 总行数(用于导入预判)

在文件上传阶段,可先统计有效行数,便于后续分页或资源分配。

java 复制代码
try (InputStream is = file.getInputStream()) {
    long rowCount = ApachePoiUtils.getTotalRows(is);
    System.out.println("共 " + rowCount + " 行数据");
}

内部通过遍历 Row 并判断是否为空行来统计,比 getLastRowNum() 更准确。

四、完整使用示例

java 复制代码
@Test
public void testExport() throws IOException {
    Workbook workbook = new XSSFWorkbook();

    // 1. 创建主表
    List<String> titles = Arrays.asList("姓名", "电话", "身份证", "车牌号");
    Map<String, String> dataMap = new LinkedHashMap<>();
    dataMap.put("张三", "13812345678");
    dataMap.put("李四", "13987654321");

    Sheet sheet = ApachePoiUtils.createSheetAndSetValueByMap(
        titles, dataMap, "用户数据", workbook);

    // 2. 设置验证规则
    ApachePoiUtils.setMobileValidation(sheet, 1, 1);     // B列:手机号
    ApachePoiUtils.setIdCardValidation(sheet, 2, 2);     // C列:身份证
    ApachePoiUtils.setLicensePlateValidation(sheet, 3, 3); // D列:车牌
    ApachePoiUtils.setNameValidation(sheet, 0, 0);       // A列:姓名

    // 3. 添加下拉框(假设第4列是性别)
    ApachePoiUtils.setDropdownValidation(sheet, "\"男,女\"", 4, 4);

    // 4. 保存文件
    try (FileOutputStream out = new FileOutputStream("用户导入模板.xlsx")) {
        workbook.write(out);
    }
}

五、完整excel工具类

java 复制代码
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSONObject;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.ss.util.CellReference;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * @author super hero
 * @Description
 */
public class ApachePoiUtils {

    ////////////////////////////////////// 创建数据表开始 /////////////////////////////////////

    /**
     * 创建sheet并且设置数据
     *
     * @param titleNameList 标题名称
     * @param dataMap       数据,适用于只有两列的情况
     * @param dataSheetName sheet名称
     * @param workbook      工作表
     * @return 当前sheet
     */
    public static Sheet createSheetAndSetValueByMap(List<String> titleNameList, Map<String, String> dataMap,
                                                    String dataSheetName, Workbook workbook) {
        List<List<String>> regionDataList = new ArrayList<>();
        if (CollUtil.isNotEmpty(dataMap)) {
            Iterator<Map.Entry<String, String>> iterator = dataMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, String> next = iterator.next();
                String code = Optional.ofNullable(next.getKey()).orElse("");
                String name = Optional.ofNullable(next.getValue()).orElse("");
                regionDataList.add(Arrays.asList(code, name));
            }
        }
        return createSheetAndSetValue(titleNameList, regionDataList, dataSheetName, workbook);
    }

    /**
     * 创建sheet并且设置数据
     *
     * @param titleNameList 标题名称
     * @param dataList      数据,数据是一行 一行的
     * @param dataSheetName sheet名称
     * @param workbook      工作表
     * @return 当前sheet
     */
    public static Sheet createSheetAndSetValue(List<String> titleNameList, List<List<String>> dataList,
                                               String dataSheetName, Workbook workbook) {
        // 创建数据表
        Sheet dataSheet = workbook.createSheet(dataSheetName);
        // 设置标题
        Row rowTitle = dataSheet.createRow(0);

        // 创建并配置标题的单元格样式
        Font font = workbook.createFont();
        // 加粗字体
        font.setBold(true);
        // 设置字体大小为 14
        font.setFontHeightInPoints((short) 14);

        CellStyle headerCellStyle = workbook.createCellStyle();
        headerCellStyle.setFont(font);
        // 背景颜色
        headerCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 设置边框和居中
        setBorderAndCenter(headerCellStyle, true);

        int colTotalSize = titleNameList.size();
        for (int i = 0; i < colTotalSize; i++) {
            String currentTitleName = Optional.ofNullable(titleNameList.get(i)).orElse("");
            Cell cell = rowTitle.createCell(i);
            cell.setCellValue(currentTitleName);
            // 应用样式到标题单元格
            cell.setCellStyle(headerCellStyle);
        }
        // 设置行高
        rowTitle.setHeight((short) 400);

        for (int row = 0; row < dataList.size(); row++) {
            List<String> rowDataList = dataList.get(row);
            if (CollUtil.isEmpty(rowDataList)) {
                continue;
            }
            int dataSize = rowDataList.size();
            if (colTotalSize != dataSize) {
                throw new RuntimeException("第" + (row + 1) + "条数据,缺少数据," + JSONObject.toJSONString(rowDataList));
            }
            Row rowInfo = dataSheet.createRow(row + 1);
            // 文本行
            rowInfo.setHeight((short) 300);
            for (int col = 0; col < colTotalSize; col++) {
                rowInfo.createCell(col).setCellValue(rowDataList.get(col));
            }
        }
        // === 列可编辑 + 文本格式(避免科学计数法)===
        CreationHelper createHelper = workbook.getCreationHelper();
        CellStyle phoneCellStyle = workbook.createCellStyle();
        phoneCellStyle.setDataFormat(createHelper.createDataFormat().getFormat("@")); // 文本格式
        // 设置边框和居中
        setBorderAndCenter(phoneCellStyle, false);
//                phoneCellStyle.setLocked(false); // ✅ 必须可编辑
        for (int i = 0; i < colTotalSize; i++) {
            dataSheet.setDefaultColumnStyle(i, phoneCellStyle);
            dataSheet.setColumnWidth(i, 28 * 256);
        }
        return dataSheet;
    }

    /**
     * 创建sheet并且设置数据
     *
     * @param titleNameList 标题名称
     * @param colDataList   数据,这里的数据是 一列 一列 的
     * @param dataSheetName sheet名称
     * @param workbook      工作表
     * @return 当前sheet
     */
    public static Sheet createSheetAndSetValueByCol(List<String> titleNameList, List<List<String>> colDataList,
                                                    String dataSheetName, Workbook workbook) {
        // 创建数据表
        Sheet dataSheet = workbook.createSheet(dataSheetName);
        // 设置标题
        Row rowTitle = dataSheet.createRow(0);

        // 创建并配置标题的单元格样式
        Font font = workbook.createFont();
        // 加粗字体
        font.setBold(true);
        // 设置字体大小为 14
        font.setFontHeightInPoints((short) 14);

        CellStyle headerCellStyle = workbook.createCellStyle();
        headerCellStyle.setFont(font);
        // 背景颜色
        headerCellStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
        // 实心前景色填充
        headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 设置边框和居中
        setBorderAndCenter(headerCellStyle, true);

        CellStyle headerTwoCellStyle = workbook.createCellStyle();
        headerTwoCellStyle.setFont(font);
        // 背景颜色
        headerTwoCellStyle.setFillForegroundColor(IndexedColors.LAVENDER.getIndex());
        // 实心前景色填充
        headerTwoCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        // 设置边框和居中
        setBorderAndCenter(headerTwoCellStyle, true);

        int colTotalSize = titleNameList.size();
        for (int i = 0; i < colTotalSize; i++) {
            String currentTitleName = Optional.ofNullable(titleNameList.get(i)).orElse("");
            Cell cell = rowTitle.createCell(i);
            cell.setCellValue(currentTitleName);
            cell.setCellStyle(i % 2 == 0 ? headerCellStyle : headerTwoCellStyle);
        }
        // 设置行高
        rowTitle.setHeight((short) 400);

        int maxSize = 1;
        for (List<String> stringList : colDataList) {
            int size = stringList.size();
            if (size > maxSize) {
                maxSize = size;
            }
        }

        List<Row> rowList = new ArrayList<>();
        for (int row = 1; row <= maxSize; row++) {
            // 创建行
            Row row1 = dataSheet.createRow(row);
            // 文本行
            row1.setHeight((short) 300);
            rowList.add(row1);
        }
        for (int i = 0; i < colDataList.size(); i++) {
            List<String> colData = colDataList.get(i);
            if (CollUtil.isEmpty(colData)) {
                continue;
            }
            for (int j = 0; j < colData.size(); j++) {
                Row cells = rowList.get(j);
                cells.createCell(i).setCellValue(colData.get(j));
            }
        }

        // === 列可编辑 + 文本格式(避免科学计数法)===
        CreationHelper createHelper = workbook.getCreationHelper();

        CellStyle txtOneCellStyle = workbook.createCellStyle();
        txtOneCellStyle.setDataFormat(createHelper.createDataFormat().getFormat("@")); // 文本格式
//                phoneCellStyle.setLocked(false); // ✅ 必须可编辑
        // 设置边框和居中
        setBorderAndCenter(txtOneCellStyle, false);
        // 背景颜色
        txtOneCellStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex());
        // 实心前景色填充
        txtOneCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        CellStyle txtTwoCellStyle = workbook.createCellStyle();
        txtTwoCellStyle.setDataFormat(createHelper.createDataFormat().getFormat("@")); // 文本格式
//                phoneCellStyle.setLocked(false); // ✅ 必须可编辑
        // 设置边框和居中
        setBorderAndCenter(txtTwoCellStyle, false);
        // 背景颜色
        txtTwoCellStyle.setFillForegroundColor(IndexedColors.LAVENDER.getIndex());
        // 实心前景色填充
        txtTwoCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        for (int i = 0; i < colTotalSize; i++) {
            dataSheet.setDefaultColumnStyle(i, i % 2 == 0 ? txtOneCellStyle : txtTwoCellStyle);
            dataSheet.setColumnWidth(i, 28 * 256);
        }
        return dataSheet;
    }

    /**
     * 设置单元格样式, 设置边框,并且设置字体居中
     * @param cellStyle 样式
     * @param center 是否居中
     */
    private static void setBorderAndCenter(CellStyle cellStyle, boolean center){
        // 添加顶部边框
        cellStyle.setBorderTop(BorderStyle.THIN);
        // 添加底部边框
        cellStyle.setBorderBottom(BorderStyle.THIN);
        // 添加左侧边框
        cellStyle.setBorderLeft(BorderStyle.THIN);
        // 添加右侧边框
        cellStyle.setBorderRight(BorderStyle.THIN);
        if (center){
            // 水平居中
            cellStyle.setAlignment(HorizontalAlignment.CENTER);
            // 垂直居中
            cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        }
    }

    ////////////////////////////////////// 创建数据表结束 /////////////////////////////////////

    ////////////////////////////////////// 设置样式开始 /////////////////////////////////////

    /**
     * 设置列为文本格式
     *
     * @param currentSheet 当前sheet
     * @param firstCol     开始列 ,从0开始
     * @param lastCol      结束刘,从0开始
     */
    public static void setColumnStyleToText(Sheet currentSheet, int firstCol, int lastCol) {
        Workbook workbook = currentSheet.getWorkbook();
        // 1. 设置单元格样式为【文本】
        CellStyle textStyle = workbook.createCellStyle();
        // 设置为不锁定
        textStyle.setLocked(false);
        // 创建一个简单的文本格式
        DataFormat format = workbook.createDataFormat();
        // "@" 表示文本格式
        textStyle.setDataFormat(format.getFormat("@"));
        // 将指定列的默认列样式设为文本(适用于新输入的内容)
        for (int col = firstCol; col <= lastCol; col++) {
            currentSheet.setDefaultColumnStyle(col, textStyle);
        }
    }

    ////////////////////////////////////// 设置样式结束 /////////////////////////////////////


    ////////////////////////////////////// 设置验证规则开始 /////////////////////////////////////

    /**
     * 设置所有类型车牌号验证规则(支持蓝牌、新能源)
     * 支持格式:
     * - 普通蓝牌:京A12345(7位)
     * - 新能源绿牌:京AB12345(8位)
     *
     * @param currentSheet 当前工作表
     * @param firstCol     开始列索引(从0开始)
     * @param lastCol      结束列索引(从0开始)
     */
    public static void setLicensePlateValidation(Sheet currentSheet, int firstCol, int lastCol) {
        DataValidationHelper helper = currentSheet.getDataValidationHelper();
        String colLetter = CellReference.convertNumToColString(firstCol);
        // 构建综合车牌验证公式
        String formula =
                "OR(" +
                        // 1. 普通蓝牌:7位,中文 + 字母 + 5位(数字/字母)
                        "AND(LEN(" + colLetter + "2)=7," +
                        "LENB(LEFT(" + colLetter + "2,1))=2," +  // 首字符中文
                        "AND(CODE(MID(" + colLetter + "2,2,1))>=65, CODE(MID(" + colLetter + "2,2,1))<=90)" +  // 第2位 A-Z
                        ")," +
                        // 2. 新能源绿牌:8位,中文 + 字母 + 6位(数字/字母)
                        "AND(LEN(" + colLetter + "2)=8," +
                        "LENB(LEFT(" + colLetter + "2,1))=2," +
                        "AND(CODE(MID(" + colLetter + "2,2,1))>=65, CODE(MID(" + colLetter + "2,2,1))<=90)" +
                        ")" +
                        ")";
        DataValidationConstraint constraint = helper.createCustomConstraint(formula);
        CellRangeAddressList addressList = new CellRangeAddressList(1, 100000, firstCol, lastCol);
        DataValidation validation = helper.createValidation(constraint, addressList);
        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        validation.createErrorBox(
                "无效车牌号", "请输入合法车牌号,例如:\n普通车:川A12345(7位)\n新能源:川AB12345(8位)"
        );
        validation.setShowErrorBox(true);
        currentSheet.addValidationData(validation);
    }

    /**
     * 设置手机号码验证规则
     *
     * @param currentSheet 当前工作表
     * @param firstCol     开始第几列,从0开始
     * @param lastCol      结束第几列,从0开始
     */
    public static void setMobileValidation(Sheet currentSheet, int firstCol, int lastCol) {
        DataValidationHelper dataValidationHelper = currentSheet.getDataValidationHelper();
        // 动态将列索引转换为列字母,例如 0 -> "A", 3 -> "D"
        String colLetter = CellReference.convertNumToColString(firstCol);
        // 构建 A1 风格公式:校验是否为 11 位、纯数字、且以 "1" 开头
        String phoneFormula =
                "AND(" +
                        "LEN(" + colLetter + "2)=11," +                    // 长度为11
                        "ISNUMBER(VALUE(" + colLetter + "2))," +           // 整个单元格内容可转为数字(隐含:全为数字)
                        "LEFT(" + colLetter + "2,1)=\"1\"" +               // 第一个字符是 "1"
                        ")";
        DataValidationConstraint phoneConstraint = dataValidationHelper.createCustomConstraint(phoneFormula);
        CellRangeAddressList addressListD = new CellRangeAddressList(1, 100000, firstCol, lastCol);
        DataValidation dvD = dataValidationHelper.createValidation(phoneConstraint, addressListD);
        dvD.setErrorStyle(DataValidation.ErrorStyle.STOP);
        dvD.createErrorBox("无效电话号码", "请输入合法的11位手机号码(以1开头,如13912345678)。");
        dvD.setShowErrorBox(true);
        currentSheet.addValidationData(dvD);
    }


    /**
     * 设置证件号码验证规则
     *
     * @param currentSheet 当前工作表
     * @param firstCol     开始第几列,从0开始
     * @param lastCol      结束第几列,从0开始
     */
    public static void setIdCardValidation(Sheet currentSheet, int firstCol, int lastCol) {
        DataValidationHelper dataValidationHelper = currentSheet.getDataValidationHelper();
        // 构造身份证校验公式(包含格式 + 出生日期 <= 当前日期)
        // 将列索引转换为列字母,例如 0 -> "A", 1 -> "B"
        String colLetter = CellReference.convertNumToColString(firstCol);
        // 构建 A1 风格的公式,从第2行开始(对应 Excel 第2行)
        // 注意:这里使用 "2" 表示第二行,Excel 会自动相对引用到每一行
        String idCardFormula =
                "AND(" +
                        "LEN(" + colLetter + "2)=18," +                                 // 长度为18
                        "ISNUMBER(VALUE(LEFT(" + colLetter + "2,17)))," +               // 前17位是数字
                        "OR(ISNUMBER(VALUE(RIGHT(" + colLetter + "2,1))),UPPER(RIGHT(" + colLetter + "2,1))=\"X\")," + // 最后一位是数字或X
                        "LEN(MID(" + colLetter + "2,7,8))=8," +                         // 出生年月日部分为8位
                        "ISNUMBER(VALUE(MID(" + colLetter + "2,7,8)))," +               // 出生年月日是数字
                        "DATEVALUE(TEXT(MID(" + colLetter + "2,7,8),\"0000-00-00\"))<=TODAY()" + // 出生日期 <= 当前日期
                        ")";
        DataValidationConstraint idCardConstraint = dataValidationHelper.createCustomConstraint(idCardFormula);
        CellRangeAddressList addressListC = new CellRangeAddressList(1, 100000, firstCol, lastCol);
        DataValidation dvC = dataValidationHelper.createValidation(idCardConstraint, addressListC);
        dvC.setErrorStyle(DataValidation.ErrorStyle.STOP);
        dvC.createErrorBox("无效身份证号", "请输入合法的18位身份证号(最后一位可以是X),且不能重复");
        dvC.setShowErrorBox(true);
        currentSheet.addValidationData(dvC);
    }

    /**
     * 设置姓名验证规则:仅允许 2-15 位中文字符(双字节),不能包含字母、数字、符号或不可见字符
     *
     * @param currentSheet 当前工作表
     * @param firstCol     开始列(从 0 开始)
     * @param lastCol      结束列(从 0 开始)
     */
    public static void setNameValidation(Sheet currentSheet, int firstCol, int lastCol) {
        DataValidationHelper helper = currentSheet.getDataValidationHelper();
        // 使用 A1 风格引用,假设 firstCol 对应列字母
        String colLetter = CellReference.convertNumToColString(firstCol);
        String nameFormula = "AND(LEN(" + colLetter + "2)>=2, "
                + "LEN(" + colLetter + "2)<=15, "
                + "LENB(" + colLetter + "2)=LEN(" + colLetter + "2)*2, "
                + "EXACT(" + colLetter + "2,CLEAN(" + colLetter + "2)))";
        // ✅ 使用字符串公式(唯一可用方式)
        DataValidationConstraint constraint = helper.createCustomConstraint(nameFormula);
        // 应用范围:第2行到第10000行(行索引从0开始,所以是1~10000)
        CellRangeAddressList addressList = new CellRangeAddressList(1, 100000, firstCol, lastCol);
        DataValidation validation = helper.createValidation(constraint, addressList);
        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        validation.createErrorBox("无效姓名", "请输入2-15位中文姓名,不能包含字母、数字、符号或空格。");
        validation.setShowErrorBox(true);
        currentSheet.addValidationData(validation);
    }

    /**
     * 设置下拉选择框
     *
     * @param currentSheet 当前工作表
     * @param dataSource   数据源
     * @param firstCol     下拉列表-开始列(从 0 开始)
     * @param lastCol      下拉列表-结束列(从 0 开始)
     */
    public static void setDropdownValidation(Sheet currentSheet, String dataSource, int firstCol, int lastCol) {
        // ==================== 设置名称列下拉列表 ====================
        DataValidationHelper dvHelper = currentSheet.getDataValidationHelper();
        DataValidationConstraint dvConstraint = dvHelper.createFormulaListConstraint(dataSource);
        // 作用范围: C列 (第2行到第10000行)
        CellRangeAddressList addressList = new CellRangeAddressList(1, 100000, firstCol, lastCol);
        DataValidation validation = dvHelper.createValidation(dvConstraint, addressList);
        validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
        validation.createErrorBox("无效输入", "请选择下拉列表中的有效值。");
        validation.setShowErrorBox(true);
        currentSheet.addValidationData(validation);
    }

    ////////////////////////////////////// 设置验证规则结束 /////////////////////////////////////

//    /**
//     * 设置数据联动
//     * 【注】:不支持多线程调用,当多个线程同时调用:createRow(j)、getRow(j)  就会导致:ConcurrentModificationException、NullPointerException
//     * @param currentSheet 当前工作表
//     * @param sourceColumn 源头列
//     * @param resultColumn 结果列
//     * @param dataSheetName 数据表名
//     */
//    public static void setDataLinkage(Sheet currentSheet,int sourceColumn,int resultColumn,String dataSheetName){
//        Workbook workbook = currentSheet.getWorkbook();
//        CellStyle lockedCellStyle = workbook.createCellStyle();
//        // 设置锁定
//        lockedCellStyle.setLocked(true);
//        // 单元格设置公式
//        for (int j = 1; j <= 100000 ; j++) {
//            // 获取对应行
//            Row formulaRow = currentSheet.getRow(j);
//            if (formulaRow == null) {
//                formulaRow = currentSheet.createRow(j);
//            }
//            // (设置对应列,从左到右,从0开始)
//            Cell formulaCell = formulaRow.createCell(resultColumn);
//            // 下标转字母
//            String sourceName = CellReference.convertNumToColString(sourceColumn);
//            // 正确写法:公式字符串不包含等号
//            formulaCell.setCellFormula("IFERROR(VLOOKUP("+ sourceName + (j+1) + ", "+ dataSheetName +"!$A$2:$B$100000, 2, FALSE), \"\")");
//            formulaCell.setCellStyle(lockedCellStyle);
//        }
//        boolean protect = currentSheet.getProtect();
//        if (!protect){
//            // 保护工作表
//            currentSheet.protectSheet("");
//        }
//    }


    /**
     * 设置数据联动(优化版)
     * 【注】:不支持多线程调用同一个 Sheet
     *
     * @param currentSheet  当前工作表
     * @param sourceColumn  源头列
     * @param resultColumn  结果列
     * @param dataSheetName 数据表名
     */
    public static void setDataLinkage(Sheet currentSheet, int sourceColumn, int resultColumn, String dataSheetName) {
        Workbook workbook = currentSheet.getWorkbook();

        // 复用 CellStyle
        CellStyle lockedCellStyle = workbook.createCellStyle();
        lockedCellStyle.setDataFormat(workbook.createDataFormat().getFormat("@"));
        lockedCellStyle.setLocked(true);

        // 预计算公式字符串
        String sourceName = CellReference.convertNumToColString(sourceColumn);
        String formulaPrefix = "IFERROR(VLOOKUP(" + sourceName;
        String formulaSuffix = ", " + dataSheetName + "!$A$2:$B$100000, 2, FALSE), \"\")";

        // 预创建行(可选)
        final int startRow = 1, endRow = 100000;
        Row[] rows = new Row[endRow - startRow + 1];

        for (int j = startRow; j <= endRow; j++) {
            Row row = currentSheet.getRow(j);
            if (row == null) {
                row = currentSheet.createRow(j);
            }
            rows[j - startRow] = row;

            // 设置公式
            Cell cell = row.createCell(resultColumn);
            // 先强制指定为字符串
            cell.setCellValue("");
            cell.setCellFormula(formulaPrefix + (j + 1) + formulaSuffix);
            cell.setCellStyle(lockedCellStyle);
        }

        // 由调用方统一保护,避免重复
        if (!currentSheet.getProtect()) {
            currentSheet.protectSheet("");
        }
    }


    /**
     * 获取总行数(包含标题行)
     *
     * @param inputStream
     * @return
     * @throws IOException
     */
    public static long getTotalRows(InputStream inputStream) throws IOException {
        try (Workbook workbook = WorkbookFactory.create(inputStream)) {
            // 默认读第一个 sheet
            Sheet sheet = workbook.getSheetAt(0);
            long rowCount = 0;

            // 遍历所有物理存在的行(跳过空行判断,只计最大行号)
            for (Row row : sheet) {
                // 判断是否为空行(可选:更精确统计有效数据行)
                if (isRowEmpty(row)) {
                    continue; // 如果你想跳过空行,取消这行注释
                }
                rowCount++;
            }
            // 或者直接获取最后一行的行号(包含空行)
            // long rowCount = sheet.getLastRowNum() + 1; // 包含标题行
            return rowCount;
        }
    }

    /**
     * 判断某行是否为空(所有单元格都为空)
     */
    private static boolean isRowEmpty(Row row) {
        if (row == null) {
            return true;
        }
        for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
            Cell cell = row.getCell(c);
            if (cell != null && cell.getCellType() != CellType.BLANK) {
                return false;
            }
        }
        return true;
    }

}
相关推荐
毕设源码-赖学姐7 小时前
【开题答辩全过程】以 基于Android的校园快递互助APP为例,包含答辩的问题和答案
java·eclipse
damo017 小时前
stripe 支付对接
java·stripe
麦麦鸡腿堡8 小时前
Java的单例设计模式-饿汉式
java·开发语言·设计模式
假客套8 小时前
Request method ‘POST‘ not supported,问题分析和解决
java
傻童:CPU8 小时前
C语言需要掌握的基础知识点之前缀和
java·c语言·算法
爱吃山竹的大肚肚8 小时前
@Valid校验 -(Spring 默认不支持直接校验 List<@Valid Entity>,需用包装类或手动校验。)
java·开发语言
雨夜之寂9 小时前
mcp java实战 第一章-第一节-MCP协议简介.md
java·后端
皮皮林5519 小时前
蚂蚁又开源了一个顶级 Java 项目!
java
吹晚风吧10 小时前
spring是如何解决循环依赖的(二级缓存不行吗)?
java·spring·循环依赖·三级缓存