Java 基于 POI 模板 Excel 导出工具类 双数据源 + 自动合并单元格 + 自适应行高 完整实战

Java基于POI模板Excel导出工具类 双数据源+自动合并单元格+自适应行高 完整实战

实现效果为

摘要

在日常Java Web项目开发中,Excel导出是高频通用需求,传统硬编码创建Excel样式繁琐、格式难以统一、多数据源拼接复杂、长文本排版错乱、合并单元格需要手动处理。本文基于Apache POI 实现一套通用版Excel模板导出工具,支持双列表数据源填充、自定义多级字体样式、单元格自动合并、内容自动换行、自适应行高、多余列隐藏、尾部空行自动清理、浏览器下载文件名兼容乱码、日期数字空值统一处理,通用所有业务报表导出场景,开箱即用,可直接复制到项目使用。
关键字:POI、Excel导出、模板导出、双数据源、合并单元格、自适应行高、JavaWeb

一、技术环境与依赖

1. 技术栈

  1. JDK 1.8+
  2. Apache POI(处理.xlsx新版Excel)
  3. FastJSON 用于实体对象转Map反射取值
  4. SpringBoot Web 接收前端请求、浏览器文件响应下载

2. Maven依赖

xml 复制代码
<!-- POI 核心 + xlsx扩展包 -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>

<!-- fastjson 实体转Map -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

二、工具类完整源码

代码完全保留你原生所有逻辑、方法、重载、修复的bug、样式、自动行高、合并规则,仅删除所有业务化文字(党校、分校、基地、班次全部清除,注释通用化),可直接复制。

java 复制代码
package cn.net.att.module.system.util.attachment;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * 通用Excel模板导出工具类
 * 支持双数据源填充、自定义字体样式、自动合并单元格、自动换行、自适应行高、空行清理
 * 基于POI实现.xlsx格式导出,通用所有业务报表场景
 */
public class ExcelUtilsNewFBTJNEWS {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 模板文件服务器存放路径,可根据自身项目修改
//    private static final String TEMPLATE_PATH_FB_TJ = "C:\\Users\\Administrator\\Desktop\\newfbtj.xlsx";
    private static final String TEMPLATE_PATH_FB_TJ = "/data/datatow/xls/newfbtj.xlsx";

    /**
     * 通用导出主方法,支持两个独立数据源填充
     * @param title 导出报表标题&文件名
     * @param rowName 表头数组
     * @param fieldName 实体对象属性字段数组,与表头一一对应
     * @param dataList 主数据源集合
     * @param dataListFx 第二个附加数据源集合,可为空、空集合
     * @param totalMap 合计统计数据Map,可为空
     * @param response 浏览器响应对象,用于文件下载
     */
    public static <T> void exportExcelxlsfbtj(String title, String[] rowName, String[] fieldName,
                                              List<T> dataList, List<T> dataListFx,
                                              Map<String, Object> totalMap,
                                              HttpServletResponse response) throws IOException {

        // 标题为空默认赋值
        if (title == null || title.trim().isEmpty()) {
            title = "Excel导出数据";
        }
        // 过滤文件名非法特殊字符
        title = title.replace(":", ":").replace("/", "-").replace("\\", "-");

        XSSFWorkbook workbook = null;
        // 加载本地Excel模板文件
        try (FileInputStream fis = new FileInputStream(TEMPLATE_PATH_FB_TJ)) {
            workbook = new XSSFWorkbook(fis);
        } catch (IOException e) {
            throw new RuntimeException("模板文件加载失败:" + e.getMessage());
        }

        XSSFSheet sheet = (XSSFSheet) workbook.getSheetAt(0);
        sheet.getDataValidations().clear();

        int columnCount = rowName.length;
        // 隐藏模板多余无用列
        clearExtraColumns(sheet, columnCount);

        int currentRow = 0;

        // 第一行:大标题行
        currentRow = addTitleRow(sheet, title, columnCount, workbook, currentRow);

        // 第二行:第一部分区域标题
        currentRow = addSectionTitleRow(sheet, "一、数据统计一", columnCount, workbook, currentRow);

        // 第三行:表头行
        currentRow = addHeaderRow(sheet, rowName, workbook, currentRow);

        // 填充主数据源数据
        float templateRowHeight = 25;
        currentRow = fillDataWithStyle(sheet, fieldName, dataList, workbook, currentRow, templateRowHeight);

        // 第二个附加数据源不为空时追加渲染
        if (dataListFx != null && !dataListFx.isEmpty()) {
            // 追加区域标题
            currentRow = addSectionTitleRow(sheet, "二、数据统计二", columnCount, workbook, currentRow);
            // 填充第二个数据源数据
            currentRow = fillDataWithStyle(sheet, fieldName, dataListFx, workbook, currentRow, templateRowHeight);
        }

        // 自动删除表格末尾多余空行
        removeLastEmptyRow(sheet);

        // 设置浏览器下载响应参数
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        String fileName = URLEncoder.encode(title + ".xlsx", "UTF-8");
        // 解决中文文件名乱码
        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);

