目 录CONTENT

文章目录

Excel导出工具类

BKUN
2025-03-12 / 0 评论 / 0 点赞 / 382 阅读 / 2,419 字

🚀 Excel 模板生成工具:ExcelTemplateUtil

ExcelTemplateUtil 是一个基于 Apache POI 的 Java 工具类,用于从 Excel 模板生成动态 Excel 文件。它支持占位符替换(如 ${variable})和循环处理(如 <jx:foreach>),适合批量生成报表或数据导出场景。以下是功能说明和代码解析!📜


📝 功能概述

  • 占位符替换:将 Excel 单元格中的 ${variable} 替换为实际数据。
  • 循环处理:通过 <jx:foreach> 标签动态生成多行数据。
  • 格式支持:自动识别日期、数字(包括百分比)、科学计数法等格式。
  • 日志记录:使用 SLF4J 记录操作日志,便于调试。

使用场景

  • 生成动态报表(如财务、销售数据)。
  • 批量导出用户数据到 Excel。
  • 基于模板生成复杂格式的 Excel 文件。

🔧 代码解析

核心类:ExcelTemplateUtil

  • 依赖:Apache POI(处理 Excel)、SLF4J(日志)。
  • 主要方法
    • generateExcel:读取模板,处理数据,生成输出文件。
    • processSheet:处理每个 Sheet,包含占位符替换和循环逻辑。
    • replacePlaceholders:替换 ${variable} 占位符。
    • applyCellValue:根据单元格格式设置值(字符串、数字、日期)。

关键逻辑

  1. 占位符处理
    • 使用正则表达式 Pattern.compile("\\$\\{([^}]+)\\}") 匹配 ${variable}
    • 支持嵌套字段(如 ${user.name})和循环索引(如 ${status.index})。
  2. 循环处理
    • 解析 <jx:foreach items="${list}" var="item" varstatus="status"> 标签。
    • 动态复制模板行,填充列表数据。
  3. 单元格格式
    • 自动检测单元格格式(如日期、百分比)。
    • 科学计数法和数字通过正则匹配处理。

示例代码

