基于 POI 實現(xiàn)一個 Excel 模板引擎

0. 效果預(yù)覽

新建一個 excel 文檔template.xlsx,作為模板:

template.png

用于生成 excel 文件的數(shù)據(jù):

        Map<String, Object> data = new HashMap<>();
        Map<String, Object> cls = new HashMap<>();
        data.put("cls", cls);
        cls.put("headmaster", "李景文");
        cls.put("type", "文科班");
        List<Stu> stus = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            Stu stu = new Stu();
            stu.code = String.format("1490001%02d", i + 1);
            stu.name = String.format("%s%s", fName[Math.abs(random.nextInt()) % fName.length], sName[Math.abs(random.nextInt()) % sName.length]);
            stu.gender = String.format("%s", Math.abs(random.nextInt()) % 2 == 0 ? "男" : "女");
            stu.age = Math.abs(random.nextInt()) % 10 + 10;
            stu.phone = String.format("%s%s", "150", Math.abs(random.nextInt()) % 89999999 + 10000000);
            stu.donation = (int) (random.nextDouble() * 10);
            stus.add(stu);
        }
        cls.put("students", stus);

        FileOutputStream fos = new FileOutputStream(new File("template_ins.xlsx"));
        String templatePath = "template.xlsx";
        //根據(jù)模板 templatePath 和數(shù)據(jù) data 生成 excel 文件,寫入 fos 流
        ExcelTemplateUtils.process(data, templatePath, fos);

模板生成效果(數(shù)據(jù)均隨機生成):
Snipaste_2018-08-28_11-32-08.png

實現(xiàn)
這是一個簡單的置換型模板引擎,將指定模板內(nèi)容(Excel文件)中的特定標(biāo)記(單元格內(nèi)的字符串)替換一下便生成了最終需要的業(yè)務(wù)數(shù)據(jù)。