        // 流输出文件到浏览器
        try (OutputStream os = response.getOutputStream()) {
            workbook.write(os);
            os.flush();
        } finally {
            if (workbook != null) {
                workbook.close();
            }
        }
    }

    /**
     * 创建报表大标题行
     * 整行合并、居中、无边框、指定大字号字体
     */
    private static int addTitleRow(Sheet sheet, String title, int columnCount, Workbook workbook, int rowNum) {
        Row titleRow = sheet.createRow(rowNum);

        CellStyle titleStyle = workbook.createCellStyle();
        Font titleFont = workbook.createFont();
        titleFont.setFontName("方正小标宋简体");
        titleFont.setFontHeightInPoints((short) 30);
        titleStyle.setFont(titleFont);
        titleStyle.setAlignment(CellStyle.ALIGN_CENTER);
        titleStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        // 全部无边框
        titleStyle.setBorderTop(CellStyle.BORDER_NONE);
        titleStyle.setBorderBottom(CellStyle.BORDER_NONE);
        titleStyle.setBorderLeft(CellStyle.BORDER_NONE);
        titleStyle.setBorderRight(CellStyle.BORDER_NONE);

        // 初始化整行单元格
        for (int col = 0; col < columnCount; col++) {
            Cell cell = titleRow.createCell(col);
            cell.setCellStyle(titleStyle);
        }

        // 整行单元格合并
        sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, columnCount - 1));

        // 赋值标题文本
        titleRow.getCell(0).setCellValue(title);
        titleRow.setHeightInPoints(50);

        return rowNum + 1;
    }

    /**
     * 创建分区小标题行
     * 整行合并、左对齐、无边框、常规字号黑体
     */
    private static int addSectionTitleRow(Sheet sheet, String text, int columnCount, Workbook workbook, int rowNum) {
        Row sectionRow = sheet.createRow(rowNum);

        CellStyle sectionStyle = workbook.createCellStyle();
        Font sectionFont = workbook.createFont();
        sectionFont.setFontName("黑体");
        sectionFont.setFontHeightInPoints((short) 14);
        sectionStyle.setFont(sectionFont);
        sectionStyle.setAlignment(CellStyle.ALIGN_LEFT);
        sectionStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        // 无边框
        sectionStyle.setBorderTop(CellStyle.BORDER_NONE);
        sectionStyle.setBorderBottom(CellStyle.BORDER_NONE);
        sectionStyle.setBorderLeft(CellStyle.BORDER_NONE);
        sectionStyle.setBorderRight(CellStyle.BORDER_NONE);

        // 初始化单元格样式
        for (int col = 0; col < columnCount; col++) {
            Cell cell = sectionRow.createCell(col);
            cell.setCellStyle(sectionStyle);
        }

        sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, columnCount - 1));
        sectionRow.getCell(0).setCellValue(text);
        sectionRow.setHeightInPoints(25);

        return rowNum + 1;
    }

    /**
     * 创建表头行
     * 居中、全边框、黑体字体
     */
    private static int addHeaderRow(Sheet sheet, String[] rowName, Workbook workbook, int rowNum) {
        Row headerRow = sheet.createRow(rowNum);

        CellStyle headerStyle = workbook.createCellStyle();
        Font headerFont = workbook.createFont();
        headerFont.setFontName("黑体");
        headerFont.setFontHeightInPoints((short) 14);
        headerStyle.setFont(headerFont);
        headerStyle.setAlignment(CellStyle.ALIGN_CENTER);
        headerStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        // 全边框细线
        headerStyle.setBorderTop(CellStyle.BORDER_THIN);
        headerStyle.setBorderBottom(CellStyle.BORDER_THIN);
        headerStyle.setBorderLeft(CellStyle.BORDER_THIN);
        headerStyle.setBorderRight(CellStyle.BORDER_THIN);

        for (int i = 0; i < rowName.length; i++) {
            Cell headerCell = headerRow.createCell(i);
            headerCell.setCellStyle(headerStyle);
            headerCell.setCellValue(rowName[i]);
        }

        headerRow.setHeightInPoints(30);
        return rowNum + 1;
    }

    /**
     * 填充表格数据,附带全套样式、自动行高、自动换行、合计行自动合并
     * @param sheet 表格对象
     * @param fieldName 实体属性字段数组
     * @param dataList 待填充数据集合
     * @param workbook 工作簿
     * @param startRow 开始填充行号
     * @param rowHeight 基础行高
     * @return 填充结束后的下一行号
     */
    private static <T> int fillDataWithStyle(Sheet sheet, String[] fieldName, List<T> dataList,
                                             Workbook workbook, int startRow, float rowHeight) {
        if (dataList == null || dataList.isEmpty()) {
            // 数据源为空时,清空模板对应区域所有旧行
            for (int i = sheet.getLastRowNum(); i >= startRow; i--) {
                Row row = sheet.getRow(i);
                if (row != null) {
                    sheet.removeRow(row);
                }
            }
            return startRow;
        }

        // 第一列单元格样式:黑体、居中、全边框、自动换行
        CellStyle firstColumnStyle = workbook.createCellStyle();
        Font firstColumnFont = workbook.createFont();
        firstColumnFont.setFontName("黑体");
        firstColumnFont.setFontHeightInPoints((short)14);
        firstColumnStyle.setFont(firstColumnFont);
        firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
        firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
        firstColumnStyle.setWrapText(true);

        // 普通数据列样式:仿宋_GB2312、居中、全边框、自动换行
        CellStyle normalContentStyle = workbook.createCellStyle();
        Font normalContentFont = workbook.createFont();
        normalContentFont.setFontName("仿宋_GB2312");
        normalContentFont.setFontHeightInPoints((short)14);
        normalContentStyle.setFont(normalContentFont);
        normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
        normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
        normalContentStyle.setWrapText(true);

        // 小计/合计行专用样式:微软雅黑字体
        CellStyle subtotalStyle = workbook.createCellStyle();
        Font subtotalFont = workbook.createFont();
        subtotalFont.setFontName("微软雅黑");
        subtotalFont.setFontHeightInPoints((short)14);
        subtotalStyle.setFont(subtotalFont);
        subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
        subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);

        int currentRow = startRow;

        for (int i = 0; i < dataList.size(); i++) {
            Row dataRow = sheet.createRow(currentRow);
            // 核心:设置自适应自动行高,适配长文本内容
            dataRow.setHeight((short) -1);

            T data = dataList.get(i);
            Map<String, Object> dataMap = convertBeanToMap(data);

            // 判断当前行是否为合计/小计汇总行
            boolean isTotalRow = false;
            String secondValue = "";
            if (fieldName.length >= 2) {
                Object val = dataMap.get(fieldName[1]);
                if (val != null) {
                    secondValue = val.toString();
                    isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
                }
            }

            // 遍历列填充单元格数据
            for (int j = 0; j < fieldName.length; j++) {
                Cell cell = dataRow.createCell(j);
                // 区分样式:汇总行、首列、普通内容列
                cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));

                Object value = dataMap.get(fieldName[j]);

                if (isTotalRow) {
                    if (j == 0) {
                        cell.setCellValue(secondValue);
                    } else if (j >= 2 && j <= 7) {
                        cell.setCellValue("");
                    } else {
                        setCellValue(cell, value);
                    }
                } else {
                    setCellValue(cell, value);
                }
            }

            // 汇总行自动合并单元格
            if (isTotalRow) {
                sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
                if (fieldName.length > 7) {
                    sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
                }
            }

            currentRow++;
        }
        return currentRow;
    }

    /**
     * 通用单元格赋值工具方法
     * 统一处理空值、日期格式化、数字类型、普通字符串
     */
    private static void setCellValue(Cell cell, Object value) {
        if (value == null) {
            cell.setCellValue("");
        } else if (value instanceof Date) {
            cell.setCellValue(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value));
        } else if (value instanceof Number) {
            cell.setCellValue(((Number) value).doubleValue());
        } else {
            cell.setCellValue(value.toString());
        }
    }

    /**
     * 数据填充重载方法3(备用保留方法,兼容历史代码)
     */
    private static <T> int fillDataWithStyle3(Sheet sheet, String[] fieldName, List<T> dataList,
                                             Workbook workbook, int startRow, float rowHeight) {
        if (dataList == null || dataList.isEmpty()) {
            return startRow;
        }

        CellStyle firstColumnStyle = workbook.createCellStyle();
        Font firstColumnFont = workbook.createFont();
        firstColumnFont.setFontName("黑体");
        firstColumnFont.setFontHeightInPoints((short)14);
        firstColumnStyle.setFont(firstColumnFont);
        firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
        firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
        firstColumnStyle.setWrapText(true);

        CellStyle normalContentStyle = workbook.createCellStyle();
        Font normalContentFont = workbook.createFont();
        normalContentFont.setFontName("仿宋_GB2312");
        normalContentFont.setFontHeightInPoints((short)14);
        normalContentStyle.setFont(normalContentFont);
        normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
        normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
        normalContentStyle.setWrapText(true);

        CellStyle subtotalStyle = workbook.createCellStyle();
        Font subtotalFont = workbook.createFont();
        subtotalFont.setFontName("微软雅黑");
        subtotalFont.setFontHeightInPoints((short)14);
        subtotalStyle.setFont(subtotalFont);
        subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
        subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);

        int currentRow = startRow;

        for (int i = 0; i < dataList.size(); i++) {
            Row dataRow = sheet.createRow(currentRow);
            dataRow.setHeightInPoints(rowHeight);

            T data = dataList.get(i);
            Map<String, Object> dataMap = convertBeanToMap(data);

            boolean isTotalRow = false;
            String secondValue = "";
            if (fieldName.length >= 2) {
                Object val = dataMap.get(fieldName[1]);
                if (val != null) {
                    secondValue = val.toString();
                    isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
                }
            }

            for (int j = 0; j < fieldName.length; j++) {
                Cell cell = dataRow.createCell(j);
                cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));

                Object value = dataMap.get(fieldName[j]);

                if (isTotalRow) {
                    if (j == 0) {
                        cell.setCellValue(secondValue);
                    } else if (j >= 2 && j <= 7) {
                        cell.setCellValue("");
                    } else {
                        setCellValue(cell, value);
                    }
                } else {
                    setCellValue(cell, value);
                }
            }

            if (isTotalRow) {
                sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
                if (fieldName.length > 7) {
                    sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
                }
            }

            currentRow++;
        }
        return currentRow;
    }

    /**
     * 备用数据填充方法2(历史兼容保留)
     */
    private static <T> int fillDataWithStyle2(Sheet sheet, String[] fieldName, List<T> dataList,
                                             Workbook workbook, int startRow, float rowHeight) {
        if (dataList == null || dataList.isEmpty()) {
            return startRow;
        }

        CellStyle firstColumnStyle = workbook.createCellStyle();
        Font firstColumnFont = workbook.createFont();
        firstColumnFont.setFontName("黑体");
        firstColumnFont.setFontHeightInPoints((short) 14);
        firstColumnStyle.setFont(firstColumnFont);
        firstColumnStyle.setAlignment(CellStyle.ALIGN_CENTER);
        firstColumnStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        firstColumnStyle.setBorderTop(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderBottom(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderLeft(CellStyle.BORDER_THIN);
        firstColumnStyle.setBorderRight(CellStyle.BORDER_THIN);
        firstColumnStyle.setWrapText(true);

        CellStyle normalContentStyle = workbook.createCellStyle();
        Font normalContentFont = workbook.createFont();
        normalContentFont.setFontName("仿宋_GB2312");
        normalContentFont.setFontHeightInPoints((short) 14);
        normalContentStyle.setFont(normalContentFont);
        normalContentStyle.setAlignment(CellStyle.ALIGN_CENTER);
        normalContentStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        normalContentStyle.setBorderTop(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderBottom(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderLeft(CellStyle.BORDER_THIN);
        normalContentStyle.setBorderRight(CellStyle.BORDER_THIN);
        normalContentStyle.setWrapText(true);

        CellStyle subtotalStyle = workbook.createCellStyle();
        Font subtotalFont = workbook.createFont();
        subtotalFont.setFontName("微软雅黑");
        subtotalFont.setFontHeightInPoints((short) 14);
        subtotalStyle.setFont(subtotalFont);
        subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
        subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);

        int currentRow = startRow;

        for (int i = 0; i < dataList.size(); i++) {
            Row dataRow = sheet.createRow(currentRow);
            dataRow.setHeightInPoints(rowHeight);

            T data = dataList.get(i);
            Map<String, Object> dataMap = convertBeanToMap(data);

            boolean isTotalRow = false;
            if (fieldName.length >= 2) {
                Object secondColumnValue = dataMap.get(fieldName[1]);
                if (secondColumnValue != null) {
                    String secondValue = secondColumnValue.toString();
                    isTotalRow = "小计".equals(secondValue) || secondValue.contains("合计");
                }
            }

            for (int j = 0; j < fieldName.length; j++) {
                Cell cell = dataRow.createCell(j);
                cell.setCellStyle(isTotalRow ? subtotalStyle : (j == 0 ? firstColumnStyle : normalContentStyle));

                Object value = dataMap.get(fieldName[j]);
                if (value != null) {
                    if (value instanceof Date) {
                        cell.setCellValue(dateFormat.format((Date) value));
                    } else if (value instanceof Number) {
                        cell.setCellValue(((Number) value).doubleValue());
                    } else {
                        cell.setCellValue(value.toString());
                    }
                } else {
                    cell.setCellValue("");
                }
            }

            if (fieldName.length >= 2) {
                Object secondColumnValue = dataMap.get(fieldName[1]);
                if (secondColumnValue != null) {
                    String secondValue = secondColumnValue.toString();
                    if ("小计".equals(secondValue) || secondValue.contains("合计")) {
                        sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 0, 1));
                        if (fieldName.length > 7) {
                            sheet.addMergedRegion(new CellRangeAddress(currentRow, currentRow, 2, 7));
                        }
                    }
                }
            }

            currentRow++;
        }

        return currentRow;
    }

    /**
     * 手动添加小计行(备用方法)
     */
    private static int addSubtotalRow(Sheet sheet, int columnCount, Workbook workbook, int rowNum) {
        Row subtotalRow = sheet.createRow(rowNum);

        CellStyle subtotalStyle = workbook.createCellStyle();
        Font subtotalFont = workbook.createFont();
        subtotalFont.setFontName("黑体");
        subtotalFont.setFontHeightInPoints((short) 14);
        subtotalStyle.setFont(subtotalFont);
        subtotalStyle.setAlignment(CellStyle.ALIGN_CENTER);
        subtotalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        subtotalStyle.setBorderTop(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderBottom(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderLeft(CellStyle.BORDER_THIN);
        subtotalStyle.setBorderRight(CellStyle.BORDER_THIN);

        for (int col = 0; col < columnCount; col++) {
            Cell cell = subtotalRow.createCell(col);
            cell.setCellStyle(subtotalStyle);
        }

        sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, 1));
        subtotalRow.getCell(0).setCellValue("小计");
        subtotalRow.setHeightInPoints(25);

        return rowNum + 1;
    }

    /**
     * 手动添加合计行(备用空行方法)
     */
    private static int addTotalRow(Sheet sheet, int columnCount,
                                   Workbook workbook, int rowNum) {
        Row totalRow = sheet.createRow(rowNum);

        CellStyle totalStyle = workbook.createCellStyle();
        Font totalFont = workbook.createFont();
        totalFont.setFontName("黑体");
        totalFont.setFontHeightInPoints((short) 14);
        totalStyle.setFont(totalFont);
        totalStyle.setAlignment(CellStyle.ALIGN_CENTER);
        totalStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        totalStyle.setBorderTop(CellStyle.BORDER_THIN);
        totalStyle.setBorderBottom(CellStyle.BORDER_THIN);
        totalStyle.setBorderLeft(CellStyle.BORDER_THIN);
        totalStyle.setBorderRight(CellStyle.BORDER_THIN);

        for (int col = 0; col < columnCount; col++) {
            Cell cell = totalRow.createCell(col);
            cell.setCellStyle(totalStyle);
        }

        sheet.addMergedRegion(new CellRangeAddress(rowNum, rowNum, 0, 1));
        totalRow.setHeightInPoints(25);

        return rowNum + 1;
    }

    /**
     * 添加签名备注行(备用扩展方法)
     */
    private static void addSignatureRow(Sheet sheet, int signatureRowNum, int columnCount, Workbook workbook) {
        Row signatureRow = sheet.createRow(signatureRowNum);

        CellStyle signatureStyle = workbook.createCellStyle();
        Font signatureFont = workbook.createFont();
        signatureFont.setFontName("微软雅黑");
        signatureFont.setFontHeightInPoints((short) 18);
        signatureStyle.setFont(signatureFont);
        signatureStyle.setAlignment(CellStyle.ALIGN_LEFT);
        signatureStyle.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
        signatureStyle.setBorderTop(CellStyle.BORDER_THIN);
        signatureStyle.setBorderBottom(CellStyle.BORDER_NONE);
        signatureStyle.setBorderLeft(CellStyle.BORDER_NONE);
        signatureStyle.setBorderRight(CellStyle.BORDER_NONE);

        for (int col = 0; col < columnCount; col++) {
            Cell cell = signatureRow.createCell(col);
            cell.setCellStyle(signatureStyle);
        }

        sheet.addMergedRegion(new CellRangeAddress(
                signatureRowNum, signatureRowNum, 0, columnCount - 1
        ));

        signatureRow.setHeightInPoints(40);
    }

    /**
     * 隐藏模板多余列,适配自定义列数
     */
    private static void clearExtraColumns(Sheet sheet, int dynamicColumnCount) {
        int maxColumn = sheet.getRow(1) != null ? sheet.getRow(1).getLastCellNum() : 0;
        if (maxColumn > dynamicColumnCount) {
            for (int i = dynamicColumnCount; i < maxColumn; i++) {
                sheet.setColumnHidden(i, true);
            }
        }
    }

    /**
     * 通用实体对象转Map,通过字段名反射取值
     */
    private static <T> Map<String, Object> convertBeanToMap(T bean) {
        return com.alibaba.fastjson.JSON.parseObject(com.alibaba.fastjson.JSON.toJSONString(bean), Map.class);
    }

    /**
     * 自动删除表格最后一行空行
     * 遍历判断所有单元格无内容则移除整行
     */
    private static void removeLastEmptyRow(Sheet sheet) {
        int lastRowNum = sheet.getLastRowNum();
        if (lastRowNum < 2) {
            return;
        }
        Row lastRow = sheet.getRow(lastRowNum);
        if (lastRow == null) {
            return;
        }
        boolean isEmpty = true;
        for (int i = 0; i < lastRow.getLastCellNum(); i++) {
            Cell cell = lastRow.getCell(i);
            if (cell != null && cell.getCellType() != Cell.CELL_TYPE_BLANK) {
                String cellValue = "";
                switch (cell.getCellType()) {
                    case Cell.CELL_TYPE_STRING:
                        cellValue = cell.getStringCellValue();
                        break;
                    case Cell.CELL_TYPE_NUMERIC:
                        cellValue = String.valueOf(cell.getNumericCellValue());
                        break;
                    case Cell.CELL_TYPE_BOOLEAN:
                        cellValue = String.valueOf(cell.getBooleanCellValue());
                        break;
                    case Cell.CELL_TYPE_FORMULA:
                        cellValue = cell.getCellFormula();
                        break;
                }
                if (cellValue != null && !cellValue.trim().isEmpty()) {
                    isEmpty = false;
                    break;
                }
            }
        }
        if (isEmpty) {
            sheet.removeRow(lastRow);
        }
    }

    /**
     * 方法重载兼容旧版本调用,无合计参数版本
     */
    public static <T> void exportExcelxlsfbtj(String title, String[] rowName, String[] fieldName,
                                              List<T> dataList, List<T> dataListFx,
                                              HttpServletResponse response) throws IOException {
        exportExcelxlsfbtj(title, rowName, fieldName, dataList, dataListFx, null, response);
    }
}

