SpringBoot中Excel处理完全指南分享
目录
- SpringBoot中Excel处理指南
- 1. Excel处理基础知识
- 1.1 为什么需要在应用中处理Excel文件?
- 1.2 Java中的Excel处理库介绍
- 1.3 Spring Boot中集成Excel处理
- 2. 在Spring Boot中集成Excel处理库
- 2.1 集成Apache POI
- 2.2 集成EasyExcel
- 3. 使用Apache POI读取Excel文件
- 3.1 创建数据模型
- 3.2 创建Excel读取服务
- 3.3 创建Controller处理Excel上传
- 3.4 创建html上传页面
- 3.5 处理更复杂的Excel结构
- 4. 使用Apache POI创建和导出Excel文件
- 4.1 创建基本Excel文件
- 4.2 创建导出控制器
- 4.3 创建导出页面
- 4.4 创建复杂的Excel文件
- 4.5 使用模板导出Excel
- 5. 使用EasyExcel处理Excel文件
- 5.1 使用EasyExcel读取Excel
- 5.2 使用EasyExcel导出Excel
- 6. 处理大型Excel文件的策略
- 6.1 使用Apache POI SXSSF模式
- 6.2 使用EasyExcel处理大文件
- 6.3 使用CSV代替Excel
- 6.4 分页导出大型数据集
- 7. 实际应用场景和最佳实践
- 7.1 动态列导出
- 7.2 Excel模板填充
- 7.3 Excel文件校验
- 7.4 统一异常处理
- 8. 性能优化和注意事项
- 8.1 性能优化建议
- 8.2 注意事项
- 总结
SpringBoot中Excel处理指南
1. 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导入导出的常见功能
- 如何处理常见问题和优化性能
2. 在Spring Boot中集成Excel处理库
2.1 集成Apache POI
2.1.1 添加依赖
在pom.XML
文件中添加以下依赖:
<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>
如果使用Gradle,在build.gradle
中添加:
implementation 'org.apache.poi:poi:5.2.3' implementation 'org.apache.poi:poi-ooxml:5.2.3'
2.1.2 创建基本配置类
创建一个配置类来处理Excel相关的配置:
package com.example.excel.config; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; @Configuration public class ExcelConfig { @Bean public MultipartResolver multipartResolver() { CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MB return resolver; } }
2.2 集成EasyExcel
2.2.1 添加依赖
在pom.xml
文件中添加以下依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.2.1</version> </dependency>
如果使用Gradle,在build.gradle
中添加:
implementation 'com.alibaba:easyexcel:3.2.1'
2.2.2 创建配置类
package com.example.excel.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; @Configuration public class EasyExcelConfig { @Bean public MultipartResolver multipartResolver() { CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MB return resolver; } }
3. 使用Apache POI读取Excel文件
3.1 创建数据模型
首先,创建一个模型类来映射Excel中的数据:
package com.example.excel.model; import lombok.Data; @Data public class User { private Long id; private String name; private Integer age; private String email; private String department; }
3.2 创建Excel读取服务
创建一个服务类来处理Excel文件读取:
package com.example.excel.service; import com.example.excel.model.User; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @Service public class ExcelService { public List<User> readUsersFromExcel(MultipartFile file) throws IOException { List<User> userList = new ArrayList<>(); // 获取工作簿 try (InputStream inputStream = file.getInputStream()) { Workbook workbook = WorkbookFactory.create(inputStream); // 获取第一个工作表 Sheet sheet = workbook.getSheetAt(0); // 跳过标题行 Iterator<Row> rowIterator = sheet.rowIterator(); if (rowIterator.hasNext()) { rowIterator.next(); // 跳过标题行 } // 遍历数据行 while (rowIterator.hasNext()) { Row row = rowIterator.next(); User user = new User(); // 读取单元格数据 user.setId((long) row.getCell(0, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).getNumericCellValue()); user.setName(getCellValueAsString(row.getCell(1))); user.setAge((int) row.getCell(2, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).getNumericCellValue()); user.setEmail(getCellValueAsString(row.getCell(3))); user.setDepartment(getCellValueAsString(row.getCell(4))); userList.add(user); } workbook.close(); } return userList; } // 获取单元格的字符串值 private String getCellValueAsString(Cell cell) { if (cell == null) { return ""; } switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case NUMERIC: if (DateUtil.isCellDateFormatted(cell)) { return cell.getDateCellValue().toString(); } else { return String.valueOf((int) cell.getNumericCellValue()); } case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); case FORMULA: return cell.getCellFormula(); default: return ""; } } }
3.3 创建Controller处理Excel上传
创建一个Controller来处理Excel文件上传:
package com.example.excel.controller; import com.example.excel.model.User; import com.example.excel.service.ExcelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; @RestController @RequestMapping("/api/excel") public class ExcelController { @Autowired private ExcelService excelService; @PostMapping("/upload") public ResponseEntity<List<User>> uploadExcel(@RequestParam("file") MultipartFile file) { try { List<User> users = excelService.readUsersFromExcel(file); return ResponseEntity.ok(users); } catch (IOException e) { e.printStackTrace(); return ResponseEntity.badRequest().build(); } } }
3.4 创建HTML上传页面
在src/main/resources/templates
目录下创建upload.html
:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Excel上传</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" > </head> <body> <div class="container mt-5"> <div class="card"> <div class="card-header"> <h3>上传Excel文件</h3> </div> <div cljavascriptass="card-body"> <form id="uploadForm" enctype="multipart/form-data"> <div class="form-group"> <label for="file">选择Excel文件:</label> <input type="file" class="form-control-file" id="file" name="file" accept=".xls,.xlsx"> </div> <button type="button" class="btn btn-primary" onclick="uploadExcel()">上传</button> </form> <div class="mt-4"> <h4>上传结果:</h4> <div id="resultContainer"></div> </div> </div> </div> </div> <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> <script> function uploadExcel() { var formData = new FormData(document.getElementById('uploadForm')); $.AJAX({ url: '/api/excel/upload', type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { var resultHtml = '<table class="table table-striped">' + '<thead><tr><th>ID</th><th>姓名</th><th>年龄</th><th>邮箱</th><th>部门</th></tr></thead>' + '<tbody>'; for (var i = 0; i < response.length; i++) { var user = response[i]; resultHtml += '<tr>' + '<td>' + user.id + '</td>' + '<td>' + user.name + '</td>' + '<td>' + user.age + '</td>' + '<td>' + user.email + '</td>' + '<td>' + user.department + '</td>' + '</tr>'; } resultHtml += '</tbody></table>'; $('#resultContainer').html(resultHtml); }, error: function(error) { $('#resultContainer').html('<div class="alert alert-danger">上传失败: ' + error.responseText + '</div>'); } }); } </script> </body> </html>
3.5 处理更复杂的Excel结构
在实际应用中,Excel结构可能更复杂,如多个工作表、合并单元格、公式等。以下是处理这些情况的示例:
public List<Department> readComplexExcel(MultipartFile file) throws IOException { List<Department> departments = new ArrayList<>(); try (InputStream inputStream = file.getInputStream()) { Workbook workbook = WorkbookFactory.create(inputStream); // 读取部门信息(第一个工作表) Sheet departmentSheet = workbook.getSheetAt(0); for (int i = 1; i <= departmentSheet.getLastRowNum(); i++) { Row row = departmentSheet.getRow(i); if (row == null) continue; Department department = new Department(); department.setId((long) row.getCell(0).getNumericCellValue()); department.setName(row.getCell(1).getStringCellValue()); department.setManager(row.getCell(2).getStringCellValue()); department.setEmployees(new ArrayList<>()); departments.add(department); } // 读取员工信息(第二个工作表) Sheet employeeSheet = workbook.getSheetAt(1); for (int i = 1; i <= employeeSheet.getLastRowNum(); i++) { Row row = employeeSheet.getRow(i); if (row == null) continue; User employee = new User(); employee.setId((long) row.getCell(0).getNumericCellValue()); employee.setName(row.getCell(1).getStringCellValue()); employee.setAge((int) row.getCell(2).getNumericCellValue()); employee.setEmail(row.getCell(3).getStringCellValue()); // 获取部门ID并关联到相应部门 long departmentId = (long) row.getCell(4).getNumericCellValue(); for (Department dept : departments) { if (dept.getId() == departmentId) { dept.getEmployees().add(employee); break; } } } workbook.close(); } return departments; }
4. 使用Apache POI创建和导出Excel文件
4.1 创建基本Excel文件
以下是一个创建简单Excel文件的示例:
package com.example.excel.service; import com.example.excel.model.User; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @Service public class ExcelExportService { public ByteArrayInputStream exportUsersToExcel(List<User> users) throws IOException { try (Workbook workbook = new XSSFWorkbook()) { // 创建工作表 Sheet sheet = workbook.createSheet("用户数据"); // 创建表头样式 Font headerFont = workbook.createFont(); headerFont.setBold(true); headerFont.setColor(IndexedColors.BLUE.getIndex()); CellStyle headerCellStyle = workbook.createCellStyle(); headerCellStyle.setFont(headerFont); headerCellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex()); headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); headerCellStyle.setBorderBottom(BorderStyle.THIN); headerCellStyle.setBorderTop(BorderStyle.THIN); headerCellStyle.setBorderRight(BorderStyle.THIN); headerCellStyle.setBorderLeft(BorderStyle.THIN); // 创建表头行 Row headerRow = sheet.createRow(0); // 创建表头单元格 Cell cell0 = headerRow.createCell(0); cell0.setCellValue("ID"); cell0.setCellStyle(headerCellStyle); Cell cell1 = headerRow.createCell(1); cell1.setCellValue("姓名"); cell1.setCellStyle(headerCellStyle); Cell cell2 = headerRow.createCell(2); cell2.setCellValue("年龄"); cell2.setCellStyle(headerCellStyle); Cell cell3 = headerRow.createCell(3); cell3.setCellValue("邮箱"); cell3.setCellStyle(headerCellStyle); Cell cell4 = headerRow.createCell(4); cell4.setCellValue("部门"); cell4.setCellStyle(headerCellStyle); // 设置数据单元格样式 CellStyle dataCellStyle = workbook.createCellStyle(); dataCellStyle.setBorderBottom(BorderStyle.THIN); dataCellStyle.setBorderTop(BorderStyle.THIN); dataCellStyle.setBorderRight(BorderStyle.THIN); dataCellStyle.setBorderLeft(BorderStyle.THIN); // 创建数据行 int rowIdx = 1; for (User user : users) { Row row = sheet.createRow(rowIdx++); Cell idCell = row.createCell(0); idCell.setCellValue(user.getId()); idCell.setCellStyle(dataCellStyle); Cell nameCell = row.createCell(1); nameCell.setCellValue(user.getName()); nameCell.setCellStyle(dataCellStyle); Cell ageCell = row.createCell(2); ageCell.setCellValue(user.getAge()); ageCell.setCellStyle(dataCellStyle); Cell emailCell = row.createCell(3); emailCell.setCellValue(user.getEmail()); emailCell.setCellStyle(dataCellStyle); Cell deptCell = row.createCell(4); deptCell.setCellValue(user.getDepartment()); deptCell.setCellStyle(dataCellStyle); } // 自动调整列宽 for (int i = 0; i < 5; i++) { sheet.autoSizeColumn(i); } // 写入ByteArrayOutputStream ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); workbook.write(outputStream); return new ByteArrayInputStream(outputStream.toByteArray()); } } }
4.2 创建导出控制器
package com.example.excel.controller; import com.example.excel.model.User; import com.example.excel.service.ExcelExportService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @RestController @RequestMapping("/api/excel") public class ExcelExportController { @Autowired private ExcelExportService excelExportService; @GetMapping("/export") public ResponseEntity<InputStreamResource> exportUsers() throws IOException { // 生成示例数据 List<User> users = getTestUsers(); // 生成Excel文件 ByteArrayInputStream in = excelExportService.exportUsersToExcel(users); // 设置HTTP头 HttpHeaders headers = new HttpHeaders(); headers.add("Content-Disposition", "attachment; filename=users.xlsx"); // 返回Excel文件 return ResponseEntity .ok() .headers(headers) .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) .body(new InputStreamResource(in)); } // 生成测试用户数据 private List<User> getTestUsers() { List<User> users = new ArrayList<>(); User user1 = new User(); user1.setId(1L); user1.setName("张三"); user1.setAge(28); user1.setEmail("zhangsan@example.com"); user1.setDepartment("研发部"); users.add(user1); User user2 = new User(); user2.setId(2L); user2.setName("李四"); user2.setAge(32); user2.setEmail("lisi@example.com"); user2.setDepartment("市场部"); users.add(user2); User user3 = new User(); user3.setId(3L); user3.setName("王五"); user3.setAge(45); user3.setEmail("wangwu@example.com"); user3.setDepartment("行政部"); users.add(user3); User user4 = new User(); user4.setId(4L); user4.setName("赵六"); user4.setAge(36); user4.setEmail("zhaoliu@example.com"); user4.setDepartment("财务部"); users.add(user4); User user5 = new User(); user5.setId(5L); user5.setName("钱七"); user5.setAge(29); user5.setEmail("qianqi@example.com"); user5.setDepartment("人力资源部"); users.add(user5); return users; } }
4.3 创建导出页面
在src/main/resources/templates
目录下创建export.html
:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Excel导出</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="external nofollow" rel="external nofollow" > </head> <body> <div class="container mt-5"> <div class="card"> <div class="card-header"> <h3>导出Excel文件</h3> </div> <div class="card-body"> <p>点击下面的按钮导出用户数据到Excel文件:</p> <a href="/api/excel/export" rel="external nofollow" class="btn btn-primary">导出用户数据</a> </div> </div> </div> </body> </html>
4.4 创建复杂的Excel文件
以下是一个创建更复杂Excel文件的示例,包含多个工作表、合并单元格、公式等:
public ByteArrayInputStream exportComplexExcel(List<Department> departments) throws IOException { try (Workbook workbook = new XSSFWorkbook()) { // 创建字体和样式 Font headerFont = workbook.createFont(); headerFont.setBold(true); headerFont.setFontHeightInPoints((short) 14); CellStyle headerStyle = workbook.createCellStyle(); headerStyle.setFont(headerFont); headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_phpFOREGROUND); headerStyle.setAlignment(HorizontalAlignment.CENTER); CellStyle titleStyle = workbook.createCellStyle(); Font titleFont = workbook.createFont(); titleFont.setBold(true); titleFont.setFontHeightInPoints((short) 16); titleStyle.setFont(titleFont); titleStyle.setAlignment(HorizontalAlignment.CENTER); // 创建汇总表 Sheet summarySheet = workbook.createSheet("部门汇总"); // 创建标题行 Row titleRow = summarySheet.createRow(0); Cell titleCell = titleRow.createCell(0); titleCell.setCellValue("公司部门人员统计"); titleCell.setCellStyle(titleStyle); // 合并标题单元格 summarySheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 3)); // 创建表头 Row headerRow = summarySheet.createRow(1); String[] headers = {"部门ID", "部门名称", "部门经理", "员工数量"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 填充部门数据 int rowIdx = 2; int totalEmployees = 0; for (Department dept : departments) { Row row = summarySheet.createRow(rowIdx++); row.createCell(0).setCellValue(dept.getId()); row.createCell(1).setCellValue(dept.getName()); row.createCell(2).setCellValue(dept.getManager()); row.createCell(3).setCellValue(dept.getEmployees().size()); totalEmployees += dept.getEmployees().size(); // 为每个部门创建单独的工作表 Sheet deptSheet = workbook.createSheet(dept.getName()); // 创建部门表头 Row deptHeaderRow = deptSheet.createRow(0); Cell deptTitleCell = deptHeaderRow.createCell(0); deptTitleCell.setCellValue(dept.getName() + " - 员工列表"); deptTitleCell.setCellStyle(titleStyle); deptSheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 4)); // 员工表头 Row empHeaderRow = deptSheet.createRow(1); String[] empHeaders = {"员工ID", "姓名", "年龄", "邮箱", "入职年限"}; for (int i = 0; i < empHeaders.length; i++) { Cell cell = empHeaderRow.createCell(i); cell.setCellValue(empHeaders[i]); cell.setCellStyle(headerStyle); } // 填充员工数据 int empRowIdx = 2; for (User emp : dept.getEmployees()) { Row empRow = deptSheet.createRow(empRowIdx++); empRow.createCell(0).setCellValue(emp.getId()); empRow.createCell(1).setCellValue(emp.getName()); empRow.createCell(2).setCellValue(emp.getAge()); empRow.createCell(3).setCellValue(emp.getEmail()); // 使用公式计算入职年限(假设年龄减去25) Cell tenureCell = empRow.createCell(4); tenureCell.setCellFormula("C" + empRowIdx + "-25"); } // 自动调整列宽 for (int i = 0; i < 5; i++) { deptSheet.autoSizeColumn(i); } } // 创建总计行 Row totalRow = summarySheet.createRow(rowIdx); Cell totalLabelCell = totalRow.createCell(0); totalLabelCell.setCellValue("总计"); totalLabelCell.setCellStyle(headerStyle); // 合并总计标签单元格 summarySheet.addMergedRegion(new CellRangeAddress(rowIdx, rowIdx, 0, 2)); Cell totalValueCell = totalRow.createCell(3); totalValueCell.setCellValue(totalEmployees); totalValueCell.setCellStyle(headerStyle); // 自动调整列宽 for (int i = 0; i < 4; i++) { summarySheet.autoSizeColumn(i); } // 添加图表 XSSFSheet chartSheet = (XSSFSheet) workbook.createSheet("部门统计图"); // 复制部门数据到图表数据表 Row chartHeaderRow = chartSheet.createRow(0); chartHeaderRow.createCell(0).setCellValue("部门"); chartHeaderRow.createCell(1).setCellValue("员工数"); int chartRowIdx = 1; for (Department dept : departments) { Row row = chartSheet.createRow(chartRowIdx++); row.createCell(0).setCellValue(dept.getName()); row.createCell(1).setCellValue(dept.getEmployees().size()); } // 创建图表和数据序列 XSSFDrawing drawing = chartSheet.createDrawingPatriarch(); XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 4, 0, 15, 15); XSSFChart chart = drawing.createChart(anchor); chart.setTitleText("部门人员分布"); chart.setTitleOverlay(false); XDDFChartLegend legend = chart.getOrAddLegend(); legend.setPosition(LegendPosition.RIGHT); // X轴和Y轴 XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM); bottomAxis.setTitle("部门"); XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT); leftAxis.setTitle("员工数"); // 创建数据源 XDDFDataSource<String> departments = XDDFDataSourcesFactory.fromStringCellRange( chartSheet, new CellRangeAddress(1, chartRowIdx - 1, 0, 0)); XDDFNumericalDataSource<Double> values = XDDFDataSourcesFactory.fromNumericCellRange( chartSheet, new CellRangeAddress(1, chartRowIdx - 1, 1, 1)); // 创建柱状图 XDDFBarChartData barChart = (XDDFBarChartData) chart.createData( ChartTypes.BAR, bottomAxis, leftAxis); barChart.setVaryColors(true); XDDFBarChartData.Series series = (XDDFBarChartData.Series) barChart.addSeries(departments, values); series.setTitle("员工数", null); chart.plot(barChart); // 写入ByteArrayOutputStream ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); workbook.write(outputStream); return new ByteArrayInputStream(outputStream.toByteArray()); } }
注意:上面的图表代码需要添加以下依赖:
<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml-full</artifactId> <version>5.2.3</version> </dependency>
4.5 使用模板导出Excel
在某些场景下,我们需要基于预定义的Excel模板生成文件,以下是一个示例:
public ByteArrayInputStream exportFromTemplate(List<User> users) throws IOException { // 加载模板文件 try (InputStream templateStream = getClass().getResourceAsStream("/templates/user_template.xlsx"); Workbook workbook = WorkbookFactory.create(templateStream)) { Sheet sheet = workbook.getSheetAt(0); // 从第二行开始填充数据(第一行是表头) int rowIdx = 1; for (User user : users) { Row row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(user.getId()); row.createCell(1).setCellValue(user.getName()); row.createCell(2).setCellValue(user.getAge()); row.createCell(3).setCellValue(user.getEmail()); row.createCell(4).setCellValue(user.getDepartment()); } // 更新模板中的日期单元格(假设在A1位置) Row headerRow = sheet.getRow(0); if (headerRow.getCell(6) != null) { Cell dateCell = headerRow.getCell(6); dateCell.setCellValue(new Date()); } // 自动调整列宽 for (int i = 0; i < 5; i++) { sheet.autoSizeColumn(i); } // 写入ByteArrayOutputStream ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); workbook.write(outputStream); return new ByteArrayInputStream(outputStream.toByteArray()); } }
5. 使用EasyExcel处理Excel文件
EasyExcel是阿里巴巴开源的基于POI的Excel处理工具,相比原生POI,它提供了更简洁的API,并且在处理大文件时有明显的性能优势。
5.1 使用EasyExcel读取Excel
5.1.1 创建数据模型
使用EasyExcel时,通常使用注解来映射Excel列:
package com.example.excel.model; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.format.DateTimeFormat; import lombok.Data; import java.util.Date; @Data public class Employee { @ExcelProperty("员工ID") private Long id; @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("邮箱") private String email; @ExcelProperty("部门") private String department; @ExcelProperty("入职日期") @DateTimeFormat("yyyy-MM-dd") private Date hireDate; @ExcelProperty("薪资") private Double salary; }
5.1.2 创建读取监听器
EasyExcel采用事件模式读取Excel,需要创建一个监听器来处理读取的数据:
package com.example.excel.listener; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.example.excel.model.Employee; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; @Slf4j public class EmployeeReadListener extends AnalysisEventListener<Employee> { /** * 用于暂存读取的数据 */ private List<Employee> employeeList = new ArrayList<>(); /** * 每读取一行数据就会调用一次invoke方法 */ @Override public void invoke(Employee employee, AnalysisContext context) { log.info("读取到一条数据: {}", employee); employeeList.add(employee); // 达到BATCH_COUNT时,需要存储一次数据库,防止数据几万条数据在内存,容易OOM if (employeeList.size() >= 5000) { saveData(); // 清理内存 employeeList.clear(); } } /** * 所有数据解析完成后调用此方法 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { // 确保最后一批数据被保存 saveData(); log.info("所有数据解析完成!"); } /** * 保存数据,这里只是打印,实际应用中可以将数据存入数据库 */ private void saveData() { log.info("{}条数据,开始保存数据库!", employeeList.size()); // 这里可以调用持久层完成数据入库 log.info("存储数据库成功!"); } /** * 获取读取到的数据 */ public List<Employee> getEmployeeList() { return employeeList; } }
5.1.3 创建Excel读取服务
package com.example.excel.service; import com.alibaba.excel.EasyExcel; import com.example.excel.listener.EmployeeReadListener; import com.example.excel.model.Employee; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; @Slf4j @Service public class EasyExcelService { public List<Employee> readEmployeeData(MultipartFile file) throws IOException { EmployeeReadListener listener = new EmployeeReadListener(); EasyExcel.read(file.getInputStream(), Employee.class, listener).sheet().doRead(); return listener.getEmployeeList(); } }
5.1.4 创建Controller
package com.example.excel.controller; import com.example.excel.model.Employee; import com.example.excel.service.EasyExcelService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; @Slf4j @RestController @RequestMapping("/api/easyexcel") public class EasyExcelController { @Autowired private EasyExcelService easyExcelService; @PostMapping("/upload") public ResponseEntity<List<Employee>> uploadExcel(@RequestParam("file") MultipartFile file) { try { List<Employee> employees = easyExcelService.readEmployeeData(file); return ResponseEntity.ok(employees); } catch (IOException e) { log.error("Excel读取失败", e); return ResponseEntity.badRequest().build(); } } }
5.2 使用EasyExcel导出Excel
5.2.1 简单导出示例
package com.example.excel.service; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.example.excel.model.Employee; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.List; @Service public class EasyExcelExportService { /** * 导出员工数据到Excel文件 */ public void exportEmployees(List<Employee> employees, OutputStream outputStream) { EasyExcel.write(outputStream, Employee.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自动调整列宽 .sheet("员工数据") .doWrite(employees); } /** * 导出员工数据到指定文件 */ public void exportEmployeesToFile(List<Employee> employees, String fileName) throws IOException { // 确保目录存在 File file = new File(fileName); if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } EasyExcel.write(fileName, Employee.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet("员工数据") .doWrite(employees); } /** * 导出多个Sheet的Excel */ public void exportMultipleSheets(List<List<Employee>> departmentEmployees, List<String> sheetNames, OutputStream outputStream) { // 创建ExcelWriter try (var excelWriter = EasyExcel.write(outputStream, Employee.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .build()) { // 同一个对象多个Sheet写入 for (int i = 0; i < departmentEmployees.size(); i++) { // 获取Sheet名称 String sheetName = i < sheetNames.size() ? sheetNames.get(i) : "Sheet" + (i + 1); // 创建新的Sheet var writeSheet = EasyExcel.writerSheet(i, sheetName).build(); // 写入数据 excelWriter.write(departmentEmployees.get(i), writeSheet); } } } }
5.2.2 创建Controller
package com.example.excel.controller; import com.example.excel.model.Employee; import com.example.excel.service.EasyExcelExportService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; @RestController @RequestMapping("/api/easyexcel") public class EasyExcelExportController { @Autowired private EasyExcelExportService exportService; @GetMapping("/export") public void exportEmployees(HttpServletResponse response) throws IOException { // 设置响应内容 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); // 设置文件名 String fileName = URLEncoder.encode("员工数据", StandardCharsets.UTF_8).replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 获取测试数据 List<Employee> employees = getTestEmployees(); // 导出Excel exportService.exportEmployees(employees, response.getOutputStream()); } @GetMapping("/export-multiple-sheets") public void exportMultipleSheets(HttpServletResponse response) throws IOException { // 设置响应内容 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); // 设置文件名 String fileName = URLEncoder.encode("部门员工数据", StandardCharsets.UTF_8).replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); // 获取测试数据 - 三个部门的员工 List<List<Employee>> departmentEmployees = new ArrayList<>(); departmentEmployees.add(getEmployeesByDepartment("研发部")); departmentEmployees.add(getEmployeesByDepartment("市场部")); departmentEmployees.add(getEmployeesByDepartment("行政部")); // Sheet名称 List<String> sheetNames = Arrays.asList("研发部员工", "市场部员工", "行政部员工"); // 导出Excel exportService.exportMultipleSheets(departmentEmployees, sheetNames, response.getOutputStream()); } /** * 生成测试员工数据 */ private List<Employee> getTestEmployees() { List<Employee> employees = new ArrayList<>(); // 添加测试数据 for (int i = 1; i <= 10; i++) { Employee employee = new Employee(); employee.setId((long) i); employee.setName("员工" + i); employee.setAge(20 + i); employee.setEmail("employee" + i + "@example.com"); employee.setDepartment(i % 3 == 0 ? "研发部" : (i % 3 == 1 ? "市场部" : "行政部")); employee.setHireDate(new Date()); employee.setSalary(5000.0 + i * 1000); employees.add(employee); } return employees; } /** * 根据部门获取员工 */ private List<Employee> getEmployeesByDepartment(String department) { List<Employee> allEmployees = getTestEmployees(); List<Employee> departmentEmployees = new ArrayList<>(); for (Employee employee : allEmployees) { if (department.equals(employee.getDepartment())) { departmentEmployees.add(employee); } } return departmentEmployees; } // ... 5.2.3 使用自定义样式和复杂表头 /** * 导出自定义样式的Excel */ public void exportWithCustomStyle(List<Employee> employees, OutputStream outputStream) { // 设置自定义拦截器来处理样式 EasyExcel.write(outputStream, Employee.class) // 自动调整列宽 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 设置表头样式 .registerWriteHandler(new AbstractRowHeightStyleStrategy() { @Override protected void setHeadColumnHeight(Row row, int relativeRowIndex) { // 设置表头行高 row.setHeight((short) 500); } @Override protected void setContentColumnHeight(Row row, int relativeRowIndex) { // 设置内容行高 row.setHeight((short) 400); } }) // 设置单元格样式 .registerWriteHandler(new CellWriteHandler() { @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 设置表头样式 if (isHead) { Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); font.setFontHeightInPoints((short) 12); font.setColor(IndexedColors.WHITE.getIndex()); style.setFont(font); style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); cell.setCellStyle(style); } } @Override public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 在这里可以根据数据内容设置样式 } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 内容行的样式 if (!isHead) { // 偶数行设置背景色 if (relativeRowIndex % 2 == 0) { Workbook workbook = writeSheetHolder.getSheet().getWorkbook(); CellStyle style = workbook.createCellStyle(); style.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); cell.setCellStyle(style); } } } }) .sheet("员工数据") .doWrite(employees); } /** * 导出复杂表头的Excel */ public void exportWithComplexHead(List<Employee> employees, OutputStream outputStream) { // 构建复杂表头 List<List<String>> head = new ArrayList<>(); // 第一列 ID List<String> head1 = new ArrayList<>(); head1.add("基本信息"); head1.add("员工ID"); head.add(head1); // 第二列 姓名 List<String> head2 = new ArrayList<>(); head2.add("基本信息"); head2.add("姓名"); head.add(head2); // 第三列 年龄 List<String> head3 = new ArrayList<>(); head3.add("基本信息"); head3.add("年龄"); head.add(head3); // 第四列 邮箱 List<String> head4 = new ArrayList<>(); head4.add("联系方式"); head4.add("邮箱"); head.add(head4); // 第五列 部门 List<String> head5 = new ArrayList<>(); head5.add("工作信息"); head5.add("部门"); head.add(head5); // 第六列 入职日期 List<String> head6 = new ArrayList<>(); head6.add("工作信息"); head6.add("入职日期"); head.add(head6); // 第七列 薪资 List<String> head7 = new ArrayList<>(); head7.add("薪资信息"); head7.add("月薪(元)"); head.add(head7); // 将数据转为List<List<Object>>格式 List<List<Object>> dataList = new ArrayList<>(); for (Employee employee : employees) { List<Object> data = new ArrayList<>(); data.add(employee.getId()); data.add(employee.getName()); data.add(employee.getAge()); data.add(employee.getEmail()); data.add(employee.getDepartment()); data.add(employee.getHireDate()); data.add(employee.getSalary()); dataList.add(data); } // 写入Excel EasyExcel.write(outputStream) .head(head) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet("员工数据") .doWrite(dataList); } }
6. 处理大型Excel文件的策略
6.1 使用Apache POI SXSSF模式
SXSSF(Streaming Xlsx Writer)是POI提供的流式写入方式,可以大大减少内存使用:
public void exportLargeExcel(String fileName, int rowCount) throws IOException { try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) { // 100表示内存中保留的行数 Sheet sheet = workbook.createSheet("大数据"); // 创建表头 Row headerRow = sheet.createRow(0)编程客栈; for (int i = 0; i < 10; i++) { headerRow.createCell(i).setCellValue("列 " + (i + 1)); } // 创建数据行 for (int i = 0; i < rowCount; i++) { Row row = sheet.createRow(i + 1); for (int j = 0; j < 10; j++) { row.createCell(j).setCellValue("数据 " + (i + 1) + "-" + (j + 1)); } // 每生成10000行清理一次临时文件 if (i % 10000 == 0) { ((SXSSFSheet)sheet).flushRows(); } } // 写入文件 try (FileOutputStream outputStream = new FileOutputStream(fileName)) { workbook.write(outputStream); } // 清理临时文件 workbook.dispose(); } }
注意事项:
- 使用完毕后一定要调用
dispose()
方法清理临时文件 - SXSSF仅支持写入操作,不支持读取
- 不支持某些高级特性(如合并单元格等)
6.2 使用EasyExcel处理大文件
EasyExcel在设计上就考虑了大文件处理,采用SAX方式逐行读取,内存占用小:
// 读取大文件 public void readLargeExcel(String fileName) { // 使用SAX方式读取 EasyExcel.read(fileName, Employee.class, new EmployeeReadListener()) .sheet() .doRead(); } // 写入大文件 public void writeLargeExcel(String fileName, int batchSize) { // 分批获取数据 try (ExcelWriter excelWriter = EasyExcel.write(fileName, Employee.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .build()) { // 获取WriteSheet对象 WriteSheet writeSheet = EasyExcel.writerSheet("员工数据").build(); // 模拟分批获取数据 int totalCount = 100000; // 总数据量 for (int i = 0; i < totalCount; i += batchSize) { // 获取当前批次数据 List<Employee> data = getBatchData(i, Math.min(i + batchSize, totalCount)); // 写入Excel excelWriter.write(data, writeSheet); } } } // 模拟分批获取数据 private List<Employee> getBatchData(int start, int end) { List<Employee> list = new ArrayList<>(); for (int i = start; i < end; i++) { Employee employee = new Employee(); employee.setId((long) i); employee.setName("员工" + i); employee.setAge(20 + (i % 20)); employee.setEmail("employee" + i + "@example.com"); employee.setDepartment(i % 3 == 0 ? "研发部" : (i % 3 == 1 ? "市场部" : "行政部")); employee.setHireDate(new Date()); employee.setSalary(5000.0 + (i % 10) * 1000); list.add(employee); } return list; }
6.3 使用CSV代替Excel
对于极大的数据集,考虑使用CSV格式替代Excel:
public void exportToCsv(List<Employee> employees, String fileName) throws IOException { try (FileWriter writer = new FileWriter(fileName); CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT .withHeader("ID", "姓名", "年龄", "邮箱", "部门", "入职日期", "薪资"))) { for (Employee employee : employees) { csvPrinter.printRecord( employee.getId(), employee.getName(), employee.getAge(), employee.getEmail(), employee.getDepartment(), employee.getHireDate(), employee.getSalary() ); } csvPrinter.flush(); } }
注意:使用CSV需要添加依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.9.0</version> </dependency>
6.4 分页导出大型数据集
对于Web应用中需要导出的大型数据集,可以考虑分页导出:
@GetMapping("/export/paged") public ResponseEntity<String> exportPaged() { // 生成唯一任务ID String taskId = UUID.randomUUID().toString(); // 启动异步任务 CompletableFuture.runAsync(() -> { try { // 导出文件路径 String filePath = "/temp/" + taskId + ".xlsx"; // 分页查询数据并写入Excel int pageSize = 1000; int totalPages = getTotalPages(pageSize); try (ExcelWriter excelWriter = EasyExcel.write(filePath, Employee.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .build()) { WriteSheet writeSheet = EasyExcel.writerSheet("员工数据").build(); // 分页导出 for (int pageNum = 0; pageNum < totalPages; pageNum++) { // 从数据库分页查询数据 List<Employee> pageData = getPageData(pageNum, pageSize); // 写入Excel excelWriter.write(pageData, writeSheet); // 更新进度 updateExportProgress(taskId, (pageNum + 1) * 100 / totalPages); } } // 更新导出状态为完成 updateExportStatus(taskId, "COMPLETED", filePath); } catch (Exception e) { // 更新导出状态为失败 updateExportStatus(taskId, "FAILED", null); } }); // 返回任务ID return ResponseEntity.ok(taskId); } @GetMapping("/export/status/{taskId}") public ResponseEntity<Map<String, Object>> getExportStatus(@PathVariable String taskId) { // 获取任务状态 Map<String, Object> status = getTaskStatus(taskId); return ResponseEntity.ok(status); } @GetMapping("/export/download/{taskId}") public ResponseEntity<Resource> downloadExportedFile(@PathVariable String taskId) { // 获取导出文件路径 String filePath = getExportedFilePath(taskId); if (filePath == null) { return ResponseEntity.notFound().build(); } // 创建文件资源 Resource resource = new FileSystemResource(filePath); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=employee_data.xlsx") .contentType(MediaType.parseMediaType("application/vnd.ms-excel")) .body(resource); }
7. 实际应用场景和最佳实践
7.1 动态列导出
在某些业务场景中,需要根据用户选择动态确定导出的列:
public ByteArrayInputStream exportDynamicColumns(List<Employee> employees, List<String> selectedColumns) throws IOException { // 定义所有可能的列 Map<String, String> allColumns = new HashMap<>(); allColumns.put("id", "员工ID"); allColumns.put("name", "姓名"); allColumns.put("age", "年龄"); allColumns.put("email", "邮箱"); allColumns.put("department", "部门"); allColumns.put("hireDate", "入职日期"); allColumns.put("salary", "薪资"); try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("员工数据"); // 创建表头行 Row headerRow = sheet.createRow(0); // 设置表头样式 CellStyle headerStyle = workbook.createCellStyle(); Font headerFont = workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); // 填充表头 int colIdx = 0; for (String column : selectedColumns) { if (allColumns.containsKey(column)) { Cell cell = headerRow.createCell(colIdx++); cell.setCellValue(allColumns.get(column)); cell.setCellStyle(headerStyle); } } // 填充数据 int rowIdx = 1; for (Employee employee : employees) { Row row = sheet.createRow(rowIdx++); colIdx = 0; for (String column : selectedColumns) { Cell cell = row.createCell(colIdx++); // 根据列名设置单元格值 switch (column) { case "id": cell.setCellValue(employee.getId()); break; case "name": cell.setCellValue(employee.getName()); break; case "age": 编程客栈 cell.setCellValue(employee.getAge()); break; case "email": cell.setCellValue(employee.getEmail()); break; case "department": cell.setCellValue(employee.getDepartment()); break; case "hireDate": if (employee.getHireDate() != null) { cell.setCellValue(employee.getHireDate()); // 设置日期格式 CellStyle dateStyle = workbook.createCellStyle(); CreationHelper createHelper = workbook.getCreationHelper(); dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-mm-dd")); cell.setCellStyle(dateStyle); } break; case "salary": cell.setCellValue(employee.getSalary()); break; } } } // 自动调整列宽 for (int i = 0; i < selectedColumns.size(); i++) { sheet.autoSizeColumn(i); } // 输出 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); workbook.write(outputStream); return new ByteArrayInputStream(outputStream.toByteArray()); } }
7.2 Excel模板填充
使用Freemarker或其他模板引擎生成Excel:
public ByteArrayInputStream fillTemplate(Map<String, Object> data) throws Exception { // 加载模板 Configuration cfg = new Configuration(Configuration.VERSION_2_3_30); cfg.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "templates"); cfg.setDefaultEncoding("UTF-8"); // 获取模板 Template template = cfg.getTemplate("excel_template.ftl"); // 输出目录 File tempDir = new File(System.getProperty("java.io.tmpdir")); File tempFile = new File(tempDir, "temp_" + System.currentTimeMillis() + ".xlsx"); // 填充模板 try (Writer out = new FileWriter(tempFile)) { template.process(data, out); } // 读取填充后的文件 try (FileInputStream fis = new FileInputStream(tempFile)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) > -1) { baos.write(buffer, js0, len); } baos.flush(); // 删除临时文件 tempFile.delete(); return new ByteArrayInputStream(baos.toByteArray()); } }
7.3 Excel文件校验
在导入Excel文件前进行数据校验:
public class ExcelValidationListener extends AnalysisEventListener<Employee> { private List<Employee> validEmployees = new ArrayList<>(); private List<Map<String, Object>> errorRecords = new ArrayList<>(); private int rowIndex = 1; // 从1开始,0是表头 @Override public void invoke(Employee employee, AnalysisContext context) { rowIndex++; // 验证数据 List<String> errors = validateEmployee(employee); if (errors.isEmpty()) { // 数据有效 validEmployees.add(employee); } else { // 记录错误 Map<String, Object> errorRecord = new HashMap<>(); errorRecord.put("rowIndex", rowIndex); errorRecord.put("data", employee); errorRecord.put("errors", errors); errorRecords.add(errorRecord); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { // 处理完成 } // 验证员工数据 private List<String> validateEmployee(Employee employee) { List<String> errors = new ArrayList<>(); // 验证姓名 if (employee.getName() == null || employee.getName().trim().isEmpty()) { errors.add("姓名不能为空"); } // 验证年龄 if (employee.getAge() == null) { errors.add("年龄不能为空"); } else if (employee.getAge() < 18 || employee.getAge() > 65) { errors.add("年龄必须在18-65岁之间"); } // 验证邮箱 if (employee.getEmail() != null && !employee.getEmail().isEmpty()) { String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@" + "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"; if (!employee.getEmail().matches(emailRegex)) { errors.add("邮箱格式不正确"); } } // 验证部门 if (employee.getDepartment() == null || employee.getDepartment().trim().isEmpty()) { errors.add("部门不能为空"); } // 验证薪资 if (employee.getSalary() != null && employee.getSalary() < 0) { errors.add("薪资不能为负数"); } return errors; } public List<Employee> getValidEmployees() { return validEmployees; } public List<Map<String, Object>> getErrorRecords() { return errorRecords; } public boolean hasErrors() { return !errorRecords.isEmpty(); } }
7.4 统一异常处理
为Excel处理添加统一的异常处理:
@ControllerAdvice public class ExcelExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(ExcelExceptionHandler.class); @ExceptionHandler(IOException.class) public ResponseEntity<Map<String, String>> handleIOException(IOException e) { logger.error("文件读写异常", e); Map<String, String> response = new HashMap<>(); response.put("error", "文件读写异常"); response.put("message", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<Map<String, String>> handleIllegalArgumentException(IllegalArgumentException e) { logger.error("参数异常", e); Map<String, String> response = new HashMap<>(); response.put("error", "参数异常"); response.put("message", e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleGenericException(Exception e) { logger.error("Excel处理异常", e); Map<String, String> response = new HashMap<>(); response.put("error", "Excel处理异常"); response.put("message", e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } }
8. 性能优化和注意事项
8.1 性能优化建议
使用适当的Excel库:
- 小文件可使用Apache POI
- 大文件请使用EasyExcel或POI的SXSSF模式
- 极大文件考虑使用CSV格式
避免一次性加载整个文件:
- 读取时使用流式解析
- 写入时使用分批写入
合理设置缓冲区大小:
- 在SXSSFWorkbook中设置合理的内存行数
- 在批处理中选择合适的批次大小
减少样式对象:
- 样式对象重用,而不是为每个单元格创建新样式
- 限制使用的颜色、字体和边框样式数量
使用异步处理:
- 将大文件处理放在后台线程中执行
- 提供进度反馈机制
8.2 注意事项
内存管理:
- 注意监控JVM内存使用情况
- 对于大文件处理,考虑增加JVM堆内存(-Xmx参数)
- 使用完毕后及时关闭资源和清理临时文件
安全考虑:
- 限制上传文件大小
- 验证文件类型和内容
- 防止恶意Excel文件(包含宏或公式)
编码问题:
- 处理国际字符时,确保使用正确的字符编码
- 文件名包含中文时,确保正确编码
并发控制:
- 大文件处理时注意服务器负载
- 限制并发处理任务数量
临时文件清理:
- 使用SXSSF时,必须调用dispose()方法清理临时文件
- 定期清理服务器上的临时文件
总结
Spring Boot提供了强大而灵活的Excel处理能力,通过结合Apache POI和EasyExcel等工具,可以轻松实现Excel文件的读取、创建和导出功能。在实际应用中,应根据具体需求和数据量选择合适的处理策略,既要保证功能完整,又要注重性能和资源使用。
无论是简单的数据导出,还是复杂的报表生成,或是大数据量的文件处理,都可以通过本文介绍的方法灵活实现。重点是要根据实际业务场景,选择合适的技术方案,并注意性能优化和异常处理。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论