文章目录
-
- [一、为什么选择 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;
}
}