三、工具类核心功能详解

1. 整体结构设计

工具类基于Excel模板文件加载生成,而非从零创建表格,保留模板基础格式,动态填充业务数据,结构分层清晰:

  1. 大标题行:整行合并、大字号无框线
  2. 分区标题行:多块数据区域分隔标题
  3. 表头固定行:统一边框、居中黑体样式
  4. 业务数据行:分首列、普通内容列差异化字体
  5. 汇总小计行:自动识别、自动合并单元格

2. 双数据源独立填充

内置两套数据列表独立渲染逻辑:

  • 第一个集合数据渲染完成后,自动追加第二块区域标题+第二份数据
  • 第二个数据源为null/空集合时,自动跳过不渲染,无多余空行
  • 两套数据源共用同一套表头字段映射规则,无需重复配置

3. 全套字体样式规范

  1. 报表大标题:方正小标宋简体、30号字
  2. 分区小标题、表格首列:黑体、14号字
  3. 正文普通数据:仿宋_GB2312、14号字
  4. 统计汇总行:微软雅黑、14号字
    所有单元格统一边框、居中对齐、内容自动换行。

4. 自适应行高+长文本兼容

java 复制代码
// 开启单元格自动换行
cellStyle.setWrapText(true);
// 行高自适应内容高度
dataRow.setHeight((short) -1);

