Excel处理基础知识
1.1 为什么需要在应用中处理Excel文件?
在企业应用开发中,Excel文件处理是一个非常常见的需求,主要用于以下场景:
数据导入:允许用户通过Excel上传批量数据到系统
数据导出:将系统数据导出为Excel供用户下载分析
报表生成:生成复杂的报表并格式化为Excel
数据交换:作为不同系统间交换数据的媒介
批量数据处理:处理大量结构化数据
1.2 Java中的Excel处理库介绍
Java中处理Excel文件的主要库有以下几种:
1.2.1 Apache POI
Apache POI是Java中使用最广泛的Excel处理库,提供了全面的API来创建、读取和修改Office文档。
优点:
功能全面,支持Excel所有功能
支持.xls (HSSF - Excel 97-2003)和.xlsx (XSSF - Excel 2007+)格式
社区活跃,文档丰富
支持公式计算、图表、合并单元格等高级功能
缺点:
API相对复杂
处理大文件时内存消耗大(尤其是XSSF)
1.2.2 EasyExcel
EasyExcel是阿里巴巴开源的Excel处理库,基于POI,但做了大量优化。
优点:
内存占用低,使用SAX模式读取,避免OOM
API简单易用,注解驱动
读写速度快
适合处理大型Excel文件
缺点:
功能不如POI全面
灵活性相对较低
1.2.3 JExcel
JExcel是另一个处理Excel的Java库。
优点:
API较简单
速度较快
缺点:
仅支持旧版Excel (.xls)格式
不再积极维护
功能有限
1.2.4 Apache POI SXSSF
SXSSF是POI提供的一种流式处理模式,专为处理大型Excel文件设计。
优点:
大大降低内存占用
适合生成大型Excel文件
缺点:
仅支持写入操作,不支持读取
功能比XSSF受限
1.3 Spring Boot中集成Excel处理
Spring Boot本身不提供Excel处理功能,但可以轻松集成上述各种Excel处理库。本指南将主要介绍:
如何在Spring Boot项目中集成Apache POI和EasyExcel
如何实现Excel导入导出的常见功能
如何处理常见问题和优化性能
Spring Boot中Excel处理完全指南:从基础到高级实践
在数据驱动的现代应用中,Excel处理能力是后端开发的必备技能。本文全面解析Spring Boot中Excel操作的12种核心技术方案。
一、基础方案:Apache POI原生集成
1. 添加依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
2. 基础读写操作
// 写入Excel
public void writeExcel(List<User> users, String filePath) throws IOException {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("用户表");
// 创建表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("ID");
headerRow.createCell(1).setCellValue("姓名");
headerRow.createCell(2).setCellValue("年龄");
// 填充数据
int rowNum = 1;
for (User user : users) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(user.getId());
row.createCell(1).setCellValue(user.getName());
row.createCell(2).setCellValue(user.getAge());
}
// 自动调整列宽
for (int i = 0; i < 3; i++) {
sheet.autoSizeColumn(i);
}
try (FileOutputStream fos = new FileOutputStream(filePath)) {
workbook.write(fos);
}
}
}
// 读取Excel
public List<User> readExcel(String filePath) throws IOException {
List<User> users = new ArrayList<>();
try (Workbook workbook = new XSSFWorkbook(new File(filePath))) {
Sheet sheet = workbook.getSheetAt(0);
Iterator<Row> rowIterator = sheet.iterator();
// 跳过表头
if (rowIterator.hasNext()) rowIterator.next();
while (rowIterator.hasNext()) {
Row row = rowIterator.next();
User user = new User();
user.setId((long) row.getCell(0).getNumericCellValue());
user.setName(row.getCell(1).getStringCellValue());
user.setAge((int) row.getCell(2).getNumericCellValue());
users.add(user);
}
}
return users;
}
二、高效方案:EasyExcel处理百万级数据
1. 添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
2. 基于注解的实体映射
@Data
public class UserExcel {
@ExcelProperty("ID")
private Long id;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("年龄")
private Integer age;
@ExcelProperty(value = "创建时间", converter = LocalDateTimeConverter.class)
private LocalDateTime createTime;
}
// 自定义日期转换器
public class LocalDateTimeConverter implements Converter<LocalDateTime> {
@Override
public LocalDateTime convertToJavaData(ReadCellData<?> cellData) {
return LocalDateTime.parse(cellData.getStringValue(),
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
3. 百万数据导出(避免OOM)
@GetMapping("/exportBigData")
public void exportBigData(HttpServletResponse response) {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("百万用户数据", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), UserExcel.class)
.sheet("用户数据")
.doWrite(() -> {
// 分页查询数据(每次查询10000条)
int page = 0;
while (true) {
Pageable pageable = PageRequest.of(page, 10000);
Page<User> userPage = userRepository.findAll(pageable);
if (userPage.isEmpty()) break;
// 转换数据
List<UserExcel> data = userPage.getContent().stream()
.map(user -> {
UserExcel excel = new UserExcel();
BeanUtils.copyProperties(user, excel);
return excel;
}).collect(Collectors.toList());
page++;
return data;
}
return null;
});
}
三、Web集成方案:文件上传下载
1. 文件上传解析
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
return "请选择文件";
}
try {
List<User> users = EasyExcel.read(file.getInputStream())
.head(User.class)
.sheet()
.doReadSync();
userRepository.saveAll(users);
return "导入成功,共处理数据: " + users.size() + "条";
} catch (IOException e) {
return "文件处理失败: " + e.getMessage();
}
}
2. 模板下载
@GetMapping("/downloadTemplate")
public void downloadTemplate(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode("用户导入模板", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// 创建模板数据(带下拉菜单)
List<String> headers = Arrays.asList("ID", "姓名", "年龄", "部门");
List<ExcelSelect> selects = Collections.singletonList(
new ExcelSelect(3, Arrays.asList("研发部", "市场部", "财务部"))
);
EasyExcel.write(response.getOutputStream())
.head(headers)
.registerWriteHandler(new DropDownHandler(selects)) // 添加下拉菜单
.sheet("模板")
.doWrite(Collections.emptyList());
}
// 下拉菜单处理器
public class DropDownHandler implements SheetWriteHandler {
private final List<ExcelSelect> selects;
public DropDownHandler(List<ExcelSelect> selects) {
this.selects = selects;
}
@Override
public void afterSheetCreate(WriteWorkbookContext context) {
Sheet sheet = context.getWriteSheetHolder().getSheet();
DataValidationHelper helper = sheet.getDataValidationHelper();
for (ExcelSelect select : selects) {
// 创建下拉数据源
String range = "D2:D1048576"; // 示例:D列所有行
CellRangeAddressList addressList = new CellRangeAddressList(1, 65535, select.getColIndex(), select.getColIndex());
// 设置下拉选项
DataValidationConstraint constraint = helper.createExplicitListConstraint(
select.getOptions().toArray(new String[0]));
DataValidation validation = helper.createValidation(constraint, addressList);
// 应用验证
sheet.addValidationData(validation);
}
}
}
四、高级技巧:动态模板与复杂报表
1. 动态表头生成
public void exportDynamicColumns(HttpServletResponse response, List<String> columns) {
// 动态构建表头
List<List<String>> head = new ArrayList<>();
for (String col : columns) {
head.add(Collections.singletonList(col));
}
// 动态数据填充
List<List<Object>> data = new ArrayList<>();
for (User user : userRepository.findAll()) {
List<Object> row = new ArrayList<>();
for (String col : columns) {
switch (col) {
case "ID": row.add(user.getId()); break;
case "姓名": row.add(user.getName()); break;
case "部门": row.add(user.getDepartment()); break;
// 更多字段处理...
}
}
data.add(row);
}
// 导出
EasyExcel.write(response.getOutputStream())
.head(head)
.sheet("动态报表")
.doWrite(data);
}
2. 复杂报表合并单元格
public void exportComplexReport() {
String fileName = "复杂报表.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("部门统计").build();
// 设置合并策略
excelWriter.registerWriteHandler(new CellMergeStrategy(
Arrays.asList(
new MergeRange(0, 0, 0, 3), // 第一行合并4列
new MergeRange(1, 1, 0, 0), // 第二行第一列
new MergeRange(1, 1, 1, 3) // 第二行后三列合并
)
));
// 构建表头
List<List<String>> head = Arrays.asList(
Collections.singletonList("公司年度报表"),
Arrays.asList("部门", "Q1", "Q2", "Q3", "Q4")
);
// 填充数据
List<List<Object>> data = new ArrayList<>();
data.add(Arrays.asList("研发部", 150, 180, 200, 220));
data.add(Arrays.asList("市场部", 120, 150, 180, 210));
excelWriter.write(head, writeSheet);
excelWriter.write(data, writeSheet);
}
}
// 自定义合并策略
public class CellMergeStrategy implements RowWriteHandler {
private final List<MergeRange> mergeRanges;
@Override
public void afterRowDispose(WriteSheetContext context) {
Sheet sheet = context.getWriteSheetHolder().getSheet();
for (MergeRange range : mergeRanges) {
sheet.addMergedRegion(new CellRangeAddress(
range.getFirstRow(), range.getLastRow(),
range.getFirstCol(), range.getLastCol()
));
}
}
}
五、性能优化与问题解决方案
1. 内存溢出解决方案
// 使用SXSSFWorkbook处理大数据
try (Workbook workbook = new SXSSFWorkbook(100)) { // 保持100行在内存中
Sheet sheet = workbook.createSheet();
// 写入数据
for (int i = 0; i < 1000000; i++) {
Row row = sheet.createRow(i);
// 填充单元格...
// 定期刷新磁盘
if (i % 1000 == 0) {
((SXSSFSheet) sheet).flushRows(100);
}
}
}
2. 常见问题处理方案
问题类型 | 解决方案 |
---|---|
日期格式不一致 | 自定义Converter统一处理日期格式 |
大文件导出超时 | 分页查询+异步导出,前端轮询结果 |
特殊字符(emoji)乱码 | 使用String sanitized = StringEscapeUtils.escapeHtml4(rawString) 处理 |
公式计算 | 使用FormulaEvaluator 预处理公式结果 |
样式丢失 | 定义CellStyle 缓存池避免重复创建 |
下拉菜单动态数据源 | 使用DVConstraint.createFormulaListConstraint 引用隐藏Sheet数据 |
3. Excel操作最佳实践
读写分离原则:
- 读操作:使用EasyExcel的监听器模式(
ReadListener
) - 写操作:大数据用分页写入,小数据用全量写入
- 读操作:使用EasyExcel的监听器模式(
资源管理三要素:
try (Workbook workbook = ...; InputStream is = ...) { // 操作逻辑 } // 自动关闭资源
样式复用技巧:
private CellStyle createHeaderStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); style.setFont(font); style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); return style; }
六、扩展方案:集成第三方库
1. JExcelAPI(适合.xls格式)
<dependency>
<groupId>net.sourceforge.jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.6.12</version>
</dependency>
2. Apache POI + JXLS模板引擎
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-poi</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.jxls</groupId>
<artifactId>jxls-jexcel</artifactId>
<version>1.0.9</version>
</dependency>
模板示例(template.xlsx):
| 姓名 | 年龄 | 部门 |
|------|------|----------|
| ${u.name} | ${u.age} | ${u.department} |
Java代码:
public void exportWithTemplate(HttpServletResponse response) throws IOException {
try (InputStream is = getClass().getResourceAsStream("/template.xlsx");
OutputStream os = response.getOutputStream()) {
Context context = new Context();
context.putVar("users", userRepository.findAll());
JxlsHelper.getInstance()
.processTemplate(is, os, context);
}
}
最佳实践总结:对于中小数据量(<10万行)推荐使用EasyExcel+注解方案;大数据场景采用POI的SXSSF模式;需要复杂报表时选择JXLS模板引擎。始终记住:在Spring Boot中处理Excel,资源关闭和内存管理是成败关键!
扩展学习: