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

0. 效果預(yù)覽

新建一個(gè) 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ù)均隨機(jī)生成):
Snipaste_2018-08-28_11-32-08.png

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

模板分為5個(gè)部分:

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

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

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

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

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. 剩余方法實(shí)現(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)通過反射獲取對(duì)象的屬性值 getAttributeByPath(Object, String) .

/**
 * 
 * @param obj 訪問對(duì)象
 * @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)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

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