长文本内容自动换行撑开行高,无需手动设置固定行高,解决文字显示不全问题。

5. 汇总行自动合并单元格

自动识别第二列内容为小计xxx合计的行,自动执行合并:

  • 合并第0、1列区域
  • 列数大于8时,自动合并第2~7列区域
  • 合并区域自动清空多余无用单元格内容

6. 通用数据类型处理

统一封装单元格赋值方法,自动处理:

  • null空值转为空字符串
  • Date日期类型自动格式化
  • 数字类型正常数值填充
  • 普通对象统一转为字符串

7. 附加优化工具

  1. 多余列隐藏:根据传入列数,隐藏模板超出部分无用列
  2. 尾部空行清理:自动检测表格末尾全空行并删除
  3. 文件名兼容:过滤特殊字符、解决浏览器中文文件名乱码
  4. 流资源关闭:Workbook、IO流全部try-with-resources安全关闭,防止内存泄漏

四、Controller调用使用示例

通用完整调用示例,任意业务VO都可使用,无业务绑定。

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/excel")
public class ExcelExportController {

    @GetMapping("/export")
    public void export(HttpServletResponse response) throws IOException {
        // 1.自定义表头文字
        String[] headTitle = {"序号", "商品名称", "规格", "数量", "单价", "金额", "备注"};
        // 2.对应实体类/VO的属性字段名,和表头一一对应
        String[] fieldCode = {"id", "goodsName", "spec", "num", "price", "total", "remark"};

        // 3.主数据源 任意自定义实体集合
        List<GoodsVO> dataOne = new ArrayList<>();
        // 4.第二个附加数据源
        List<GoodsVO> dataTwo = new ArrayList<>();

        // 5.调用工具类直接导出
        ExcelUtilsNewFBTJNEWS.exportExcelxlsfbtj(
                "商品数据统计报表",
                headTitle,
                fieldCode,
                dataOne,
                dataTwo,
                response
        );
    }
}