模板分為5個部分:

  • 1~2行:標(biāo)題,2018級1班學(xué)生捐贈名單
  • 3行:班級信息
  • 4行:表頭
  • 5行:學(xué)生列表
  • 6~7行:合計
    模板引擎主要完成兩個事情:
    (1)非列表字段的置換,例如上圖中的${cls.headmaster}, ${cls.students.size()}, ${cls.type}
    (2)列表字段的展開和置換,例如上圖中的學(xué)生列表,在模板中僅占第5行,而在生成文件中,按照列表長度,展開到5~14行,共十條數(shù)據(jù)。
    置換標(biāo)記說明
    (1)非列表字段標(biāo)記。形如${cls.headmaster},替換規(guī)則為:找到對象clsheadmaster屬性值,直接替換到當(dāng)前單元格。
    (2)列表字段標(biāo)記。形如${cls.students[#].name},其中的[#]標(biāo)志著對象students是個數(shù)組(List),替換規(guī)則為:在當(dāng)前單元格所在行下面插入students.size - 1行,然后在第i個插入行的單元格中填入cls.students[i].name

1. 新建 maven 工程,添加 poi 依賴

<dependencies>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.15</version>
        </dependency>
    </dependencies>

2. 新建類 ExcelTemplateEngine.java

(1) 創(chuàng)建靜態(tài)方法 process ,根據(jù)模板生成 excel 文件

/**
 * 根據(jù)模板生成 excel 文件
 * @param data 數(shù)據(jù)
 * @param templatePath 模板文件路徑
 * @param os 生成 excel 輸出流,可保存成文件或返回到前端等
 */
public static void process(Object data, String templatePath, OutputStream os) {
    if (data == null || StringUtil.isEmpty(templatePath)) {
        return;
    }
    try {
        OPCPackage pkg = OPCPackage.open(templatePath);
        XSSFWorkbook wb = new XSSFWorkbook(pkg);
        Iterator<Sheet> iterable = wb.sheetIterator();
        while (iterable.hasNext()) {
            processSheet(data, iterable.next());
        }
        wb.write(os);
        pkg.close();
    } catch (IOException | InvalidFormatException e) {
        e.printStackTrace();
    }
}

因為一個模板中可能有多個工作表(Sheet),所以遍歷每一個 sheet,依次進(jìn)行置換
(2)創(chuàng)建方法 processSheet ,處理單個工作表
處理單個工作表的流程是:
a. 遍歷每個有內(nèi)容的單元格,并獲取到單元格的值cellValue
b. 如果 cellValue 不是字符串類型,則跳過這個單元格,處理下一個單元格
c. 如果這個單元格包含非列表型置換標(biāo)記(形如${cls.headmaster}),直接對該單元格執(zhí)行置換
d. 如果這個單元格包含列表型置換標(biāo)記(形如${cls.students[#].name}),將單元格存入 listRecord 中備用
e. 單元格遍歷完畢
f. 遍歷 listRecord 中存儲的單元格(包含列表型置換標(biāo)記),計算出當(dāng)前單元格所在行下,需要插入的行數(shù)(取決于數(shù)組的元素個數(shù),因為一行之中可能存在多個數(shù)組,因此要去最大值)并插入;同時記錄下當(dāng)前單元格的樣式(列表同一列的樣式相同),當(dāng)前單元格的置換標(biāo)記(例如cls.students#name,代表這一列取 students 內(nèi)元素的 name 屬性)
此時:已完成非列表型字段的置換,已為列表型字段插入所需行,效果如下:

Snipaste_2018-08-28_17-12-48.png

g. 置換列表。再次遍歷 listRecord 中存儲的單元格,從當(dāng)前單元格開始依次向下置換,并應(yīng)用 f 中存儲的樣式。

private static void processSheet(Object data, Sheet sheet) {
    Map<Integer, Map<Integer, Cell>> listRecord = new LinkedHashMap<>();
    int lastRowNum = sheet.getLastRowNum();
    for (int i = lastRowNum; i >= 0; i--) {
        Row row = sheet.getRow(i);
        if (row == null) {
            continue;
        }
        int lastCellNum = row.getLastCellNum();
        for (int j = 0; j < lastCellNum; j++) {
            Cell cell = row.getCell(j);
            if (cell == null) {
                continue;
            }
            try {
                String cellValue = cell.getStringCellValue();
                if (cellValue.matches(".*\\$\\{[\\w.()]+}.*")) {
                    fillCell(cell, cellValue, data);
                } else if (cellValue.matches(".*\\$\\{[\\w.]+\\[#][\\w.]+}.*")) {
                    Map<Integer, Cell> rowRecord = listRecord.computeIfAbsent(i, k -> new HashMap<>());
                    rowRecord.put(j, cell);
                }
            } catch (Exception ignored) {
            
            }
        }
    }
    
    Map<String, List> listInData = new HashMap<>();
    Map<String, CellStyle> listCellStyle = new HashMap<>();
    Map<Cell, String> listCellPath = new HashMap<>();
    listRecord.forEach((rowNum, colMap) -> {
        Pattern p = Pattern.compile("\\$\\{[\\w.\\[#\\]]+}");
        Set<String> listPath = new HashSet<>();
        colMap.forEach((colNum, cell) -> {
            String cellValue = cell.getStringCellValue();
            Matcher m = p.matcher(cellValue);
            if (m.find()) {
                String reg = m.group();
                String regPre = reg.substring(2, reg.indexOf("["));
                String regSuf = reg.substring(reg.lastIndexOf("].") + 2, reg.length() - 1);
                listPath.add(regPre);
                listCellStyle.put(String.format("%s.%s", regPre, regSuf), cell.getCellStyle());
                listCellPath.put(cell, String.format("%s#%s", regPre, regSuf));
            }
        });
        int maxRow = 0;
        for (String s : listPath) {
            Object list = getAttributeByPath(data, s);
            if (list == null) {
                list = new ArrayList<>();
            }
            if (list instanceof List) {
                int len = ((List) list).size();
                maxRow = maxRow > len ? maxRow : len;
                listInData.put(s, ((List) list));
            } else {
                throw new IllegalArgumentException(String.format("%s is not a list but a %s", s, list.getClass().getSimpleName()));
            }
        }
        if (maxRow > 1) {
            int endRow = sheet.getLastRowNum();
            sheet.shiftRows(rowNum + 1, endRow + 1, maxRow - 1);
        }
    });
    
    listRecord.forEach((rowNum, colMap) -> {
        colMap.forEach((colNum, cell) -> {
            String path = listCellPath.get(cell);
            String[] pathData = path.split("#");
            List list = listInData.get(pathData[0]);
            int baseRowIndex = cell.getRowIndex();
            int colIndex = cell.getColumnIndex();
            CellStyle style = listCellStyle.get(String.format("%s.%s", pathData[0], pathData[1]));
            for (int i = 0; i < list.size(); i++) {
                int rowIndex = baseRowIndex + i;
                Row row = sheet.getRow(rowIndex);
                if (row == null) {
                    row = sheet.createRow(rowIndex);
                }
                Cell cellToFill = row.getCell(colIndex);
                if (cellToFill == null) {
                    cellToFill = row.createCell(colIndex);
                }
                cellToFill.setCellStyle(style);
                setCellValue(cellToFill, getAttribute(list.get(i), pathData[1]));
            }
        });
    });
}

3. 剩余方法實現(xiàn)

(1)置換單元格 fillCell(Cell, String, Object) .

/**
 * @param cell 要置換的單元格
 * @param expression 單元格內(nèi)的置換標(biāo)記
 * @param data 數(shù)據(jù)源
 */
private static void fillCell(Cell cell, String expression, Object data) {
    Pattern p = Pattern.compile("\\$\\{[\\w.\\[\\]()]+}");
    Matcher m = p.matcher(expression);
    StringBuffer sb = new StringBuffer();
    while (m.find()) {
        String exp = m.group();
        String path = exp.substring(2, exp.length() - 1);
        Object value = getAttributeByPath(data, path);
        m.appendReplacement(sb, value == null ? "" : value.toString());
    }
    setCellValue(cell, sb.toString());
}

(2)給單元格設(shè)置值 setCellValue(Cell, Object) .

/**
 * @param cell 單元格
 * @param value 值
 */
private static void setCellValue(Cell cell, Object value) {
    if (value == null) {
        cell.setCellValue("");
    } else if (value instanceof Date) {
        cell.setCellValue((Date) value);
    } else if (value instanceof Integer) {
        cell.setCellValue((Integer) value);
    } else if (value instanceof Long) {
        cell.setCellValue((Long) value);
    } else if (value instanceof Double) {
        cell.setCellValue((Double) value);
    } else if (value instanceof Float) {
        cell.setCellValue((Float) value);
    } else if (value instanceof Character) {
        cell.setCellValue((Character) value);
    } else if (value instanceof BigDecimal) {
        cell.setCellValue(((BigDecimal) value).doubleValue());
    } else {
        cell.setCellValue(value.toString());
    }
}

(3)通過反射獲取對象的屬性值 getAttributeByPath(Object, String) .

/**
 * 
 * @param obj 訪問對象
 * @param path 屬性路徑,形如(cls.type, cls.students.size())
 * @return
 */
private static Object getAttributeByPath(Object obj, String path) {
    String[] paths = path.split("\\.");
    Object o = obj;
    for (String s : paths) {
        o = getAttribute(o, s);
    }
    return o;
}

private static Object getAttribute(Object obj, String member) {
    if (obj == null) {
        return null;
    }
    boolean isMethod = member.endsWith("()");
    if (!isMethod && obj instanceof Map) {
        return ((Map) obj).get(member);
    }
    try {
        Class<?> cls = obj.getClass();
        if (isMethod) {
            Method method = cls.getDeclaredMethod(member.substring(0, member.length() - 2));
            return method.invoke(obj);
        } else {
            Field field = cls.getDeclaredField(member);
            field.setAccessible(true);
            return field.get(obj);
        }
    } catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
        e.printStackTrace();
    }
    return null;
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容