Spring Boot中Excel处理完全指南

文章目录

    • 1. Excel处理基础知识
      • 1.1 为什么需要在应用中处理Excel文件?
      • 1.2 Java中的Excel处理库介绍
        • 1.2.1 Apache POI
        • 1.2.2 EasyExcel
        • 1.2.3 JExcel
        • 1.2.4 Apache POI SXSSF
      • 1.3 Spring Boot中集成Excel处理
    • 2. 在Spring Boot中集成Excel处理库
      • 2.1 集成Apache POI
        • 2.1.1 添加依赖
        • 2.1.2 创建基本配置类
      • 2.2 集成EasyExcel
        • 2.2.1 添加依赖
        • 2.2.2 创建配置类
    • 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.1.1 创建数据模型
        • 5.1.2 创建读取监听器
        • 5.1.3 创建Excel读取服务
        • 5.1.4 创建Controller
      • 5.2 使用EasyExcel导出Excel
        • 5.2.1 简单导出示例
        • 5.2.2 创建Controller
    • 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 注意事项
    • 9. 总结

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处理库。本指南将主要介绍:

  1. 如何在Spring Boot项目中集成Apache POI和EasyExcel
  2. 如何实现Excel导入导出的常见功能
  3. 如何处理常见问题和优化性能

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 {@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver resolver = new CommonsMultipartResolver();resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MBreturn 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 {@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver resolver = new CommonsMultipartResolver();resolver.setMaxUploadSize(10485760); // 设置上传文件最大为10MBreturn 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 {@Autowiredprivate 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">
</head>
<body><div class="container mt-5"><div class="card"><div class="card-header"><h3>上传Excel文件</h3></div><div class="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);}// 写入ByteArrayOutputStreamByteArrayOutputStream 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 {@Autowiredprivate 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">
</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" 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_FOREGROUND);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);// 写入ByteArrayOutputStreamByteArrayOutputStream 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);}// 写入ByteArrayOutputStreamByteArrayOutputStream 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方法*/@Overridepublic void invoke(Employee employee, AnalysisContext context) {log.info("读取到一条数据: {}", employee);employeeList.add(employee);// 达到BATCH_COUNT时,需要存储一次数据库,防止数据几万条数据在内存,容易OOMif (employeeList.size() >= 5000) {saveData();// 清理内存employeeList.clear();}}/*** 所有数据解析完成后调用此方法*/@Overridepublic 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 {@Autowiredprivate 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) {// 创建ExcelWritertry (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);// 创建新的Sheetvar 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 {@Autowiredprivate 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();// 导出ExcelexportService.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("研发部员工", "市场部员工", "行政部员工");// 导出ExcelexportService.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() {@Overrideprotected void setHeadColumnHeight(Row row, int relativeRowIndex) {// 设置表头行高row.setHeight((short) 500);}@Overrideprotected void setContentColumnHeight(Row row, int relativeRowIndex) {// 设置内容行高row.setHeight((short) 400);}})// 设置单元格样式.registerWriteHandler(new CellWriteHandler() {@Overridepublic 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);}}@Overridepublic void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {// 在这里可以根据数据内容设置样式}@Overridepublic 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<>();// 第一列 IDList<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);}// 写入ExcelEasyExcel.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();}
}

注意事项:

  1. 使用完毕后一定要调用dispose()方法清理临时文件
  2. SXSSF仅支持写入操作,不支持读取
  3. 不支持某些高级特性(如合并单元格等)

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));// 写入ExcelexcelWriter.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() {// 生成唯一任务IDString taskId = UUID.randomUUID().toString();// 启动异步任务CompletableFuture.runAsync(() -> {try {// 导出文件路径String filePath = "/temp/" + taskId + ".xlsx";// 分页查询数据并写入Excelint 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);// 写入ExcelexcelWriter.write(pageData, writeSheet);// 更新进度updateExportProgress(taskId, (pageNum + 1) * 100 / totalPages);}}// 更新导出状态为完成updateExportStatus(taskId, "COMPLETED", filePath);} catch (Exception e) {// 更新导出状态为失败updateExportStatus(taskId, "FAILED", null);}});// 返回任务IDreturn 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, 0, 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是表头@Overridepublic 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);}}@Overridepublic 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 性能优化建议

  1. 使用适当的Excel库

    • 小文件可使用Apache POI
    • 大文件请使用EasyExcel或POI的SXSSF模式
    • 极大文件考虑使用CSV格式
  2. 避免一次性加载整个文件

    • 读取时使用流式解析
    • 写入时使用分批写入
  3. 合理设置缓冲区大小

    • 在SXSSFWorkbook中设置合理的内存行数
    • 在批处理中选择合适的批次大小
  4. 减少样式对象

    • 样式对象重用,而不是为每个单元格创建新样式
    • 限制使用的颜色、字体和边框样式数量
  5. 使用异步处理

    • 将大文件处理放在后台线程中执行
    • 提供进度反馈机制