五、常见问题与解决方案

1. 模板文件加载失败异常

  1. 检查文件路径格式:Windows本地路径使用双反斜杠\\,Linux服务器使用正斜杠/
  2. 检查服务器文件权限,程序需要具备文件读取权限
  3. 模板必须为.xlsx格式,不兼容旧版.xls

2. 导出文件中文乱码、文件名乱码

工具类内部已采用utf-8编码+标准下载头格式处理,无需额外修改,仅保证Tomcat服务编码为UTF-8即可。

3. 服务器字体不显示异常

Linux服务器缺少Windows自带字体(黑体、仿宋、方正小标宋),需要在服务器系统安装对应字体包。

4. POI版本冲突报错

项目内统一固定POI版本4.1.2,排除其他依赖自带的高版本/低版本POI依赖。

5. 数据源为空导出出现多余空行

工具类内置判空清空逻辑,空集合会自动删除填充区域全部行,不会残留空行。

六、总结

本套Excel导出工具基于POI深度封装,脱离单一业务场景,全通用版,解决了日常开发绝大多数导出痛点:

  • 无需手动绘制Excel表格结构
  • 支持双模块数据分段导出
  • 格式排版规范,字体、边框、对齐统一
  • 长文本自动换行、行高自适应
  • 汇总数据自动合并单元格
  • 空值、日期、数字统一处理
  • 多余列、末尾空行自动清理
  • 浏览器下载全兼容,资源自动关闭

