EasyExcel 实现 Excel 导入导出

在企业级 Java Web 项目中,Excel 导入导出是高频核心需求 ------ 比如批量导入用户数据、导出业务报表、批量更新商品信息等。传统 POI 操作 Excel 易引发内存溢出(OOM)、API 繁琐且性能低下,而阿里开源的 EasyExcel 框架完美解决这些问题。本文基于 EasyExcel 3.3.2 版本,从基础配置到高级场景,详解 Excel 导入导出的全流程实现,适配单表导出、条件导出、批量导入、导入校验等所有常见场景。

一、技术选型:为什么选择 EasyExcel?

对比传统 POI/JXL,EasyExcel 的核心优势:

特性 EasyExcel 传统 POI
内存占用 逐行读写,低内存(MB 级) 加载整表到内存,易 OOM
API 易用性 注解式映射,代码量少 手动解析单元格,代码繁琐
性能 处理 10 万行数据仅需数秒 处理 10 万行数据耗时久
功能支持 样式定制、导入校验、合并单元格 需手动实现复杂功能
兼容性 支持 Excel 2003-2019 需区分 HSSF/XSSF

二、环境准备与基础配置

1. 引入 Maven 依赖

在项目pom.xml中添加 EasyExcel 核心依赖及辅助依赖:

XML 复制代码
<!-- EasyExcel核心依赖 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.3.2</version>
</dependency>
<!-- POI核心依赖(EasyExcel底层依赖) -->
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>
<!-- 日期格式化工具 -->
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>
<!-- JSON依赖(导入校验返回错误信息) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>

2. 通用工具类封装

编写ExcelUtil工具类,封装导出响应头设置、文件名编码等通用逻辑,减少重复代码:

java 复制代码
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * Excel通用工具类
 */
public class ExcelUtil {

    /**
     * 设置Excel导出响应头(解决文件名乱码、指定文件类型)
     */
    public static void setExcelResponseHeader(HttpServletResponse response, String fileName) {
        try {
            // 文件名编码,兼容浏览器
            String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
                    .replaceAll("\\+", "%20");
            // 设置响应头
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-Disposition", 
                               "attachment;filename*=UTF-8''" + encodeFileName + ".xlsx");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
        } catch (Exception e) {
            throw new RuntimeException("设置Excel响应头失败:" + e.getMessage());
        }
    }
}

三、Excel 导出实现(核心场景)

1. 基础场景:单列表数据导出

适用于简单数据导出(如用户列表、商品列表),核心是通过注解映射 Java 对象与 Excel 列。

(1)定义导出数据实体类

使用@ExcelProperty注解映射 Excel 表头与字段,@ColumnWidth设置列宽:

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;

import java.util.Date;

/**
 * 用户Excel导出实体
 */
@Data
public class UserExcelVO {

    @ExcelProperty("用户ID")
    @ColumnWidth(10)
    private Long id;

    @ExcelProperty("用户名")
    @ColumnWidth(20)
    private String username;

    @ExcelProperty("手机号")
    @ColumnWidth(15)
    private String phone;

    @ExcelProperty("创建时间")
    @ColumnWidth(25)
    private Date createTime;

    @ExcelProperty(value = "状态", converter = StatusConverter.class)
    @ColumnWidth(10)
    private Integer status; // 0-禁用 1-启用
}
(2)自定义转换器(可选)

将枚举 / 数字类型转换为中文展示(如状态 0→禁用,1→启用):

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 状态转换器
 */
public class StatusConverter implements Converter<Integer> {

    @Override
    public Class<Integer> supportJavaTypeKey() {
        return Integer.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public WriteCellData<?> convert(Integer value, ExcelContentProperty contentProperty, 
                                   GlobalConfiguration globalConfiguration) {
        String statusDesc = value == 1 ? "启用" : "禁用";
        return new WriteCellData<>(statusDesc);
    }
}
(3)编写导出接口
java 复制代码
import com.alibaba.excel.EasyExcel;
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.util.List;

/**
 * Excel导出控制器
 */
@RestController
@RequestMapping("/excel/export")
public class ExcelExportController {

    // 模拟业务服务
    private final UserService userService;