以下是完整代码(已精简注释以节省空间):

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExcelTemplateUtil {
    private static final Logger logger = LoggerFactory.getLogger(ExcelTemplateUtil.class);
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
    private static final Pattern FOREACH_PATTERN = Pattern.compile("<jx:foreach\\s+([^>]*)>", Pattern.CASE_INSENSITIVE);
    private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\s*(\\w+)=\"([^\"]*)\"", Pattern.CASE_INSENSITIVE);
    private static final Pattern INDEX_EXPR_PATTERN = Pattern.compile("^([\\w]+)\\.index(?:([+\\-*/])(\\d+))?$", Pattern.CASE_INSENSITIVE);

    public static void generateExcel(String templatePath, String outputPath, Map<String, Object> data) throws IOException {
        try (FileInputStream fis = new FileInputStream(templatePath);
             Workbook workbook = new XSSFWorkbook(fis);
             FileOutputStream fos = new FileOutputStream(outputPath)) {
            for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
                processSheet(workbook.getSheetAt(i), data);
            }
            workbook.write(fos);
        }
    }

    private static void processSheet(Sheet sheet, Map<String, Object> data) {
        int rowIndex = 0;
        while (rowIndex <= sheet.getLastRowNum()) {
            Row row = sheet.getRow(rowIndex);
            if (row == null) {
                rowIndex++;
                continue;
            }

            String foreachStart = findForeachStart(row);
            if (foreachStart != null) {
                Matcher matcher = FOREACH_PATTERN.matcher(foreachStart);
                if (matcher.find()) {
                    Map<String, String> attrMap = parseAttributes(matcher.group(1));
                    String itemsName = attrMap.get("items").replace("${", "").replace("}", "");
                    String varName = attrMap.get("var");
                    String statusName = attrMap.get("varstatus");
                    Object listData = data.get(itemsName);

                    if (listData instanceof List) {
                        List<?> list = (List<?>) listData;
                        int endRowIndex = findForeachEnd(sheet, rowIndex);
                        if (endRowIndex == -1 || endRowIndex <= rowIndex) {
                            rowIndex++;
                            continue;
                        }

                        int rowsToCopy = endRowIndex - rowIndex - 1;
                        if (list.isEmpty()) {
                            sheet.shiftRows(endRowIndex + 1, sheet.getLastRowNum(), -(endRowIndex - rowIndex + 1));
                            rowIndex++;
                            continue;
                        }

                        List<TemplateRow> templateRows = new ArrayList<>();
                        for (int i = 0; i < rowsToCopy; i++) {
                            Row sourceRow = sheet.getRow(rowIndex + 1 + i);
                            templateRows.add(new TemplateRow(sourceRow != null ? sourceRow : sheet.createRow(rowIndex + 1 + i)));
                        }

                        sheet.shiftRows(rowIndex, sheet.getLastRowNum(), -(endRowIndex - rowIndex + 1));
                        int insertRowIndex = rowIndex;
                        int currentIndex = 0;
                        for (Object item : list) {
                            for (int i = 0; i < rowsToCopy; i++) {
                                Row newRow = sheet.createRow(insertRowIndex + i);
                                copyRowFromTemplate(newRow, templateRows.get(i), item, varName, statusName, currentIndex);
                            }
                            insertRowIndex += rowsToCopy;
                            currentIndex++;
                        }
                        rowIndex = insertRowIndex;
                    } else {
                        rowIndex++;
                    }
                } else {
                    rowIndex++;
                }
            } else {
                for (Cell cell : row) {
                    if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
                        String cellValue = cell.getStringCellValue();
                        if (cellValue != null && cellValue.contains("${")) {
                            String replacedValue = replacePlaceholders(cellValue, data);
                            applyCellValue(cell, replacedValue, cell.getCellStyle());
                        }
                    }
                }
                rowIndex++;
            }
        }
    }

    private static Map<String, String> parseAttributes(String attributes) {
        Map<String, String> attrMap = new HashMap<>();
        Matcher matcher = ATTRIBUTE_PATTERN.matcher(attributes);
        while (matcher.find()) {
            attrMap.put(matcher.group(1).toLowerCase(), matcher.group(2));
        }
        return attrMap;
    }

    private static String findForeachStart(Row row) {
        for (Cell cell : row) {
            if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
                String cellValue = cell.getStringCellValue();
                if (cellValue != null && cellValue.toLowerCase().contains("<jx:foreach")) {
                    return cellValue;
                }
            }
        }
        return null;
    }

    private static int findForeachEnd(Sheet sheet, int startRowIndex) {
        for (int i = startRowIndex + 1; i <= sheet.getLastRowNum(); i++) {
            Row row = sheet.getRow(i);
            if (row != null) {
                for (Cell cell : row) {
                    if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
                        String cellValue = cell.getStringCellValue();
                        if (cellValue != null && cellValue.toLowerCase().contains("</jx:foreach>")) {
                            return i;
                        }
                    }
                }
            }
        }
        return -1;
    }

    private static class TemplateRow {
        private final List<String> values = new ArrayList<>();
        private final List<CellStyle> styles = new ArrayList<>();

        TemplateRow(Row row) {
            int lastCellNum = row.getLastCellNum();
            for (int col = 0; col < lastCellNum; col++) {
                Cell cell = row.getCell(col, Row.RETURN_BLANK_AS_NULL);
                values.add(cell != null && cell.getCellType() == Cell.CELL_TYPE_STRING ? cell.getStringCellValue() : "");
                styles.add(cell != null ? cell.getCellStyle() : null);
            }
        }

        String getValue(int col) {
            return col < values.size() ? values.get(col) : "";
        }

        CellStyle getStyle(int col) {
            return col < styles.size() ? styles.get(col) : null;
        }

        int getCellCount() {
            return values.size();
        }
    }

    private static void copyRowFromTemplate(Row targetRow, TemplateRow templateRow, Object item, String varName, String statusName, int index) {
        for (int col = 0; col < templateRow.getCellCount(); col++) {
            Cell targetCell = targetRow.createCell(col);
            String cellValue = templateRow.getValue(col);
            CellStyle style = templateRow.getStyle(col);
            if (style != null) {
                targetCell.setCellStyle(style);
            }
            String replacedValue = (cellValue != null && cellValue.contains("${"))
                    ? replacePlaceholdersWithVar(cellValue, item, varName, statusName, index)
                    : cellValue;
            applyCellValue(targetCell, replacedValue, style);
        }
    }

    private static void applyCellValue(Cell cell, String value, CellStyle style) {
        if (value == null || style == null) {
            cell.setCellValue(value);
            return;
        }
        short dataFormat = style.getDataFormat();
        String formatString = style.getDataFormatString();

        if (dataFormat == 0 && (formatString == null || formatString.equals("General"))) {
            if (isDecimal(value) || isScientific(value)) {
                cell.setCellValue(new BigDecimal(value).doubleValue());
            } else {
                cell.setCellType(Cell.CELL_TYPE_STRING);
                cell.setCellValue(value);
            }
            return;
        }

        if (isDateFormat(dataFormat, formatString)) {
            try {
                cell.setCellType(Cell.CELL_TYPE_NUMERIC);
                cell.setCellValue(new Date(value));
            } catch (Exception e) {
                cell.setCellValue(value);
            }
        } else if (isNumericFormat(dataFormat, formatString)) {
            try {
                double numericValue = Double.parseDouble(value);
                cell.setCellType(Cell.CELL_TYPE_NUMERIC);
                cell.setCellValue(formatString != null && formatString.contains("%") ? numericValue / 100 : numericValue);
            } catch (NumberFormatException e) {
                cell.setCellValue(value);
            }
        } else {
            cell.setCellType(Cell.CELL_TYPE_STRING);
            cell.setCellValue(value);
        }
    }

    private static boolean isDateFormat(short dataFormat, String formatString) {
        return dataFormat == 14 || dataFormat == 22 || (formatString != null && formatString.contains("yy"));
    }

    private static boolean isNumericFormat(short dataFormat, String formatString) {
        return dataFormat == 0 || dataFormat == 2 || dataFormat == 10 || dataFormat == 177
                || (formatString != null && (formatString.contains("0") || formatString.contains("%")));
    }

    private static String replacePlaceholdersWithVar(String text, Object item, String varName, String statusName, int index) {
        Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
        StringBuffer result = new StringBuffer();
        while (matcher.find()) {
            String placeholder = matcher.group(1);
            if (placeholder.startsWith(varName + ".")) {
                String field = placeholder.substring(varName.length() + 1);
                String replacement = getValueFromData(field, item);
                matcher.appendReplacement(result, Matcher.quoteReplacement(replacement != null ? replacement : ""));
            } else if (statusName != null && INDEX_EXPR_PATTERN.matcher(placeholder).matches()) {
                Matcher exprMatcher = INDEX_EXPR_PATTERN.matcher(placeholder);
                if (exprMatcher.matches() && exprMatcher.group(1).equals(statusName)) {
                    String operator = exprMatcher.group(2);
                    String operand = exprMatcher.group(3);
                    String replacement = operator == null ? String.valueOf(index)
                            : computeIndex(index, operator, operand);
                    matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
                } else {
                    matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0)));
                }
            } else {
                matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0)));
            }
        }
        matcher.appendTail(result);
        return result.toString();
    }

    private static String computeIndex(int index, String operator, String operand) {
        try {
            int operandValue = Integer.parseInt(operand);
            switch (operator) {
                case "+": return String.valueOf(index + operandValue);
                case "-": return String.valueOf(index - operandValue);
                case "*": return String.valueOf(index * operandValue);
                case "/": return operandValue != 0 ? String.valueOf(index / operandValue) : "0";
                default: logger.error("Unsupported operator: " + operator); return String.valueOf(index);
            }
        } catch (NumberFormatException e) {
            logger.error("Invalid operand in expression: " + operand);
            return String.valueOf(index);
        }
    }

    private static String replacePlaceholders(String text, Map<String, Object> data) {
        Matcher matcher = PLACEHOLDER_PATTERN.matcher(text);
        StringBuffer result = new StringBuffer();
        while (matcher.find()) {
            String replacement = getValueFromData(matcher.group(1), data);
            matcher.appendReplacement(result, Matcher.quoteReplacement(replacement != null ? replacement : ""));
        }
        matcher.appendTail(result);
        return result.toString();
    }

    private static String getValueFromData(String placeholder, Object data) {
        String[] parts = placeholder.split("\\.");
        Object current = data;
        for (String part : parts) {
            if (current instanceof Map) {
                current = ((Map<?, ?>) current).get(part);
            } else if (current != null) {
                current = getFieldValue(current, part);
            } else {
                return null;
            }
        }
        return current != null ? current.toString() : null;
    }

    private static Object getFieldValue(Object obj, String fieldName) {
        try {
            Field field = obj.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            logger.error("Failed to get field value: " + fieldName, e);
            return null;
        }
    }

    public static boolean isDecimal(String str) {
        if (str == null) return false;
        String regex = "^-?\\d+(\\.\\d+)?$";
        if (str.matches(regex)) {
            String[] split = str.split("\\.");
            if (split.length > 0 && (split[0].startsWith("00") || split[0].length() >= 15)) {
                return false;
            }
            return true;
        }
        return false;
    }

    public static boolean isScientific(String str) {
        if (str == null) return false;
        String regex = "^-?\\d*\\.\\d+[eE][+-]?\\d+$";
        return str.matches(regex);
    }

    public static void main(String[] args) throws IOException {
        Map<String, Object> data = new HashMap<>();
        data.put("user", new User("张三", 25, new Date()));
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
            userList.add(new User("李四", null, new Date()));
            userList.add(new User("王五", null, new Date()));
        }
        data.put("users", userList);
        data.put("date", "2025-03-11");

        String templatePath = "D:/template.xlsx";
        String outputPath = "D:/output.xlsx";
        long startTime = System.currentTimeMillis();
        generateExcel(templatePath, outputPath, data);
        System.out.println("Excel 文件生成成功!耗时:" + (System.currentTimeMillis() - startTime) / 1000 + "秒");
    }

    static class User {
        private String name;
        private Integer age;
        private Date makedate;
        private String name1 = "00211111111111111111111111";
        private String name2 = "3.1415926";
        private String name3 = "XXXXXXXXXXXXXXXXXXXXXX";
        // ... 其他字段省略 ...

        public User(String name, Integer age, Date makedate) {
            this.name = name;
            this.age = age;
            this.makedate = makedate;
        }
    }
}

🌟 使用方法

1. 准备 Excel 模板

  • 创建 template.xlsx,包含占位符(如 ${user.name})和循环标签:
    <jx:foreach items="${users}" var="user" varstatus="status">
      ${status.index} | ${user.name} | ${user.age} | ${user.makedate}
    </jx:foreach>
    
  • 保存至 D:/template.xlsx

2. 运行代码

  • 配置 data Map,包含单对象(user)和列表(users)。
  • 调用 generateExcel("D:/template.xlsx", "D:/output.xlsx", data)
  • 输出文件将生成在 D:/output.xlsx

示例输出

  • 生成的 Excel 包含 40000 行用户数据(users 列表),每个用户填充 nameagemakedate 等字段。
  • 耗时记录:约数秒(视机器性能)。

📌 注意事项

  • 依赖配置
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>5.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>5.2.3</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.7</version>
    </dependency>
    
  • 模板规范:确保 <jx:foreach></jx:foreach> 配对,避免解析失败。
  • 文件路径:Windows 使用 D:/,Linux 使用 /path/to/,注意斜杠方向。
  • 数据校验:确保 data Map 包含模板中所有占位符对应的键。
0

评论区