java中Excel的导入和导出(EasyExcel)

前言:

Java 中处理 Excel 的导入(读取)和导出(写入),目前业界最主流、最好用的库是 EasyExcel(阿里巴巴开源)。

相比传统的 Apache POIEasyExcel 最大的优势是:节省内存。

  • POI :一次性把整个 Excel 加载到内存,文件大一点(如 10MB+)就容易 OutOfMemoryError
  • EasyExcel:采用"一行一行解析"的模式,无论文件多大,内存占用都极低。

第一步:引入依赖

在你的 pom.xml 中添加 EasyExcel 依赖:

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.4</version> <!-- 建议使用最新稳定版 -->
</dependency>

第二步:Excel 导入(读取)

导入的核心逻辑 是:定义一个 DTO 类映射 Excel 列 -> 监听器读取数据 -> 批量存入数据库。

1. 定义接收数据的 DTO

使用 @ExcelProperty 注解指定 Excel 中的列名或索引。

java 复制代码
package com.example.todo.dto;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class ImportTodoDTO {

    // index = 0 表示第一列,或者用 value = "任务标题" 匹配表头名称
    @ExcelProperty(value = "任务标题", index = 0) 
    private String title;

    @ExcelProperty(value = "是否完成", index = 1)
    private Boolean completed;

    @ExcelProperty(value = "截止时间", index = 2)
    private String deadlineStr; // 建议先用 String 接,再手动转换,避免格式报错
}

2. 创建监听器 (Listener)

这是 EasyExcel核心。它会在读取每一行时触发方法。我们需要在这里收集数据,并分批插入数据库,防止一次性插入太多导致性能问题。