    public ExcelExportController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 导出所有用户列表
     */
    @GetMapping("/user/list")
    public void exportUserList(HttpServletResponse response) {
        // 1. 设置响应头
        ExcelUtil.setExcelResponseHeader(response, "用户列表_" + System.currentTimeMillis());
        // 2. 查询业务数据
        List<UserExcelVO> userList = userService.listAllUserForExcel();
        // 3. 写入Excel并响应到前端
        EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
                .sheet("用户列表") // Sheet名称
                .doWrite(userList);
    }
}

2. 进阶场景:条件导出 + 自定义样式

适用于按条件导出数据(如导出指定时间段的订单),并自定义表头样式、字体等。

(1)自定义样式配置
java 复制代码
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;

/**
 * Excel样式工具类
 */
public class ExcelStyleUtil {

    /**
     * 获取自定义表头样式
     */
    public static HorizontalCellStyleStrategy getCustomStyle() {
        // 表头样式
        WriteCellStyle headStyle = new WriteCellStyle();
        // 表头背景色
        headStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex());
        // 表头字体
        WriteFont headFont = new WriteFont();
        headFont.setFontName("微软雅黑");
        headFont.setFontHeightInPoints((short) 12);
        headFont.setBold(true);
        headStyle.setWriteFont(headFont);
        headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 居中

        // 内容样式
        WriteCellStyle contentStyle = new WriteCellStyle();
        WriteFont contentFont = new WriteFont();
        contentFont.setFontName("微软雅黑");
        contentFont.setFontHeightInPoints((short) 10);
        contentStyle.setWriteFont(contentFont);
        contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

        // 样式策略
        return new HorizontalCellStyleStrategy(headStyle, contentStyle);
    }
}
(2)条件导出接口
java 复制代码
/**
 * 按条件导出用户(如导出指定状态的用户)
 */
@GetMapping("/user/condition")
public void exportUserByCondition(HttpServletResponse response, 
                                  @RequestParam Integer status) {
    // 1. 设置响应头
    ExcelUtil.setExcelResponseHeader(response, "启用用户列表_" + System.currentTimeMillis());
    // 2. 按条件查询数据
    List<UserExcelVO> userList = userService.listUserByStatusForExcel(status);
    // 3. 自定义样式+写入Excel
    EasyExcel.write(response.getOutputStream(), UserExcelVO.class)
            .sheet("指定状态用户列表")
            .registerWriteHandler(ExcelStyleUtil.getCustomStyle()) // 注册样式
            .doWrite(userList);
}

3. 高级场景:多 Sheet 导出

适用于一次导出多个维度的数据(如同时导出用户、订单、商品数据):

java 复制代码
/**
 * 多Sheet导出
 */
@GetMapping("/multi/sheet")
public void exportMultiSheet(HttpServletResponse response) {
    ExcelUtil.setExcelResponseHeader(response, "多维度数据_" + System.currentTimeMillis());
    
    // 构建Excel写入对象
    ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
    
    // Sheet1:用户数据
    WriteSheet userSheet = EasyExcel.writerSheet(0, "用户数据")
            .head(UserExcelVO.class)
            .registerWriteHandler(ExcelStyleUtil.getCustomStyle())
            .build();
    excelWriter.write(userService.listAllUserForExcel(), userSheet);
    
    // Sheet2:订单数据(假设存在OrderExcelVO)
    WriteSheet orderSheet = EasyExcel.writerSheet(1, "订单数据")
            .head(OrderExcelVO.class)
            .registerWriteHandler(ExcelStyleUtil.getCustomStyle())
            .build();
    excelWriter.write(orderService.listAllOrderForExcel(), orderSheet);
    
    // 关闭writer
    excelWriter.finish();
}

四、Excel 导入实现(核心场景)

1. 基础场景:批量导入数据

适用于批量新增 / 更新数据(如批量导入用户、商品),核心是通过监听器逐行读取数据并处理。

(1)定义导入数据实体类
java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

/**
 * 用户Excel导入实体
 */
@Data
public class UserExcelImportVO {

    @ExcelProperty(value = "用户名", index = 0)
    private String username;

    @ExcelProperty(value = "手机号", index = 1)
    private String phone;

    @ExcelProperty(value = "密码", index = 2)
    private String password;

    @ExcelProperty(value = "状态", index = 3)
    private String status; // 启用/禁用
}
(2)编写导入监听器

继承AnalysisEventListener,逐行处理导入数据,支持数据校验、批量入库:

java 复制代码
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;

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

/**
 * 用户Excel导入监听器
 */
@Slf4j
public class UserExcelImportListener extends AnalysisEventListener<UserExcelImportVO> {

