工具
EasyExcel 和 Apache POI(为什么 POI 能全套实现还要用 EasyExcel ?原因之一是 EasyExcel 采用流式写入的方式,可以有效地管理内存,适用于处理大数据量的场景。相较于 Apache POI 的传统方法,这可以避免在处理大文件时出现内存不足的问题)
效果图
代码
Maven 依赖(其他版本应该也行,并非要使用该特定版本)
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
POJO
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentVO implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty("学校")
private String schoolName;
@ExcelProperty("班级")
private String className;
@ExcelProperty("姓名")
private String studentName;
}
具体生成过程(注释已标明,为便于理解写 Controller 里了,自用请合理拆分)
@GetMapping("/exportStudentData")
public void exportStudentData(HttpServletResponse response) throws IOException {
/**
* 1、设置响应头,表示这是一个 Excel 工作表文件
* 2、定义返回的文件名
*/
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=students.xlsx");
// 填充假数据,生产环境中替换为 DB 数据即可
List<StudentVO> students = new ArrayList<>();
students.add(new StudentVO("清华大学", "一班", "张三"));
students.add(new StudentVO("清华大学", "一班", "李四"));
students.add(new StudentVO("北京大学", "一班", "赵六"));
students.add(new StudentVO("清华大学", "二班", "王五"));
students.add(new StudentVO("北京大学", "二班", "小明"));
/**
* 创建一个临时文件,写入数据到 Excel
* Sheet 页名称记得替换为实际的业务名称
*/
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
EasyExcel.write(outputStream, StudentVO.class).sheet("学生数据").doWrite(students);
/**
* 使用 Apache POI 进行筛选校验操作
* try-with-resources 语法
*/
try (
InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
XSSFWorkbook workbook = new XSSFWorkbook(inputStream);
) {
// 通过 workbook.getSheetAt(0) 获取的就是工作簿中的第一个工作表
XSSFSheet sheet = workbook.getSheetAt(0);
// 在指定的单元格范围内启用了自动筛选功能,使用户可以通过表头对数据进行筛选,表示:起始列为第 0 列,结束列为第 2 列
sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, 2));
// 动态获取所有学校和班级,Java8 以下替换为传统的 for 循环即可
Set<String> schools = students.stream().map(StudentVO::getSchoolName).collect(Collectors.toSet());
Map<String, Set<String>> schoolClassMap = new HashMap<>();
for (StudentVO student : students) {
/**
* 如果不存在就创建一个新的 HashSet
* 学生的班级名称添加到对应学校的 Set 中
*/
schoolClassMap.computeIfAbsent(student.getSchoolName(), k -> new HashSet<>()).add(student.getSchoolName());
}
// 创建数据验证规则
XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper(sheet);
// 创建单元格区域列表,表示:起始行为第 1 行,结束行为学生列表的最后一行,起始列为第 0 列,结束列为第 0 列
CellRangeAddressList schoolAddressList = new CellRangeAddressList(1, students.size(), 0, 0);
// 将学校名称转换为数组
String[] schoolArray = schools.toArray(new String[0]);
// 创建数据验证规则并应用到指定的单元格区域
dvHelper.createValidation(dvHelper.createExplicitListConstraint(schoolArray), schoolAddressList).setShowErrorBox(true);
// 遍历班级的 Map 集合
for (Map.Entry<String, Set<String>> entry : schoolClassMap.entrySet()) {
// 获取学校名称
String school = entry.getKey();
// 得到该学校的所有班级
Set<String> classes = entry.getValue();
String[] classArray = classes.toArray(new String[0]);
// 遍历学生列表,为每个学生的班级设置数据验证规则
for (int i = 1; i <= students.size(); i++) {
// 如果学校名称相同,为该学生的班级设置数据验证规则
if (sheet.getRow(i).getCell(0).getStringCellValue().equals(school)) {
CellRangeAddressList classAddressList = new CellRangeAddressList(i, i, 1, 1);
// 创建数据验证规则并应用到指定的单元格区域(班级)
dvHelper.createValidation(dvHelper.createExplicitListConstraint(classArray), classAddressList).setShowErrorBox(true);
}
}
}
// 将 Excel 文件写入到输出流
ByteArrayOutputStream finalOutputStream = new ByteArrayOutputStream();
workbook.write(finalOutputStream);
workbook.close();
response.getOutputStream().write(finalOutputStream.toByteArray());
}
}