java 复制代码
package com.example.todo.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.example.todo.dto.ImportTodoDTO;
import com.example.todo.entity.Todo;
import com.example.todo.mapper.TodoMapper;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class TodoImportListener extends AnalysisEventListener<ImportTodoDTO> {

    // 每批处理的数据量
    private static final int BATCH_COUNT = 100;
    
    // 缓存列表
    private List<ImportTodoDTO> cachedDataList = new ArrayList<>(BATCH_COUNT);
    
    private final TodoMapper todoMapper;

    public TodoImportListener(TodoMapper todoMapper) {
        this.todoMapper = todoMapper;
    }

    /**
     * 每解析一行数据,就会调用一次这个方法
     */
    @Override
    public void invoke(ImportTodoDTO data, AnalysisContext context) {
        log.info("解析到一条数据: {}", data);
        cachedDataList.add(data);
        
        // 达到阈值,执行批量入库
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            cachedDataList.clear(); // 清空缓存
        }
    }

    /**
     * 所有数据解析完成后,会调用这个方法
     * 用于处理最后剩余不足 BATCH_COUNT 的数据
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData(); // 确保最后一批数据也被保存
        log.info("所有数据解析完成!");
    }

    /**
     * 将缓存中的数据转换并插入数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        
        List<Todo> todoList = new ArrayList<>();
        for (ImportTodoDTO dto : cachedDataList) {
            Todo todo = new Todo();
            todo.setTitle(dto.getTitle());
            todo.setCompleted(dto.getCompleted());
            // 这里可以添加简单的日期解析逻辑
            todoList.add(todo);
        }
        
        // 调用 Mapper 批量插入
        todoMapper.insertBatch(todoList);
        
        log.info("存储数据库成功!");
    }
}

3. Service 层调用

java 复制代码
@Service
public class TodoServiceImpl {

    @Autowired
    private TodoMapper todoMapper;

    public void importTodos(MultipartFile file) throws IOException {
        // 获取 Listener 实例,传入 Mapper 以便入库
        TodoImportListener listener = new TodoImportListener(todoMapper);
        
        // 开始读取
        EasyExcel.read(file.getInputStream(), ImportTodoDTO.class, listener)
                 .sheet() // 默认读取第一个 sheet
                 .doRead();
    }
}

4. Controller 层

java 复制代码
@PostMapping("/import")
public Result importExcel(@RequestParam("file") MultipartFile file) {
    try {
        todoService.importTodos(file);
        return Result.ok("导入成功");
    } catch (Exception e) {
        return Result.fail("导入失败: " + e.getMessage());
    }
}

第三步:Excel 导出(写入)

导出非常简单,只需要定义好 DTO ,然后调用 write 方法即可。

1. 定义导出的 DTO (VO)

可以和导入共用一个 DTO,也可以单独建一个 ExportTodoVO。

java 复制代码
@Data
public class ExportTodoVO {
    @ExcelProperty(value = "ID", index = 0)
    private Long id;

    @ExcelProperty(value = "任务标题", index = 1)
    private String title;

    @ExcelProperty(value = "状态", index = 2)
    private String statusStr; // 导出时通常希望显示 "已完成/未完成",而不是 true/false

    @ExcelProperty(value = "创建时间", index = 3)
    private LocalDateTime createTime;
}

2. Service 层准备数据

java 复制代码
public List<ExportTodoVO> getExportData() {
    List<Todo> todos = todoMapper.selectList(null); // 查询所有
    
    return todos.stream().map(todo -> {
        ExportTodoVO vo = new ExportTodoVO();
        vo.setId(todo.getId());
        vo.setTitle(todo.getTitle());
        vo.setStatusStr(todo.getCompleted() ? "已完成" : "进行中");
        vo.setCreateTime(todo.getCreateTime());
        return vo;
    }).collect(Collectors.toList());
}

3. Controller 层直接写出流

注意:导出接口不需要返回 Result 对象,而是直接操作 HttpServletResponse 输出文件流。

java 复制代码
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
    // 1. 设置响应头
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setCharacterEncoding("utf-8");
    
    // 文件名 URLEncode 防止中文乱码
    String fileName = URLEncoder.encode("待办事项列表", "UTF-8").replaceAll("\\+", "%20");
    response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

    // 2. 获取数据
    List<ExportTodoVO> list = todoService.getExportData();

    // 3. 写出 Excel
    EasyExcel.write(response.getOutputStream(), ExportTodoVO.class)
             .sheet("待办列表") // Sheet 名称
             .doWrite(list);
}

💡 常见坑点与最佳实践

日期格式问题:

1.导入时:Excel 中的日期 可能是数字序列号 ,也可能是字符串 。建议在 DTO 中用 String 接收,然后在 Service 层用 DateTimeFormatter 手动解析,这样更灵活,不容易报错。

2.导出时:可以在 @ExcelProperty 中使用 converter 自定义转换器,或者直接在 VO 里准备好格式化好的字符串。

大数据量导出:

1.如果数据量超过 10 万条,不要一次性查出来再写。

2.使用 EasyExcel 的 分页查询 + 追加写入 模式:

java 复制代码
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExportTodoVO.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("Sheet1").build();

int pageNum = 1;
int pageSize = 1000;
while (true) {
    Page<Todo> page = todoMapper.selectPage(new Page<>(pageNum, pageSize), null);
    if (page.getRecords().isEmpty()) break;
    
    // 转换数据
    List<ExportTodoVO> voList = convert(page.getRecords());
    
    // 追加写入
    excelWriter.write(voList, writeSheet);
    
    if (page.getRecords().size() < pageSize) break;
    pageNum++;
}
excelWriter.finish();

模板下载:

通常导入功能会配合一个"下载模板"功能。你可以创建一个只有表头的空 Excel 文件,放在 resources/templates/todo_template.xlsx ,然后通过 ResourceUtils.getFile 读取并返回给前端。

事务控制:

导入操作涉及数据库写入,记得在 Service 方法上加 @Transactional,确保如果中间某一批数据出错,之前导入的数据能回滚(或者根据业务需求,记录错误行,继续导入其他行)。

总结

库选择 :无脑选 EasyExcel。

导入核心 :Listener 监听器 + 分批入库。

导出核心 :HttpServletResponse 输出流 + EasyExcel.write。

难点:日期格式转换、大数据量的内存优化(EasyExcel 已帮你解决大部分)。

相关推荐
xcl09258 小时前
民宿预约系统开发实战:从需求分析到上线部署全流程解析
java·spring boot
IT_Octopus8 小时前
JSON 日志里的 `{“$ref“:“$.xxx“}`:从 fastjson 兼容包到原生 fastjson2 的迁移实录
开发语言·python·json
geovindu8 小时前
python:HandWriting Recognition using paddlepaddle
开发语言·后端·python·paddlepaddle
步行cgn8 小时前
为何以继承方式引入SpringBoot
java·spring boot·后端
CoderYanger8 小时前
A.每日一题:1140. 石子游戏 II
java·程序人生·算法·leetcode·游戏·职场和发展·深度优先
禾高网络8 小时前
养老系统开发:背景、架构与养老行业展望
java·大数据·人工智能·小程序·架构
wuyk5558 小时前
Python 零基础入门第九章:用户输入与 While 循环
开发语言·python
兮动人8 小时前
Python变量与常量
开发语言·python·机器学习·python变量与常量
CCYe、8 小时前
新模型上线、旧模型下线:企业AI网关如何管住模型版本
java·网络·数据库·人工智能
风筝在晴天搁浅8 小时前
解题代码清爽版
java·开发语言