1. 项目概述:为什么我们需要“自适应”的Excel导出?
做后端开发,尤其是涉及报表导出的场景,用EasyExcel的朋友肯定不少。它确实方便,几行代码就能把数据变成Excel文件。但不知道你有没有遇到过这样的尴尬:导出的Excel文件,打开一看,有的单元格文字被挤成了“#”,有的又因为内容太少留了一大片空白,用户还得手动去调整列宽、行高,体验非常糟糕。
这就是我们今天要解决的痛点:让EasyExcel导出的Excel文件,在打开时就能根据内容自动调整行高和列宽,实现“所见即所得”的完美效果。这个需求听起来简单,但EasyExcel本身并没有提供一键“自适应”的开关。它默认的列宽策略(ColumnWidth)和行高策略(ContentRowHeight)是固定的,不会根据你写入的内容动态变化。
所以,“保姆级教程”这个前缀,意味着我们要从原理出发,手把手带你绕过EasyExcel的“舒适区”,深入到Apache POI的底层(EasyExcel基于POI封装),去实现一个真正智能、好用的自适应导出方案。这不仅仅是调个参数,更是对POI SXSSFSheet操作的一次深度实践。接下来,我会结合我踩过的坑和优化经验,把完整的实现路径、核心代码以及那些官方文档里不会写的细节,毫无保留地分享给你。
2. 核心原理与方案选型:为什么不能直接设置?
在动手之前,我们必须先理解为什么EasyExcel不直接提供这个功能,以及有哪些可能的实现路径。
2.1 EasyExcel的默认行为与局限
EasyExcel的设计哲学是简单、高效,特别是在处理大数据量时,它默认使用SXSSFWorkbook(流式导出),只将部分数据保留在内存中。这种模式下,它在写入数据时,无法预知整列或整行的最终内容长度。如果它在每写一行时就尝试计算整张表的最佳尺寸,会严重破坏流式导出的性能优势,甚至可能导致内存溢出。因此,它把单元格样式的控制权(包括宽度和高度)更多地交给了开发者通过@ExcelProperty注解或WriteCellStyle来预设。
2.2 可行的技术方案对比
要实现自适应,我们必须在数据写入完成后,对已经生成的Sheet进行“后处理”。主要有三种思路:
- 遍历计算法(推荐):在所有数据写入完成后,遍历Sheet中的每一行、每一列,计算每个单元格内容的像素宽度/高度,然后取该列最宽值、该行最高值,设置为列宽和行高。这是最精准、最通用的方法。
- 估算法:根据中文字符、英文字符、数字的大致比例,估算一个固定系数。例如,设定一个基础列宽,然后根据字段内容的预估最大长度(如数据库字段
varchar(255))来设置。这种方法简单但不精确,容易留白或溢出。 - 模板预设法:先创建一个“标准”Excel模板,在模板中手动调整好列宽行高,导出时加载这个模板。这适用于列结构固定的报表,但失去了动态性。
为什么我们选择“遍历计算法”?因为它能真正响应数据内容,实现真正的“自适应”。无论内容是用户填写的长文本,还是从数据库查出的不定长字符串,都能得到最合适的展示。虽然它需要额外的遍历计算开销,但对于一次性的导出操作,这点开销在用户体验提升面前是完全可以接受的。接下来,我们就基于这个方案进行深度拆解。
3. 核心工具类:自适应宽度与高度计算器
这是整个功能的核心。我们将创建一个工具类,它接收一个SXSSFSheet对象,然后对其执行自适应调整。
3.1 自适应列宽计算器实现
列宽的计算是难点。Excel的列宽单位不是像素,也不是字符数,而是一个特殊的单位。POI提供了Sheet的setColumnWidth(int columnIndex, int width)方法,其中的width是1/256个字符宽度。我们需要把字符串的像素宽度转换成这个单位。
一个关键点是:不同字体、不同字号下,字符宽度不同。我们需要获取工作簿中使用的字体来精确计算。以下是核心代码实现与详解:
import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import java.awt.*; import java.util.HashMap; import java.util.Map; public class ExcelAutoSizeUtil { /** * 自动设置列宽(核心方法) * @param sheet 需要调整的Sheet * @param extraWidth 额外增加的宽度(单位:字符数),用于给单元格一些边距,避免显得拥挤。 */ public static void autoSizeColumns(SXSSFSheet sheet, float extraWidth) { if (sheet == null) return; SXSSFWorkbook workbook = sheet.getWorkbook(); // 用于缓存每列计算出的最大宽度,避免重复计算 Map<Integer, Integer> maxWidthMap = new HashMap<>(); // 获取默认字体,用于计算字符串宽度 Font defaultFont = workbook.getFontAt((short) 0); FontMetrics fontMetrics = getFontMetrics(defaultFont); // 遍历所有有数据的行 for (Row row : sheet) { if (row == null) continue; for (Cell cell : row) { if (cell == null) continue; int columnIndex = cell.getColumnIndex(); // 获取单元格内容字符串 String cellValue = getCellValueAsString(cell, workbook); if (cellValue == null || cellValue.isEmpty()) { continue; } // 计算字符串的像素宽度 int stringWidthPx = fontMetrics.stringWidth(cellValue); // 关键转换:将像素宽度转换为Excel的列宽单位(1/256字符宽) // 这里是一个经验公式:像素宽度 / 单个字符平均像素宽度 * 256 + 额外宽度补偿 // 单个字符平均像素宽度可以通过测量“汉”或“W”等宽字符获得,这里简化处理 int cellWidthUnits = (int) ((stringWidthPx / (fontMetrics.charWidth('W') * 1.0)) * 256); cellWidthUnits += (int)(extraWidth * 256); // 加上额外边距 // 更新该列的最大宽度 int currentMaxWidth = maxWidthMap.getOrDefault(columnIndex, 0); if (cellWidthUnits > currentMaxWidth) { // Excel列宽有上限(255*256),需要防止溢出 maxWidthMap.put(columnIndex, Math.min(cellWidthUnits, 255 * 256)); } } } // 遍历缓存,将计算出的最大宽度应用到Sheet的每一列 for (Map.Entry<Integer, Integer> entry : maxWidthMap.entrySet()) { int columnIndex = entry.getKey(); int columnWidth = entry.getValue(); // 设置一个最小宽度,避免内容太短时列宽为0 columnWidth = Math.max(columnWidth, 8 * 256); // 最小8个字符宽 sheet.setColumnWidth(columnIndex, columnWidth); } } /** * 获取AWT环境下的FontMetrics,用于精确计算字符串像素宽度。 * 注意:在无头环境(Headless,如Linux服务器)下,需要特殊处理。 */ private static FontMetrics getFontMetrics(Font font) { // 这是一个简化示例。在生产环境中,你需要处理Headless环境。 // 通常可以创建一个虚拟的BufferedImage来获取Graphics2D和FontMetrics。 Canvas canvas = new Canvas(); return canvas.getFontMetrics(font); } /** * 将Cell内容转换为字符串,处理不同类型(数字、日期、公式等) */ private static String getCellValueAsString(Cell cell, Workbook workbook) { if (cell == null) return ""; switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { // 日期格式按原格式字符串化,或使用默认格式 DataFormatter formatter = new DataFormatter(); return formatter.formatCellValue(cell); } // 数字类型,防止科学计数法,保留适当精度 return String.valueOf(cell.getNumericCellValue()); case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); case FORMULA: // 公式单元格,尝试计算值,如果计算失败则返回公式字符串 try { return String.valueOf(cell.getNumericCellValue()); } catch (Exception e) { return cell.getCellFormula(); } case BLANK: return ""; default: return ""; } } }重要提示(踩坑经验):
getFontMetrics方法在服务器(尤其是无图形界面的Linux服务器)上运行时,可能会抛出HeadlessException。这是实现自适应列宽最常见的坑。解决方案是:要么在服务器环境中安装图形库(不推荐),要么放弃精确像素计算,采用一种更通用的“字符数估算”方法。我们接下来会提供一个更健壮的、无头环境友好的版本。
3.2 无头环境友好的列宽估算方案
既然精确计算像素在服务器端有障碍,我们可以采用一个业界常用的经验法则:一个中文汉字大致相当于2个英文字符的宽度。我们可以通过计算单元格字符串的“有效字符数”来估算列宽。
/** * 自动设置列宽(无头环境/服务器端友好版) * @param sheet 需要调整的Sheet * @param extraWidth 额外宽度(字符数) */ public static void autoSizeColumnsServerFriendly(SXSSFSheet sheet, float extraWidth) { if (sheet == null) return; Map<Integer, Integer> maxWidthMap = new HashMap<>(); for (Row row : sheet) { if (row == null) continue; for (Cell cell : row) { if (cell == null) continue; int columnIndex = cell.getColumnIndex(); String cellValue = getCellValueAsString(cell, sheet.getWorkbook()); if (cellValue == null || cellValue.isEmpty()) continue; // 核心估算逻辑:计算字符串的“显示长度” int displayLength = calculateDisplayLength(cellValue); // 转换为Excel列宽单位。经验值:一个显示长度单位约等于256个列宽单位。 // 额外宽度也按比例加上。 int cellWidthUnits = (displayLength * 256) + (int)(extraWidth * 256); int currentMax = maxWidthMap.getOrDefault(columnIndex, 0); if (cellWidthUnits > currentMax) { // 同样设置上限 maxWidthMap.put(columnIndex, Math.min(cellWidthUnits, 255 * 256)); } } } for (Map.Entry<Integer, Integer> entry : maxWidthMap.entrySet()) { int columnIndex = entry.getKey(); int columnWidth = entry.getValue(); columnWidth = Math.max(columnWidth, 10 * 256); // 设置一个更合理的最小宽度 sheet.setColumnWidth(columnIndex, columnWidth); } } /** * 计算字符串在Excel中的大致显示长度。 * 规则:ASCII字符(英文字母、数字、标点)计为0.5,中文(及其他全角字符)计为1。 * 这是一个简化模型,但实践中效果很好。 */ private static int calculateDisplayLength(String str) { if (str == null) return 0; int length = 0; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); // 判断是否为ASCII字符(范围0-127) if (c <= 127) { length += 1; // ASCII字符占位较窄,这里计为1个单位,后续会整体调整比例 } else { length += 2; // 非ASCII字符(如中文)占位较宽,计为2个单位 } } // 因为我们将一个ASCII字符的宽度单位视为1,但实际列宽单位更细, // 所以返回总长度即可,系数在调用处统一乘。 return length; }这个方案牺牲了一点精确性,但获得了极强的环境兼容性,是生产环境的首选。
3.3 自适应行高计算器实现
行高的实现比列宽简单,因为Excel的行高单位是“点”(point),与字体大小直接相关。我们通常希望行高能根据单元格内文本的行数(是否换行)来自适应。
/** * 自动设置行高 * @param sheet 需要调整的Sheet * @param extraHeight 额外增加的高度(点数),使文字不顶格。 */ public static void autoSizeRows(SXSSFSheet sheet, float extraHeight) { if (sheet == null) return; // 获取默认行高(点数) float defaultRowHeightInPoints = sheet.getDefaultRowHeightInPoints(); for (Row row : sheet) { if (row == null) continue; // 初始化为默认行高 float maxHeightInPoints = defaultRowHeightInPoints; // 遍历该行所有单元格,找出需要最高行高的那个 for (Cell cell : row) { if (cell == null) continue; CellStyle cellStyle = cell.getCellStyle(); // 获取该单元格使用的字体 Font font = sheet.getWorkbook().getFontAt(cellStyle.getFontIndex()); String cellValue = getCellValueAsString(cell, sheet.getWorkbook()); if (cellValue == null || cellValue.isEmpty()) continue; // 关键:判断单元格内容是否换行。 // 1. 单元格样式设置了自动换行(wrapText) // 2. 字符串本身包含换行符“\n” boolean wrapText = cellStyle.getWrapText(); boolean hasNewLine = cellValue.contains("\n"); if (wrapText || hasNewLine) { // 对于换行文本,需要估算行数。 // 这里我们做一个简化估算:每行大约能显示多少个字符。 // 首先,需要知道列宽(字符数)。这有点循环依赖,因为列宽可能刚被我们调整过。 // 一个更简单实用的策略:根据字体大小和内容长度,设置一个固定的多行行高。 // 例如,每增加一个换行符或每超过一定字符长度,就增加一行的高度。 int estimatedLines = estimateLinesOfText(cellValue, font, sheet.getColumnWidth(cell.getColumnIndex())); // 每行行高大致等于字体大小(点数)加上一些间距 float lineHeight = font.getFontHeightInPoints() + 2; float neededHeight = estimatedLines * lineHeight; if (neededHeight > maxHeightInPoints) { maxHeightInPoints = neededHeight; } } else { // 对于不换行的单行文本,行高至少等于字体高度 float fontHeight = font.getFontHeightInPoints(); if (fontHeight > maxHeightInPoints) { maxHeightInPoints = fontHeight; } } } // 应用计算出的行高,并加上额外边距 row.setHeightInPoints(maxHeightInPoints + extraHeight); } } /** * 估算文本在给定列宽和字体下的行数(简化版) * 注意:这是一个非常粗略的估算,精确计算需要复杂的字体度量。 * 生产环境可根据实际情况调整算法或接受一定误差。 */ private static int estimateLinesOfText(String text, Font font, int columnWidthInUnits) { // 将列宽单位转换为大概的字符数。 // 假设一个标准英文字符宽度为256单位,则列宽字符数 ≈ columnWidthInUnits / 256 double approxCharWidth = 256.0; int charsPerLine = (int) (columnWidthInUnits / approxCharWidth); if (charsPerLine <= 0) charsPerLine = 50; // 给个默认值 // 计算文本总字符数(按之前定义的显示长度) int totalDisplayLength = calculateDisplayLength(text); // 计算基础行数 int lines = (int) Math.ceil(totalDisplayLength * 1.0 / charsPerLine); // 如果文本本身有换行符,行数要加上换行符的数量 long newLineCount = text.chars().filter(ch -> ch == '\n').count(); lines += newLineCount; return Math.max(lines, 1); // 至少一行 }实操心得:行高的自适应比列宽更“模糊”,因为文本换行不仅取决于列宽,还取决于具体的字体和渲染引擎。上述
estimateLinesOfText方法是一个启发式算法,在大多数情况下能提供可接受的结果。如果对行高精度要求极高(如生成用于打印的精密报表),可能需要考虑更复杂的方案,或者引导用户使用Excel自身的“自动调整行高”功能(但这需要手动触发)。
4. 与EasyExcel集成:在写入完成后触发自适应
有了核心工具类,我们需要将其集成到EasyExcel的写入流程中。EasyExcel提供了SheetWriteHandler这个拦截器接口,允许我们在Sheet写入完成后执行自定义操作。这是最理想的集成点。
4.1 实现自定义SheetWriteHandler
import com.alibaba.excel.write.handler.SheetWriteHandler; import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext; import lombok.extern.slf4j.Slf4j; import org.apache.poi.xssf.streaming.SXSSFSheet; @Slf4j public class AutoSizeSheetWriteHandler implements SheetWriteHandler { // 额外宽度(字符数) private float extraColumnWidth; // 额外高度(点数) private float extraRowHeight; public AutoSizeSheetWriteHandler() { this(1.5f, 4.0f); // 提供默认值 } public AutoSizeSheetWriteHandler(float extraColumnWidth, float extraRowHeight) { this.extraColumnWidth = extraColumnWidth; this.extraRowHeight = extraRowHeight; } @Override public void afterSheetCreate(SheetWriteHandlerContext context) { // 这个阶段Sheet已创建,但数据未写入,不处理 } @Override public void afterSheetDispose(SheetWriteHandlerContext context) { // 关键:在所有数据写入Sheet之后,文件写出之前,执行自适应调整 log.info("开始执行Excel自适应列宽行高调整..."); try { // 获取POI原生的SXSSFSheet对象 SXSSFSheet sheet = (SXSSFSheet) context.getWriteSheetHolder().getSheet(); // 1. 先调整列宽(因为行高估算可能依赖列宽) ExcelAutoSizeUtil.autoSizeColumnsServerFriendly(sheet, extraColumnWidth); // 2. 再调整行高 ExcelAutoSizeUtil.autoSizeRows(sheet, extraRowHeight); log.info("Excel自适应调整完成。"); } catch (Exception e) { log.error("Excel自适应调整失败,将导出默认格式文件。", e); // 此处不要抛出异常,以免影响主流程。记录日志后,导出未调整的文件。 } } }4.2 在EasyExcel写入器中注册处理器
现在,在调用EasyExcel进行写入时,只需将这个SheetWriteHandler注册进去即可。
// 准备数据 List<YourDataModel> dataList = ...; // 设置导出文件路径 String fileName = "自适应报表.xlsx"; // 使用EasyExcel的写入器,并注册我们的自适应处理器 EasyExcel.write(fileName, YourDataModel.class) .registerWriteHandler(new AutoSizeSheetWriteHandler(2.0f, 6.0f)) // 可以自定义边距 // 可以注册其他处理器,如样式处理器 // .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // EasyExcel自带的简单列宽策略,但不够智能,建议不用或作为备选 .sheet("数据Sheet") .doWrite(dataList);关键点解释:
extraColumnWidth:我设置为2.0f,意味着在计算出的最合适列宽基础上,再增加大约2个字符的宽度,这样单元格内容不会紧贴边框,视觉效果更舒适。extraRowHeight:设置为6.0f点,在字体高度基础上增加6点(约2毫米)的行高,确保文字上下有足够的留白。- 执行顺序:一定要先调整列宽,再调整行高。因为行高的估算(对于换行文本)依赖于最终的列宽值。
5. 高级优化与生产级注意事项
上面的代码已经可以解决大部分问题,但在生产环境中,我们还需要考虑更多细节。
5.1 性能优化:大数据量下的处理策略
当导出数据行数上万时,遍历所有单元格计算宽度可能会比较耗时。我们可以进行以下优化:
- 抽样计算:对于数据量极大的列(如万行以上),可以不必遍历每一行。可以随机抽取一定比例(如前1000行,中间1000行,最后1000行)的样本进行计算,以样本中的最大宽度作为列宽。这牺牲了极小概率的精度,换来了显著的性能提升。
- 缓存字体度量:
FontMetrics的创建和获取有一定开销。如果使用精确计算法,应该在整个工作簿级别缓存字体及其对应的FontMetrics对象。 - 异步或分批处理:对于超大数据量(如数十万行),自适应调整本身可能成为瓶颈。可以考虑将“写出文件”和“自适应调整”分离。先快速写出一个未调整的临时文件,然后通过一个后台任务或第二步处理来读取这个文件并进行自适应调整,最后提供下载。
5.2 样式兼容性处理
我们的自适应处理器可能会和其他样式处理器(比如设置单元格背景色、边框、字体加粗等)产生冲突。需要注意:
- 执行顺序:EasyExcel的
WriteHandler执行有顺序。通常,样式处理器(CellWriteHandler)在SheetWriteHandler之前执行。这意味着我们的自适应调整是最后一步,会覆盖之前设置的固定列宽行高。这通常是我们期望的。 - 合并单元格:如果导出的表格有合并单元格,自适应逻辑会变得复杂。因为一个合并区域占据多列多行。我们的遍历计算法在遇到合并单元格时,需要特殊处理:通常只需考虑合并区域左上角那个单元格的内容,但宽度要应用到合并的所有列上。POI提供了
Sheet.getMergedRegions()方法来获取所有合并区域,在计算列宽时需要跳过被合并的内部单元格。
5.3 处理特殊内容与格式
- 超长文本:如果一个单元格包含极长的文本(如一段描述),即使我们设置了自动换行,计算出的行高可能会非常高,导致一行占据大半页。可以设置一个行高上限(例如100点),超过此高度则强制截断文本或保持默认行高,并添加注释。
- 公式单元格:公式单元格的值在写入时可能是公式字符串(如
=A1+B1),而不是计算结果。我们的getCellValueAsString方法尝试获取计算值,但如果公式依赖其他未计算的值,可能会失败。对于公式单元格,自适应其宽度可能比较困难,一个稳妥的做法是给它一个预设的较宽宽度,或者忽略其自适应。 - 图片或其他对象:如果单元格中插入了图片,POI的
Cell类型是CELL_TYPE_BLANK,我们的方法会跳过。图片的尺寸需要单独处理,这超出了本文范围,通常需要根据图片大小来设置行高列宽。
6. 常见问题排查与实战技巧
在实际使用中,你可能会遇到下面这些问题,这里是我的排查思路和解决方案。
6.1 问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
导出的文件列宽仍然很窄,内容显示为### | 1. 自适应处理器未生效。 2. 计算出的列宽单位超过Excel上限(65535)。 3. 中文字符宽度计算严重偏差。 | 1. 检查SheetWriteHandler是否被正确注册到EasyExcel.write()链中。2. 在 autoSizeColumns方法中,确保对columnWidth进行了上限检查(Math.min(cellWidthUnits, 255 * 256))。3. 切换到“无头环境友好版”的估算方法,并调整 calculateDisplayLength中的宽度系数(如将中文计为2,英文计为1)。 |
服务器(Linux)上运行报HeadlessException | 使用了依赖AWT图形环境的精确像素计算。 | 务必使用autoSizeColumnsServerFriendly方法,它不依赖FontMetrics。 |
| 行高调整后,多行文本仍然显示不全 | 行高估算函数estimateLinesOfText不准确,低估了行数。 | 增大charsPerLine的估算值(即认为每行能显示的字符数更少),或直接增加extraRowHeight的值。更激进的方法是,用(字体大小 * 行数 * 一个放大系数,如1.5)来设置行高。 |
| 自适应后,表头(第一行)的列宽不合适 | 表头文字通常比数据内容短,遍历计算时可能以数据列为准,导致表头看起来空旷。 | 在遍历计算列宽时,强制将表头行纳入计算。可以在工具类中加一个参数boolean includeHeader,或者在遍历前,先单独处理表头行,确保其宽度也被考虑在内。 |
| 性能慢,导出大量数据时卡顿 | 遍历了所有单元格,且计算复杂。 | 实施5.1节的性能优化策略,特别是抽样计算。对于超过1万行的数据,只计算前、中、后各5%的行,效果和全量计算相差无几,但速度提升巨大。 |
| 设置了固定列宽的列也被自适应调整了 | 自适应处理器覆盖了之前通过@ColumnWidth注解或样式设置的固定宽度。 | 如果希望某些列保持固定宽度,可以在自适应逻辑中增加“排除列”列表。在计算列宽前,检查列索引是否在排除列表中,如果是,则跳过该列的计算。 |
6.2 我的独家调参心得
extraColumnWidth(额外列宽):这个参数非常关键。我经过大量测试发现,1.5到3.0是一个黄金区间。小于1.5,单元格看起来会有点挤;大于3.0,表格又会显得松散。对于以中文为主的报表,建议从2.0开始尝试。extraRowHeight(额外行高):这个参数影响阅读的舒适度。默认字体(11号)下,4.0到6.0点比较合适。如果你使用了更大的表头字体(如14号加粗),可能需要为表头行单独设置更大的额外行高。- 最小列宽:一定要设置一个合理的最小列宽(如
8 * 256或10 * 256),防止内容极短的列(如“性别”、“是/否”)变得几乎看不见。 - 先列宽后行高:这个顺序不能错。我早期版本曾反过来,导致换行文本的行高估算严重错误,因为当时依据的是调整前的、过窄的列宽。
6.3 一个更健壮的生产级工具类片段
结合以上所有经验,这里给出一个增强版的列宽计算入口方法,它集成了抽样、排除列、最小最大宽度限制等功能。
/** * 增强版自动列宽调整(生产环境推荐) * @param sheet 工作表 * @param extraWidth 额外宽度 * @param excludedColumns 需要排除自适应、保持原样的列索引集合 * @param sampleRate 抽样率,0.1表示抽样10%的行进行计算。<=0 或 >=1 表示全量计算。 */ public static void autoSizeColumnsEnhanced(SXSSFSheet sheet, float extraWidth, Set<Integer> excludedColumns, double sampleRate) { // ... 参数校验 Map<Integer, Integer> maxWidthMap = new HashMap<>(); int lastRowNum = sheet.getLastRowNum(); if (lastRowNum <= 0) return; // 决定需要计算的行 List<Integer> rowsToProcess = new ArrayList<>(); if (sampleRate > 0 && sampleRate < 1) { // 抽样逻辑:取首、中、尾各一部分行 int sampleSize = (int) (lastRowNum * sampleRate); int start = Math.max(0, sampleSize); int middle = lastRowNum / 2; int end = Math.max(lastRowNum - sampleSize, middle + 1); // 添加一些固定行,如第0行(表头) rowsToProcess.add(0); for (int i = start; i < start + sampleSize && i <= lastRowNum; i++) rowsToProcess.add(i); for (int i = middle; i < middle + sampleSize/2 && i <= lastRowNum; i++) rowsToProcess.add(i); for (int i = end; i <= lastRowNum; i++) rowsToProcess.add(i); } else { // 全量处理 for (int i = 0; i <= lastRowNum; i++) rowsToProcess.add(i); } // 遍历选中的行进行计算 for (int rowIndex : rowsToProcess) { Row row = sheet.getRow(rowIndex); if (row == null) continue; // ... 遍历单元格计算逻辑,跳过excludedColumns中的列 ... } // 应用宽度,并确保在最小和最大范围内 int minWidth = 10 * 256; // 最小10字符 int maxWidth = 100 * 256; // 最大100字符 for (Map.Entry<Integer, Integer> entry : maxWidthMap.entrySet()) { int colIdx = entry.getKey(); if (excludedColumns != null && excludedColumns.contains(colIdx)) { continue; // 跳过排除列 } int width = Math.max(minWidth, Math.min(maxWidth, entry.getValue())); sheet.setColumnWidth(colIdx, width); } }这个增强版工具提供了更大的灵活性和可控性,能够应对更复杂的生产场景。