    // 批量入库阈值
    private static final int BATCH_SIZE = 100;
    // 临时存储导入数据
    private List<UserExcelImportVO> dataList = new ArrayList<>(BATCH_SIZE);
    // 业务服务
    private final UserService userService;
    // 导入错误信息
    private final List<String> errorMsgList = new ArrayList<>();

    // 构造方法注入业务服务
    public UserExcelImportListener(UserService userService) {
        this.userService = userService;
    }

    /**
     * 逐行处理数据
     */
    @Override
    public void invoke(UserExcelImportVO data, AnalysisContext context) {
        log.info("读取到Excel数据:{}", JSON.toJSONString(data));
        // 1. 数据校验
        String validateMsg = validateData(data, context.readRowHolder().getRowIndex() + 1);
        if (validateMsg != null) {
            errorMsgList.add(validateMsg);
            return;
        }
        // 2. 加入临时列表
        dataList.add(data);
        // 3. 批量入库
        if (dataList.size() >= BATCH_SIZE) {
            saveData();
            // 清空临时列表
            dataList.clear();
        }
    }

    /**
     * 数据校验
     * @param rowNum 行号(Excel行号,从1开始)
     */
    private String validateData(UserExcelImportVO data, int rowNum) {
        // 用户名非空校验
        if (data.getUsername() == null || data.getUsername().trim().isEmpty()) {
            return "第" + rowNum + "行:用户名不能为空";
        }
        // 手机号格式校验
        if (data.getPhone() == null || !data.getPhone().matches("^1[3-9]\\d{9}$")) {
            return "第" + rowNum + "行:手机号格式错误";
        }
        // 状态校验
        if (!"启用".equals(data.getStatus()) && !"禁用".equals(data.getStatus())) {
            return "第" + rowNum + "行:状态只能是"启用"或"禁用"";
        }
        // 手机号重复校验
        if (userService.existsByPhone(data.getPhone())) {
            return "第" + rowNum + "行:手机号" + data.getPhone() + "已存在";
        }
        return null;
    }

    /**
     * 批量保存数据
     */
    private void saveData() {
        log.info("批量入库{}条数据", dataList.size());
        userService.batchSave(dataList);
    }

    /**
     * 所有数据读取完成后执行
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 保存剩余数据
        if (!dataList.isEmpty()) {
            saveData();
        }
        log.info("Excel导入完成,共处理{}条数据,错误{}条", 
                 context.readRowHolder().getRowIndex(), errorMsgList.size());
    }

    // 获取错误信息
    public List<String> getErrorMsgList() {
        return errorMsgList;
    }
}
(3)编写导入接口
java 复制代码
import com.alibaba.excel.EasyExcel;
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.HashMap;
import java.util.Map;

/**
 * Excel导入控制器
 */
@RestController
@RequestMapping("/excel/import")
public class ExcelImportController {

    private final UserService userService;

    public ExcelImportController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 批量导入用户
     */
    @PostMapping("/user/batch")
    public Map<String, Object> importUser(@RequestParam("file") MultipartFile file) {
        Map<String, Object> result = new HashMap<>();
        try {
            // 1. 校验文件
            if (file.isEmpty()) {
                result.put("code", 500);
                result.put("msg", "上传文件不能为空");
                return result;
            }
            // 2. 创建导入监听器
            UserExcelImportListener listener = new UserExcelImportListener(userService);
            // 3. 读取Excel文件
            EasyExcel.read(file.getInputStream(), UserExcelImportVO.class, listener)
                    .sheet() // 读取第一个Sheet
                    .doRead();
            // 4. 处理导入结果
            if (!listener.getErrorMsgList().isEmpty()) {
                result.put("code", 500);
                result.put("msg", "部分数据导入失败");
                result.put("errorList", listener.getErrorMsgList());
            } else {
                result.put("code", 200);
                result.put("msg", "数据导入成功");
            }
        } catch (IOException e) {
            log.error("Excel导入失败", e);
            result.put("code", 500);
            result.put("msg", "导入失败:" + e.getMessage());
        }
        return result;
    }
}

2. 进阶场景:导入数据回写错误信息

适用于导入失败时,生成包含错误提示的 Excel 文件返回给用户,便于修正后重新导入:

java 复制代码
/**
 * 导入失败回写错误信息
 */
@PostMapping("/user/batch/withError")
public void importUserWithError(HttpServletResponse response, 
                                @RequestParam("file") MultipartFile file) {
    try {
        UserExcelImportListener listener = new UserExcelImportListener(userService);
        // 读取Excel
        EasyExcel.read(file.getInputStream(), UserExcelImportVO.class, listener)
                .sheet().doRead();
        
        // 有错误则生成错误Excel
        if (!listener.getErrorMsgList().isEmpty()) {
            // 设置响应头
            ExcelUtil.setExcelResponseHeader(response, "导入错误数据_" + System.currentTimeMillis());
            // 构建错误数据实体(假设存在UserErrorExcelVO)
            List<UserErrorExcelVO> errorList = buildErrorData(listener.getErrorMsgList());
            // 写入错误Excel
            EasyExcel.write(response.getOutputStream(), UserErrorExcelVO.class)
                    .sheet("导入错误信息")
                    .doWrite(errorList);
        } else {
            response.getWriter().write("{\"code\":200,\"msg\":\"导入成功\"}");
        }
    } catch (Exception e) {
        throw new RuntimeException("导入失败:" + e.getMessage());
    }
}

// 构建错误数据(示例)
private List<UserErrorExcelVO> buildErrorData(List<String> errorMsgList) {
    List<UserErrorExcelVO> list = new ArrayList<>();
    for (String msg : errorMsgList) {
        UserErrorExcelVO vo = new UserErrorExcelVO();
        vo.setErrorMsg(msg);
        list.add(vo);
    }
    return list;
}

五、核心注意事项与性能优化

1. 关键注意事项

