0. 效果预览
新建一个 excel 文档template.xlsx
,作为模板:
用于生成 excel 文件的数据:
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";
//根据模板 templatePath 和数据 data 生成 excel 文件,写入 fos 流
ExcelTemplateUtils.process(data, templatePath, fos);
模板生成效果(数据均随机生成):实现
这是一个简单的置换型模板引擎,将指定模板内容(Excel文件)中的特定标记(单元格内的字符串)替换一下便生成了最终需要的业务数据。
模板分为5个部分:
- 1~2行:标题,2018级1班学生捐赠名单
- 3行:班级信息
- 4行:表头
- 5行:学生列表
- 6~7行:合计
模板引擎主要完成两个事情:
(1)非列表字段的置换,例如上图中的${cls.headmaster}, ${cls.students.size()}, ${cls.type}
(2)列表字段的展开和置换,例如上图中的学生列表,在模板中仅占第5行,而在生成文件中,按照列表长度,展开到5~14行,共十条数据。
置换标记说明
(1)非列表字段标记。形如${cls.headmaster}
,替换规则为:找到对象cls
的headmaster
属性值,直接替换到当前单元格。
(2)列表字段标记。形如${cls.students[#].name}
,其中的[#]
标志着对象students
是个数组(List),替换规则为:在当前单元格所在行下面插入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) 创建静态方法 process ,根据模板生成 excel 文件
/**
* 根据模板生成 excel 文件
* @param data 数据
* @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,依次进行置换
(2)创建方法 processSheet ,处理单个工作表
处理单个工作表的流程是:
a. 遍历每个有内容的单元格,并获取到单元格的值cellValue
b. 如果 cellValue 不是字符串类型,则跳过这个单元格,处理下一个单元格
c. 如果这个单元格包含非列表型置换标记(形如${cls.headmaster}
),直接对该单元格执行置换
d. 如果这个单元格包含列表型置换标记(形如${cls.students[#].name}
),将单元格存入 listRecord 中备用
e. 单元格遍历完毕
f. 遍历 listRecord 中存储的单元格(包含列表型置换标记),计算出当前单元格所在行下,需要插入的行数(取决于数组的元素个数,因为一行之中可能存在多个数组,因此要去最大值)并插入;同时记录下当前单元格的样式(列表同一列的样式相同),当前单元格的置换标记(例如cls.students#name
,代表这一列取 students 内元素的 name 属性)
此时:已完成非列表型字段的置换,已为列表型字段插入所需行,效果如下:
g. 置换列表。再次遍历 listRecord 中存储的单元格,从当前单元格开始依次向下置换,并应用 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. 剩余方法实现
(1)置换单元格 fillCell(Cell, String, Object) .
/**
* @param cell 要置换的单元格
* @param expression 单元格内的置换标记
* @param data 数据源
*/
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)给单元格设置值 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;
}