8.2 注意事项

  1. 内存管理

    • 注意监控JVM内存使用情况
    • 对于大文件处理,考虑增加JVM堆内存(-Xmx参数)
    • 使用完毕后及时关闭资源和清理临时文件
  2. 安全考虑

    • 限制上传文件大小
    • 验证文件类型和内容
    • 防止恶意Excel文件(包含宏或公式)
  3. 编码问题

    • 处理国际字符时,确保使用正确的字符编码
    • 文件名包含中文时,确保正确编码
  4. 并发控制

    • 大文件处理时注意服务器负载
    • 限制并发处理任务数量
  5. 临时文件清理

    • 使用SXSSF时,必须调用dispose()方法清理临时文件
    • 定期清理服务器上的临时文件

9. 总结

Spring Boot提供了强大而灵活的Excel处理能力,通过结合Apache POI和EasyExcel等工具,可以轻松实现Excel文件的读取、创建和导出功能。在实际应用中,应根据具体需求和数据量选择合适的处理策略,既要保证功能完整,又要注重性能和资源使用。

无论是简单的数据导出,还是复杂的报表生成,或是大数据量的文件处理,都可以通过本文介绍的方法灵活实现。重点是要根据实际业务场景,选择合适的技术方案,并注意性能优化和异常处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/77721.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch 8.18 中提供了原生连接 (Native Joins)

作者&#xff1a;来自 Elastic Costin Leau 探索 LOOKUP JOIN&#xff0c;这是一条在 Elasticsearch 8.18 的技术预览中提供的新 ES|QL 命令。 很高兴宣布 LOOKUP JOIN —— 这是一条在 Elasticsearch 8.18 的技术预览中提供的新 ES|QL 命令&#xff0c;旨在执行左 joins 以进行…

