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 已帮你解决大部分)。

相关推荐
吠品2 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘2 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
mabing9933 小时前
Qt 生成条纹图
开发语言·qt
Java内核笔记3 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
蓝悦无人机3 小时前
C++基础 — 函数总结
开发语言·c++
糖果店的幽灵3 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
Fu_Lin_3 小时前
Qt 嵌入式从零基础到精通总纲
开发语言·qt·嵌入式·qt嵌入式
星空露珠4 小时前
28种颜色对应名称,
开发语言·数据库·算法·游戏·lua
码云数智-大飞4 小时前
PHP 接口开发规范:统一返回格式、异常处理与参数校验
开发语言·php