目录
- 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)。
加载中,请稍侯......
精彩评论