基于 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;
    }

}
相关推荐
qq_2546744118 小时前
Cisco Nexus 9504交换机上
java·linux·服务器
咕噜企业分发小米18 小时前
腾讯云在多云管理工具上如何实现合规性要求?
java·云计算·腾讯云
invicinble18 小时前
关于对后端开发工程师,在项目层面的基本需求与进阶方向
java
懒鸟一枚18 小时前
Java17新特性详解
java
戌中横18 小时前
JavaScript 对象
java·开发语言·javascript
crossaspeed18 小时前
面向对象的三大特征和反射(八股)
java·开发语言
zfj32119 小时前
java synchronized关键字用法和底层原理
java·开发语言·轻量级锁·重量级锁·偏向锁·线程同步
梵高的代码色盘19 小时前
互联网大厂Java求职面试实录与技术深度解析
java·spring·缓存·微服务·面试·互联网大厂·技术深度
E_ICEBLUE19 小时前
Excel vs CSV:在系统数据处理中该如何选择?
java·excel·csv·格式转换
郑州光合科技余经理20 小时前
同城020系统架构实战:中台化设计与部署
java·大数据·开发语言·后端·系统架构·uni-app·php