Spring Boot + EasyExcel 极简数据导入导出实战指南
在后台管理系统开发中,Excel导入导出是刚需功能。传统POI方案代码臃肿且内存消耗大,而阿里开源的EasyExcel以简洁API和低内存占用著称。本文将手把手带你在Spring Boot中5分钟实现企业级Excel处理能力。
1. 环境准备与项目初始化
首先创建一个基础的Spring Boot项目,添加以下核心依赖:
<dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- EasyExcel核心库 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.1.1</version> </dependency> <!-- Lombok简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>推荐使用最新稳定版EasyExcel 3.x系列,相比2.x版本在性能和功能上都有显著提升。初始化后的项目结构应包含:
src/ ├── main/ │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ ├── controller/ │ │ ├── model/ │ │ └── Application.java │ └── resources/ │ └── application.yml2. 数据模型定义与注解解析
定义用户数据实体类,通过注解配置Excel映射关系:
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserExportVO { @ExcelProperty(value = "用户ID", index = 0) private Long id; @ExcelProperty(value = "用户名", index = 1) private String username; @ExcelProperty(value = "手机号", index = 2) private String mobile; @ExcelProperty(value = "注册时间", index = 3) @DateTimeFormat("yyyy-MM-dd HH:mm:ss") private Date createTime; @ExcelProperty(value = "账户状态", index = 4) private String status; // 自定义转换器示例 @ExcelProperty(value = "会员等级", index = 5, converter = LevelConverter.class) private Integer level; }关键注解说明:
| 注解 | 作用 | 常用属性 |
|---|---|---|
@ExcelProperty | 定义字段映射 | value: 列名, index: 列顺序 |
@DateTimeFormat | 日期格式化 | value: 格式字符串 |
@NumberFormat | 数字格式化 | value: 格式模式 |
@ColumnWidth | 列宽设置 | value: 字符宽度 |
自定义枚举转换器实现:
public class LevelConverter implements Converter<Integer> { @Override public Class<?> supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return switch (cellData.getStringValue()) { case "普通会员" -> 1; case "黄金会员" -> 2; case "铂金会员" -> 3; default -> 0; }; } @Override public CellData<String> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return new CellData<>(switch (value) { case 1 -> "普通会员"; case 2 -> "黄金会员"; case 3 -> "铂金会员"; default -> "非会员"; }); } }3. 导出功能实现
创建ExportController处理导出请求:
@RestController @RequestMapping("/api/excel") public class ExcelExportController { @GetMapping("/export/users") public void exportUserList(HttpServletResponse response) throws IOException { // 1. 准备数据(实际项目从数据库查询) List<UserExportVO> dataList = mockUserData(); // 2. 设置响应头 String fileName = URLEncoder.encode("用户列表", "UTF-8"); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx"); // 3. 执行导出 EasyExcel.write(response.getOutputStream(), UserExportVO.class) .sheet("用户数据") .doWrite(dataList); } private List<UserExportVO> mockUserData() { List<UserExportVO> list = new ArrayList<>(); // 模拟20条数据 for (int i = 1; i <= 20; i++) { list.add(UserExportVO.builder() .id(10000L + i) .username("user_" + i) .mobile("188" + String.format("%08d", i)) .createTime(new Date()) .status(i % 3 == 0 ? "禁用" : "正常") .level(i % 4) .build()); } return list; } }高级导出技巧:
- 动态列控制:通过
excludeColumnFieldNames或includeColumnFieldNames方法 - 多Sheet导出:创建多个WriteSheet对象
- 模板导出:使用
withTemplate()方法基于模板填充
// 动态列示例 Set<String> excludeColumns = new HashSet<>(); excludeColumns.add("mobile"); excludeColumns.add("level"); EasyExcel.write(response.getOutputStream(), UserExportVO.class) .excludeColumnFiledNames(excludeColumns) .sheet("精简用户数据") .doWrite(dataList);4. 导入功能实现
创建ImportController处理文件上传:
@PostMapping("/import/users") public ApiResult importUsers(@RequestParam("file") MultipartFile file) { try { List<UserImportVO> importData = EasyExcel.read(file.getInputStream()) .head(UserImportVO.class) .sheet() .doReadSync(); // 业务处理(如数据校验、入库等) processImportData(importData); return ApiResult.success("导入成功", importData.size()); } catch (Exception e) { return ApiResult.fail("导入失败: " + e.getMessage()); } } // 带监听器的读取方式(适合大数据量) @PostMapping("/import/users-batch") public void importUsersBatch(@RequestParam("file") MultipartFile file) { EasyExcel.read(file.getInputStream(), UserImportVO.class, new UserDataListener(userService)) .sheet() .doRead(); }创建数据监听器实现增量处理:
public class UserDataListener extends AnalysisEventListener<UserImportVO> { private static final int BATCH_SIZE = 100; private final UserService userService; private List<UserImportVO> cachedList = new ArrayList<>(BATCH_SIZE); public UserDataListener(UserService userService) { this.userService = userService; } @Override public void invoke(UserImportVO data, AnalysisContext context) { cachedList.add(data); if (cachedList.size() >= BATCH_SIZE) { saveData(); cachedList.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { if (!cachedList.isEmpty()) { saveData(); } } private void saveData() { userService.batchProcess(cachedList); } }导入验证技巧:
- 使用
@ExcelProperty的index属性确保列顺序 - 在监听器中实现业务校验逻辑
- 使用
@DateTimeFormat和@NumberFormat保证数据格式
5. 高级功能与性能优化
5.1 样式定制
通过注解自定义单元格样式:
@Data @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 22) // 表头背景色 @HeadFontStyle(fontHeightInPoints = 12, bold = true) // 表头字体 @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40) // 内容背景色 @ContentFontStyle(fontHeightInPoints = 11) // 内容字体 public class StyledExportVO { @ColumnWidth(20) @ExcelProperty("订单编号") private String orderNo; @ContentStyle(fillForegroundColor = 42) @ExcelProperty("异常标记") private String errorFlag; }5.2 大数据量处理
对于10万+数据量的导出,建议:
- 使用分页查询避免OOM
- 启用web响应压缩
- 添加进度提示
// 分页导出示例 public void largeDataExport(HttpServletResponse response) throws IOException { response.setHeader("Content-Encoding", "gzip"); try (OutputStream out = new GZIPOutputStream(response.getOutputStream()); ExcelWriter excelWriter = EasyExcel.write(out, LargeDataVO.class).build()) { WriteSheet writeSheet = EasyExcel.writerSheet("大数据").build(); int pageSize = 5000; int pageNo = 1; while (true) { Page<LargeDataVO> page = dataService.getByPage(pageNo, pageSize); if (page.getRecords().isEmpty()) break; excelWriter.write(page.getRecords(), writeSheet); pageNo++; } } }5.3 常见问题解决方案
日期格式问题:
- 确保实体类字段使用
java.util.Date - 注解格式与Excel实际格式一致
- 确保实体类字段使用
数字精度丢失:
- 对于大数字使用String类型接收
- 使用
@NumberFormat指定精度
内存溢出处理:
// 读取配置 ExcelReaderBuilder readerBuilder = EasyExcel.read() .autoCloseStream(true) .autoTrim(true) .ignoreEmptyRow(true);浏览器兼容性:
- 设置正确的Content-Type
- 文件名进行URL编码
6. 实战:完整用户管理系统案例
整合导入导出的完整Controller示例:
@RestController @RequestMapping("/user") public class UserController { @PostMapping("/import") public ApiResult importUsers(@RequestParam MultipartFile file) { try { List<UserImportDTO> list = EasyExcel.read(file.getInputStream()) .head(UserImportDTO.class) .registerReadListener(new UserImportListener()) .sheet() .doReadSync(); return ApiResult.success("导入成功", list.size()); } catch (Exception e) { return ApiResult.fail(e.getMessage()); } } @GetMapping("/export") public void exportUsers(UserQuery query, HttpServletResponse response) throws IOException { response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); String fileName = URLEncoder.encode("用户列表_" + System.currentTimeMillis(), "UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx"); List<UserExportVO> data = userService.exportUsers(query); EasyExcel.write(response.getOutputStream(), UserExportVO.class) .registerConverter(new LocalDateTimeConverter()) .sheet("用户数据") .doWrite(data); } }前端Vue调用示例:
// 导出 function exportExcel(params) { return axios({ url: '/user/export', method: 'get', params, responseType: 'blob' }).then(res => { const blob = new Blob([res.data], { type: res.headers['content-type'] }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = decodeURIComponent( res.headers['content-disposition'].split('filename=')[1] ) document.body.appendChild(link) link.click() document.body.removeChild(link) }) } // 导入 function importExcel(file) { const formData = new FormData() formData.append('file', file) return axios.post('/user/import', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) }在微服务架构中,建议将Excel处理封装为独立服务,通过Feign客户端调用。对于超大规模数据(100万+),可以考虑以下优化方案:
- 采用分片导出策略
- 使用消息队列异步处理
- 实现断点续传功能
- 提供OSS直传方案
实际项目中遇到的典型问题包括:合并单元格处理、动态表头生成、多级表头设计等。针对这些需求,EasyExcel提供了@ExcelMerge等注解支持,也可以通过自定义合并策略实现复杂布局。