  1. 内存控制:导入超大文件(10 万 + 行)时,务必设置批量入库阈值(如 100 条 / 批),避免一次性加载过多数据;
  2. 数据校验:导入前必须校验数据格式、唯一性、非空等,避免脏数据入库;
  3. 异常处理:导出时捕获 IO 异常,导入时捕获解析异常,保证接口稳定性;
  4. 文件名编码:导出文件名需 URLEncoder 编码,兼容不同浏览器(如 Chrome、Firefox);
  5. 资源释放 :多 Sheet 导出时,必须调用excelWriter.finish()关闭流,避免资源泄漏。

2. 性能优化

  1. 批量读写:导入时设置合理的批量入库阈值(50-200 条),减少数据库交互次数;
  2. 禁用自动头 :如果 Excel 模板固定,可禁用自动生成表头(head(false)),提升导出速度;
  3. 异步导出:超大文件导出(100 万 + 行)时,采用异步方式(如 MQ + 定时任务),避免接口超时;
  4. 数据库优化 :批量入库时使用 MyBatis 批量插入(foreach),开启数据库批处理模式。

六、总结

EasyExcel 凭借低内存、高性能、易扩展的特性,成为 Java 项目处理 Excel 的首选框架。本文覆盖了 Excel 导入导出的核心场景:

  1. 基础导出:注解式映射实体与 Excel 列,实现简单列表导出;
  2. 高级导出:自定义样式、多 Sheet 导出、条件导出,适配复杂业务场景;
  3. 基础导入:监听器逐行处理数据,支持数据校验、批量入库;
  4. 进阶导入:错误信息回写,提升用户体验。

该方案可直接复用至各类 Java Web 项目(如电商、OA、后台管理系统),解决 Excel 处理的核心痛点,兼顾性能与易用性。

相关推荐
AI_56781 天前
基于职业发展的Python与Java深度对比分析
java·人工智能·python·信息可视化
程序猿20231 天前
MySQL的逻辑存储结构
java·数据库·mysql
寻星探路1 天前
【Python 全栈测开之路】Python 进阶:库的使用与第三方生态(标准库+Pip+实战)
java·开发语言·c++·python·ai·c#·pip
海边的Kurisu1 天前
苍穹外卖日记 | Day1 苍穹外卖概述、开发环境搭建、接口文档
java
C雨后彩虹1 天前
任务最优调度
java·数据结构·算法·华为·面试
heartbeat..1 天前
Spring AOP 全面详解(通俗易懂 + 核心知识点 + 完整案例)
java·数据库·spring·aop
Jing_jing_X1 天前
AI分析不同阶层思维 二:Spring 的事务在什么情况下会失效?
java·spring·架构·提升·薪资
元Y亨H1 天前
Nacos - 服务发现
java·微服务
微露清风1 天前
系统性学习C++-第十八讲-封装红黑树实现myset与mymap
java·c++·学习