2025年渗透测试面试题总结-拷打题库03(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 2025年渗透测试面试题总结-拷打题库03 一、Windows与Linux系统提权思路 Windows提权 Linux提权 二、…

【华为】OSPF震荡引起CPU占用率高怎么解决?

原创&#xff1a;厦门微思网络 现象描述 如图所示&#xff0c;Switch_1、Switch_2、Switch_3和Switch_4配置了OSPF协议&#xff0c;发现Switch_1设备的CPU占用率高&#xff0c;ROUT任务占用率明显高于其他任务并且产生路由震荡。 故障组网图 原因分析 网络中IP地址冲突导致…

Everything 安装教程与使用教程(附安装包)

文章目录 前言一、Everything 介绍二、Everything 安装教程1.Everything 安装包下载2.选择安装文件3.选择安装语言4.接受许可协议5.选择安装位置6.配置安装选项7.完成安装 三、Everything 使用教程1.启动软件2.简单关键词搜索3.按类型搜索 前言 在日常使用电脑时&#xff0c;随…

极狐GitLab CI/CD 流水线计算分钟数如何管理?

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;关于中文参考文档和资料有&#xff1a; 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 计算分钟管理 (PREMIUM SELF) 在极狐GitLab 16.1 中&#xff0c;从 CI/CD 分钟数重命名为计算配额或计算分钟数。 管理员可…

Containerd 1.7.2 离线安装与配置全指南(生产级优化)

Containerd 1.7.2 离线安装与配置全指南&#xff08;生产级优化&#xff09; 摘要&#xff1a;本文详细讲解在无外网环境下部署 Containerd 1.7.2 容器运行时的完整流程&#xff0c;涵盖二进制包安装、私有镜像仓库配置、Systemd服务集成等关键步骤&#xff0c;并提供生产环境…

33-公交车司机管理系统

技术&#xff1a; 基于 B/S 架构 SpringBootMySQLvueelementui 环境&#xff1a; Idea mysql maven jdk1.8 node 用户端功能 1.首页:展示车辆信息及车辆位置和线路信息 2.模块:车辆信息及车辆位置和线路信息 3.公告、论坛 4.在线留言 5.个人中心:修改个人信息 司机端功能…

基于 OpenCV 的图像与视频处理

基于 OpenCV 的图像处理 一、实验背景 OpenCV 是一个开源的计算机视觉库&#xff0c;广泛应用于图像处理、视频分析、目标检测等领域。通过学习 OpenCV&#xff0c;可以快速实现图像和视频的处理功能&#xff0c;为复杂的应用开发 奠定基础。本实验旨在通过实际代码示例&…

Linux 常用指令用户手册

Linux 常用指令用户手册 适合新手入门 & 日常速查 目录 基础操作文件与目录管理权限与所有权文本处理压缩与解压系统监控网络操作进程管理实用小技巧 1. 基础操作 1.1 查看系统信息 # 查看内核版本 uname -a# 查看系统发行版信息&#xff08;适用于 Debian/Ubuntu&…

长效IP与短效IP:如何选择适合业务的代理类型

在当今数据驱动的互联网环境中&#xff0c;代理IP已成为企业运营、数据采集和网络安全的关键工具。其中长效IP与短效IP作为两种主流代理类型&#xff0c;因特性差异被应用于不同场景。本文将深入解析二者的区别&#xff0c;并提供实际场景中的选择建议。 一、长效IP与短效IP&a…

数据结构|排序算法(三)选择排序 堆排序 归并排序

一、选择排序 1.算法思想 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法&#xff0c;其基本思想是&#xff1a;每次都从待排序部分中选出最小的一个数据和待排序的第一个数据交换。 将待排序序列分为已排序和未排序两部分&#xff0c;初始时已排…

Vue3 + TypeScript,关于item[key]的报错处理方法

处理方法1&#xff1a;// ts-ignore 注释忽略报错 处理方法2&#xff1a;item 设置为 any 类型

8.观察者模式:思考与解读

原文地址:观察者模式&#xff1a;思考与解读 更多内容请关注&#xff1a;7.深入思考与解读设计模式 引言 在开发软件时&#xff0c;系统的某些状态可能会发生变化&#xff0c;而你希望这些变化能够自动通知到依赖它们的其他模块。你是否曾经遇到过&#xff0c;系统中某个对象…

【HD-RK3576-PI】Ubuntu桌面多显、旋转以及更新Logo

硬件&#xff1a;HD-RK3576-PI 软件&#xff1a;Linux6.1Ubuntu22.04 在基于HD-RK3576-PI硬件平台运行Ubuntu 22系统的开发过程中&#xff0c;屏幕方向调整是提升人机交互体验的关键环节。然而&#xff0c;由于涉及uboot引导阶段、内核启动界面、桌面环境显示全流程适配&#x…

Rsync+sersync2实现目录实时同步

Sersync rsync 实现实时同步服务 sersync2二进制包目录规划 /app/tools/sersync/ /app/tools/sersync/bin /app/tools/sersync/conf项目架构是这样的&#xff1a; ------------------- ------------------- ------------------- | | …

MySQL视图高级应用与最佳实践

1. 视图与索引的协同优化​​ ​​物化视图&#xff08;模拟实现&#xff09;​​ MySQL原生不支持物化视图&#xff0c;但可通过“定时刷新”的物理表模拟&#xff1a; -- 1. 创建存储结果的物理表 CREATE TABLE cached_monthly_sales (product_id INT,total_sales DECIMAL(10…

string的模拟实现 (6)

目录 1.string.h 2.string.cpp 3.test.cpp 4.一些注意点 本篇博客就学习下如何模拟实现简易版的string类&#xff0c;学好string类后面学习其他容器也会更轻松些。 代码实现如下&#xff1a; 1.string.h #define _CRT_SECURE_NO_WARNINGS 1 #pragma once #include <…

Unity:像素(Pixels) 和 单位(Units)

目录 从第一性原理出发&#xff1a;什么是像素和 Unit&#xff1f; &#x1f9f1; 1. 像素&#xff08;Pixel&#xff09;&#xff1a;图像的最小单位 &#x1f4d0; 2. Unity Unit&#xff08;单位&#xff09;&#xff1a;游戏世界中的度量单位 核心换算公式&#xff1a;…

【失败总结】Win10系统安装docker

1.启用或关闭windows功能中&#xff0c;将Hyper-V功能勾选全部启用&#xff0c;容器勾选。设置好后要重启电脑。 2.管网下载下载安装Docker  Docker官网&#xff1a;https://www.docker.com/ 3.可以自定义Docker安装路径 新建安装目录&#xff1a;d:\MySoftware\Docker并将D…

《Adaptive Layer-skipping in Pre-trained LLMs》- 论文笔记

作者&#xff1a;Xuan Luo, Weizhi Wang, Xifeng Yan Department of Computer Science, UC Santa Barbara xuan_luoucsb.edu, weizhiwangucsb.edu, xyancs.ucsb.edu 1. 引言与动机 1.1 背景 LLM 的成功与挑战: 大型语言模型 (LLMs) 在翻译、代码生成、推理等任务上取得巨大成…