0. 效果預(yù)覽
新建一個(gè) excel 文檔template.xlsx
,作為模板:
用于生成 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ī)生成):實(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ì)象cls
的headmaster
屬性值,直接替換到當(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í):已完成非列表型字段的置換,已為列表型字段插入所需行,效果如下:
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;
}