【SpringBoot异步导入Excel实战】从设计到优化的完整解决方案
SpringBoot异步导入Excel实战:从设计到优化的完整解决方案
一、背景与需求
在企业级应用中,Excel导入是常见需求。当导入数据量较大时,同步处理可能导致接口阻塞,影响用户体验。本文结合SpringBoot、MyBatis-Plus和EasyExcel,实现异步导入Excel功能,支持任务状态跟踪、数据校验、错误文件生成等特性,解决以下核心问题:
- 异步处理:避免主线程阻塞,提升系统吞吐量
- 通用封装:业务代码只需关注数据处理,无需重复实现异步逻辑
- 错误处理:生成包含错误信息的Excel文件,方便用户修正数据
二、技术选型
技术栈 | 作用 |
---|---|
SpringBoot | 快速构建项目,提供异步任务支持 |
MyBatis-Plus | 简化数据库操作,提供CRUD基础功能 |
EasyExcel | 高效读写Excel文件,支持复杂格式处理 |
异步线程池 | 处理异步导入任务,避免阻塞主线程 |
文件存储服务 | 管理上传文件和错误文件的存储与下载 |
三、数据库设计
1. 导入任务表(import_task)
CREATE TABLE `import_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID',
`task_name` varchar(100) NOT NULL COMMENT '任务名称',
`original_file_name` varchar(200) NOT NULL COMMENT '原始文件名',
`total_rows` int DEFAULT NULL COMMENT '总行数',
`success_rows` int DEFAULT NULL COMMENT '成功行数',
`fail_rows` int DEFAULT NULL COMMENT '失败行数',
`status` tinyint NOT NULL COMMENT '任务状态(0:等待导入,1:导入中,2:成功,3:失败,4:部分成功)',
`error_file_path` varchar(500) DEFAULT NULL COMMENT '错误文件路径',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Excel导入任务表';
2. 学生信息表(示例业务表)
CREATE TABLE `student` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(50) NOT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`gender` tinyint DEFAULT NULL COMMENT '性别(0:女,1:男)',
`phone` varchar(20) DEFAULT NULL COMMENT '电话',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';
四、核心代码实现
1. 任务状态枚举(ImportTaskStatusEnum)
public enum ImportTaskStatusEnum {
WAITING(0, "等待导入"),
PROCESSING(1, "导入中"),
SUCCESS(2, "成功"),
FAILURE(3, "失败"),
PARTIAL_SUCCESS(4, "部分成功");
private final int code;
private final String description;
// 构造方法与获取方法省略
}
2. 通用异步导入服务(AsyncExcelImportService)
public interface AsyncExcelImportService {
<T> void asyncImportExcel(Long taskId, File file, ImportService<T> importService);
interface ImportService<T> {
Class<T> getDtoClass();
ImportResult processData(List<T> dataList);
String validateRow(T data); // 业务自定义校验方法
}
class ImportResult {
private int successCount;
private List<?> failedRecords;
private String errorFilePath;
// 构造方法与获取方法省略
}
}
3. 学生业务服务(StudentService)
@Service
public class StudentServiceImpl implements StudentService {
@Override
public Class<StudentImportDTO> getDtoClass() {
return StudentImportDTO.class;
}
@Override
public String validateRow(StudentImportDTO dto) {
List<String> errors = new ArrayList<>();
if (StringUtils.isEmpty(dto.getName())) {
errors.add("姓名不能为空");
}
if (dto.getAge() == null || dto.getAge() < 1 || dto.getAge() > 100) {
errors.add("年龄需在1-100之间");
}
return String.join("; ", errors);
}
@Override
public ImportResult processData(List<StudentImportDTO> dataList) {
List<StudentImportDTO> failed = new ArrayList<>();
int success = 0;
for (StudentImportDTO dto : dataList) {
String error = validateRow(dto);
if (StringUtils.isNotEmpty(error)) {
dto.setErrorMsg(error);
failed.add(dto);
continue;
}
// 保存数据库逻辑
success++;
}
return new ImportResult(success, failed, generateErrorFile(failed));
}
private String generateErrorFile(List<StudentImportDTO> failedRecords) {
// 使用EasyExcel生成错误文件并保存到文件存储服务
}
}
4. 异步处理核心逻辑(AsyncExcelImportServiceImpl)
@Service
public class AsyncExcelImportServiceImpl implements AsyncExcelImportService {
@Async("asyncExecutor")
@Override
public <T> void asyncImportExcel(Long taskId, File file, ImportService<T> importService) {
ImportTask task = importTaskService.getById(taskId);
task.setStatus(ImportTaskStatusEnum.PROCESSING.getCode());
importTaskService.updateById(task);
try {
List<T> dataList = EasyExcel.read(file).head(importService.getDtoClass()).sheet().doReadSync();
ImportResult result = importService.processData(dataList);
// 更新任务状态与错误文件路径
task.setTotalRows(dataList.size());
task.setSuccessRows(result.getSuccessCount());
task.setFailRows(result.getFailedRecords().size());
task.setErrorFilePath(result.getErrorFilePath());
// 根据成败状态更新任务状态
updateStatus(task, result);
} catch (Exception e) {
task.setStatus(ImportTaskStatusEnum.FAILURE.getCode());
importTaskService.updateById(task);
}
}
private void updateStatus(ImportTask task, ImportResult result) {
if (result.getFailedRecords().isEmpty()) {
task.setStatus(ImportTaskStatusEnum.SUCCESS.getCode());
} else if (result.getSuccessCount() == 0) {
task.setStatus(ImportTaskStatusEnum.FAILURE.getCode());
} else {
task.setStatus(ImportTaskStatusEnum.PARTIAL_SUCCESS.getCode());
}
task.setEndTime(new Date());
importTaskService.updateById(task);
}
}
五、关键优化点
1. 异步文件处理优化(解决临时文件被清理问题)
@PostMapping("/upload")
public String uploadExcel(@RequestParam("file") MultipartFile file,
@RequestParam("taskName") String taskName) {
try {
// 1. 保存文件到持久化目录
String fileName = UUID.randomUUID() + "_" + file.getOriginalFilename();
File persistFile = new File(fileStorageService.getUploadPath(), fileName);
Files.copy(file.getInputStream(), persistFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
// 2. 创建任务并启动异步处理
Long taskId = importTaskService.createImportTask(persistFile, taskName);
asyncExcelImportService.asyncImportExcel(taskId, persistFile, studentService);
return "任务创建成功,ID:" + taskId;
} catch (IOException e) {
return "上传失败:" + e.getMessage();
}
}
2. 错误文件下载优化(解决格式不匹配问题)
@Override
public void downloadErrorFile(Long taskId, HttpServletResponse response) throws IOException {
ImportTask task = getById(taskId);
if (task == null || task.getErrorFilePath() == null) {
throw new IllegalArgumentException("任务或错误文件不存在");
}
// 设置正确的MIME类型和文件名
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
String fileName = URLEncoder.encode(task.getOriginalFileName() + "_错误.xlsx", "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 使用缓冲流确保文件完整传输
try (InputStream is = fileStorageService.getFileInputStream(task.getErrorFilePath());
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.flush();
}
}
3. 线程池配置(application.yml)
async:
executor:
core-pool-size: 5 # 核心线程数
max-pool-size: 10 # 最大线程数
queue-capacity: 25 # 队列容量
thread-name-prefix: AsyncExcelImport- # 线程名前缀
六、测试验证
1. 测试文件结构(students.xlsx)
姓名 | 年龄 | 性别 | 电话 |
---|---|---|---|
张三 | 18 | 1 | 13812345678 |
李四 | 0 | 0 | 13998765432 |
25 | 1 | 15056789012 |
2. 接口测试流程
- 上传文件:调用
/api/import/task/upload
,获取任务ID - 查询状态:调用
/api/import/task/{taskId}
,等待状态变为部分成功
- 下载错误文件:调用
/api/import/task/downloadErrorFile/{taskId}
,验证错误信息是否正确
七、总结
1. 核心优势
- 异步解耦:通过线程池实现异步处理,避免接口阻塞
- 通用封装:业务代码只需实现
validateRow
和processData
,降低重复开发成本 - 完善的错误处理:生成包含错误原因的Excel文件,提升用户体验
2. 扩展建议
- 支持多Sheet导入:在EasyExcel读取时指定Sheet索引或名称
- 分布式任务调度:集成XXL-Job或Quartz,支持集群环境下的任务管理
- 权限控制:在任务查询和下载接口添加权限校验,保障数据安全
3. 代码仓库
src/main/java/com/example/demo/
├── config/AsyncConfig.java # 异步线程池配置
├── controller/ImportTaskController.java # 任务管理接口
├── entity/ # 实体类
│ ├── ImportTask.java
│ └── dto/StudentImportDTO.java
├── enums/ImportTaskStatusEnum.java # 任务状态枚举
├── mapper/ # MyBatis-Plus Mapper
│ ├── ImportTaskMapper.java
│ └── StudentMapper.java
├── service/ # 服务层
│ ├── AsyncExcelImportService.java # 核心异步导入服务
│ ├── ImportTaskService.java # 任务管理服务
│ └── impl/
│ ├── AsyncExcelImportServiceImpl.java
│ └── StudentServiceImpl.java # 学生业务实现
└── utils/FileStorageService.java # 文件存储服务
通过以上方案,我们实现了一个健壮的异步Excel导入系统,能够处理大规模数据导入并提供完善的错误反馈机制。开发者可根据实际业务扩展ImportService
接口,轻松实现不同场景的Excel导入需求。
【SpringBoot异步导入Excel实战】从设计到优化的完整解决方案的更多相关文章
- springboot批量导入excel数据
1 背景 小白今天闲着没事,在公司摸鱼,以为今天有事无聊的一天,突然上头说小子,今天实现一下批量导入Excel数据吧,当时我的内心是拒绝的,然后默默打开idea. 2 介绍 2.1 框架 java本身 ...
- POI异步导入Excel兼容xsl和xlsx
项目架构:spring+struts2+hibernate4+oracle 需求:用户导入excel文件,导入到相应的数据表中,要求提供导入模板,支持xls和xlsx文件 思路分析: 1.提供一个下载 ...
- Asp.Net异步导入Excel
故事:用户在页面上传一个excel文件,程序把excel里的内容入库. 技术方案:保存文件在服务器,jquey Ajax 异步读取文件中的记录到数据库,在页面实时刷新导入情况 页面前端 <%@ ...
- MVC异步 导入excel文件
View页面 js文件.封装到一个js文件里面 (function ($) { //可以忽略 var defaultSettings = { url: "http://upload.zhtx ...
- 集成 Redis & 异步任务 - SpringBoot 2.7 .2实战基础
SpringBoot 2.7 .2实战基础 - 09 - 集成 Redis & 异步任务 1 集成Redis <docker 安装 MySQL 和 Redis>一文已介绍如何在 D ...
- SpringBoot中关于Excel的导入和导出
前言 由于在最近的项目中使用Excel导入和导出较为频繁,以此篇博客作为记录,方便日后查阅.本文前台页面将使用layui,来演示对Excel文件导入和导出的效果.本文代码已上传至我的gitHub, ...
- SpringBoot+Mybatis-plus整合easyExcel批量导入Excel到数据库+导出Excel
一.前言 今天小编带大家一起整合一下easyExcel,之所以用这个,是因为easyExcel性能比较好,不会报OOM! 市面上常见的导入导出Excel分为三种: hutool easyExcel p ...
- [Asp.net]常见数据导入Excel,Excel数据导入数据库解决方案,总有一款适合你!
引言 项目中常用到将数据导入Excel,将Excel中的数据导入数据库的功能,曾经也查找过相关的内容,将曾经用过的方案总结一下. 方案一 NPOI NPOI 是 POI 项目的 .NET 版本.POI ...
- ASP.NET 导入EXCEL文档
鉴于教务一般都是手动输入学生信息,在未了解本校数据库的客观情况之下,我们准备设计一个导入excel文档中学生信息如数据库的功能.结合网上各类大牛的综合版本出炉.. 首先具体的实现思想如下: 1.先使用 ...
- 结合bootstrap fileinput插件和Bootstrap-table表格插件,实现文件上传、预览、提交的导入Excel数据操作流程
1.bootstrap-fileinpu的简单介绍 在前面的随笔,我介绍了Bootstrap-table表格插件的具体项目应用过程,本篇随笔介绍另外一个Bootstrap FieInput插件的使用, ...
随机推荐
- Mybatis 返回自增主键的id
Mybatis 返回自增主键的idkeyProperty=id:封装到对象中的id字段当中keyColumn=id:封装到数据库的id这一列order=AFTER:在新增语句之后执行 方法一 < ...
- 了解了这些你就是一位优秀的CTO
spring cloud 分布式 Ngix协议层做阻断应射处理 SpringBoot 容器+MVC框架 SpringSecurity 认证和授权框架 MyBatis ORM框架 Swagger-UI ...
- 傻妞教程——对接mongoDB使用返佣系统
使用docker安装mongo 1.安装 1.1 拉取mongo镜像 docker pull mongo:4.4 1.2 创建mongo数据持久化目录 mkdir -p /docker_volume/ ...
- 群晖NAS 6.2x Moments AI场景识别 补丁教程
首先,我们需要在套件中心里,停用Moments: 正常情况下,群晖是不允许root账号直接登录的,必须使用管理员账户,然后sudo -i指令登录root账户,这样非常的麻烦.所以我们可以之际开启roo ...
- copilot插件使用介绍
Copilot插件是一个由GitHub开发的人工智能代码助手,可以为开发人员提供代码自动补全和建议功能.Copilot使用了机器学习技术,通过分析大量的开源代码来自动生成代码片段和建议. 使用Copi ...
- Flink学习(三) 批流版本的wordcount JAVA版本
Flink 开发环境通常来讲,任何一门大数据框架在实际生产环境中都是以集群的形式运行,而我们调试代码大多数会在本地搭建一个模板工程,Flink 也不例外. Flink 一个以 Java 及 Scala ...
- linux服务问题传文件连不上问题远程问题等
通过iptables相关命令实现防火墙的打开和关闭 1.首先可以在打开的终端使用iptables --help查看帮助使用命令: 2.查看防火墙状态:service iptables status(此 ...
- pip 提示import error,cannot import name locations
出现这个问题的原因: 环境中没有安装年文件 安装了,环境路径错误 解决如下: 首先 执行升级命令 升级到最新 python -m pip install -U pip 再到site-packages目 ...
- Laravel11 从0开发 Swoole-Reverb 扩展包(一) - 扩展包开发
前言 大家好呀,我是yangyang.好久没更新了,最近新项目在使用laravel11(截止目前发文,laravel12也发布了)做开发,自己也是利用有些空闲时间做些除开业务以外的深入学习,因此也就萌 ...
- glib-2.60在win64,msys2下编译
前阵子,工作原因,需要在win7 64下的msys2来编译glib,下面是一些踩过的坑: 事先声明一下,这些个解决方式及纯粹是为了编译通过,可能有些做法不太适合一些需要正常使用的场合,烦请各位注意下. ...