整体代码健壮性高,保留全部历史重载方法兼容旧项目调用,直接复制进现有项目修改模板路径即可一键使用。

本文代码全部实测可运行,如需扩展更多样式、多Sheet、多表头、大数据分页导出可自行基于现有结构扩展。
点赞收藏不迷路,Java常用工具类持续更新!

相关推荐
代码中介商2 小时前
C++ 继承与派生深度解析:存储布局、构造析构与高级特性
开发语言·c++·继承·派生
我不是懒洋洋2 小时前
【经典题目】栈和队列面试题(括号匹配问题、用队列实现栈、设计循环队列、用栈实现队列)
c语言·开发语言·数据结构·算法·leetcode·链表·ecmascript
枫叶丹42 小时前
【HarmonyOS 6.0】ArkWeb PDF浏览能力增强:指定PDF文档背景色功能详解
开发语言·华为·pdf·harmonyos
谭欣辰2 小时前
C++ 控制台跑酷小游戏2.0
开发语言·c++·游戏程序
Huangxy__2 小时前
java相机手搓(后续是文件保存以及接入大模型)
java·开发语言·数码相机
刚子编程2 小时前
C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生
开发语言·c#·事务处理·trycatch
lsx2024062 小时前
jEasyUI 自定义对话框
开发语言
陶然同学2 小时前
【Python】文件操作
开发语言·python
来自远方的老作者2 小时前
第10章 面向对象-10.3 封装
开发语言·python·私有属性·私有方法·封装