EasyExcel 导入导出完整解决:从基础使用到高级特性

内容分享3周前发布
0 0 0

概述

在企业级应用开发中,Excel 导入导出是常见需求。本文将详细介绍 EasyExcel 的使用方法,提供完善的代码示例,并对比其他主流解决方案。

一、EasyExcel 简介

EasyExcel 是阿里巴巴开源的一个基于 Java 的简单、省内存的读写 Excel 工具。相比传统 POI,它具有以下优势:

  • 内存优化:采用逐行解析模式,极大减少内存占用
  • API 简洁:注解驱动,开发效率高
  • 功能丰富:支持复杂表头、样式定制、数据校验等

二、环境准备

2.1 Maven 依赖

<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>3.3.3</version>
    </dependency>
    
    <!-- Spring Boot Web 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- 数据校验 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

三、核心工具类封装

3.1 Excel 导入工具类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.PageReadListener;
import com.alibaba.excel.read.listener.ReadListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * Excel 导入工具类
 */
@Slf4j
public class ExcelImportUtil {
    
    private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    
    /**
     * 导入 Excel 数据(分批处理,适合大数据量)
     * 
     * @param file 上传的文件
     * @param clazz 数据模型类
     * @param batchSize 每批处理的数据量
     * @param batchConsumer 批次消费函数
     * @param <T> 数据类型
     */
    public static <T> void importExcelInBatches(MultipartFile file, Class<T> clazz, 
                                               int batchSize, Consumer<List<T>> batchConsumer) {
        try {
            EasyExcel.read(file.getInputStream(), clazz, new PageReadListener<T>(dataList -> {
                // 数据校验
                List<T> validData = validateData(dataList);
                if (!validData.isEmpty()) {
                    batchConsumer.accept(validData);
                }
            }, batchSize)).sheet().doRead();
        } catch (IOException e) {
            throw new RuntimeException("读取 Excel 文件失败", e);
        }
    }
    
    /**
     * 导入 Excel 数据(返回全部数据,适合小数据量)
     */
    public static <T> List<T> importExcel(MultipartFile file, Class<T> clazz, String sheetName) {
        List<T> dataList = new ArrayList<>();
        
        try {
            EasyExcel.read(file.getInputStream(), clazz, new ReadListener<T>() {
                @Override
                public void invoke(T data, AnalysisContext context) {
                    // 单条数据校验
                    Set<ConstraintViolation<T>> violations = validator.validate(data);
                    if (violations.isEmpty()) {
                        dataList.add(data);
                    } else {
                        log.warn("数据校验失败: {}", violations);
                    }
                }
                
                @Override
                public void doAfterAllAnalysed(AnalysisContext context) {
                    log.info("Excel 解析完成,共读取 {} 条数据", dataList.size());
                }
            }).sheet(sheetName).doRead();
        } catch (IOException e) {
            throw new RuntimeException("读取 Excel 文件失败", e);
        }
        
        return dataList;
    }
    
    /**
     * 数据校验
     */
    private static <T> List<T> validateData(List<T> dataList) {
        List<T> validData = new ArrayList<>();
        for (T data : dataList) {
            Set<ConstraintViolation<T>> violations = validator.validate(data);
            if (violations.isEmpty()) {
                validData.add(data);
            } else {
                log.warn("数据校验失败,跳过该行数据: {}", violations);
            }
        }
        return validData;
    }
}

3.2 Excel 导出工具类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * Excel 导出工具类
 */
public class ExcelExportUtil {
    
    /**
     * 导出 Excel 文件(基础版本)
     */
    public static <T> void exportExcel(HttpServletResponse response, String fileName, 
                                    String sheetName, Class<T> clazz, List<T> data) {
        try {
            setupResponse(response, fileName);
            
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .doWrite(data);
        } catch (IOException e) {
            throw new RuntimeException("导出 Excel 文件失败", e);
        }
    }
    
    /**
     * 导出 Excel 文件(带样式版本)
     */
    public static <T> void exportExcelWithStyle(HttpServletResponse response, String fileName,
                                              String sheetName, Class<T> clazz, List<T> data) {
        try {
            setupResponse(response, fileName);
            
            // 设置表头样式
            WriteCellStyle headStyle = new WriteCellStyle();
            headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
            
            // 设置内容样式
            WriteCellStyle contentStyle = new WriteCellStyle();
            contentStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
            
            HorizontalCellStyleStrategy styleStrategy = 
                new HorizontalCellStyleStrategy(headStyle, contentStyle);
            
            EasyExcel.write(response.getOutputStream(), clazz)
                    .registerWriteHandler(styleStrategy)
                    .sheet(sheetName)
                    .doWrite(data);
        } catch (IOException e) {
            throw new RuntimeException("导出 Excel 文件失败", e);
        }
    }
    
    /**
     * 设置 HTTP 响应头
     */
    private static void setupResponse(HttpServletResponse response, String fileName) {
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
                .replaceAll("\+", "%20");
        
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, 
                ContentDisposition.attachment()
                        .filename(encodedFileName + ".xlsx")
                        .build()
                        .toString());
    }
    
    /**
     * 大数据量导出(分页查询方式)
     */
    public static <T> void exportLargeData(HttpServletResponse response, String fileName,
                                         String sheetName, Class<T> clazz, 
                                         DataSupplier<T> dataSupplier) {
        try {
            setupResponse(response, fileName);
            
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .doWrite(() -> dataSupplier.getNextBatch());
        } catch (IOException e) {
            throw new RuntimeException("导出 Excel 文件失败", e);
        }
    }
    
    /**
     * 数据供应接口(用于大数据量导出)
     */
    @FunctionalInterface
    public interface DataSupplier<T> {
        List<T> getNextBatch();
    }
}

四、数据模型定义

4.1 基础数据模型

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.Date;

/**
 * 用户信息导入导出模型
 */
@Data
@HeadRowHeight(20)    // 表头行高
@ContentRowHeight(15) // 内容行高
public class UserInfoModel {
    
    @ExcelProperty(value = "用户ID", index = 0)
    @ColumnWidth(15)
    private Long userId;
    
    @ExcelProperty(value = "用户名", index = 1)
    @ColumnWidth(20)
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @ExcelProperty(value = "邮箱", index = 2)
    @ColumnWidth(25)
    @NotBlank(message = "邮箱不能为空")
    private String email;
    
    @ExcelProperty(value = "手机号", index = 3)
    @ColumnWidth(15)
    private String phone;
    
    @ExcelProperty(value = "年龄", index = 4)
    @ColumnWidth(10)
    @NotNull(message = "年龄不能为空")
    private Integer age;
    
    @ExcelProperty(value = "工资", index = 5)
    @ColumnWidth(15)
    @NotNull(message = "工资不能为空")
    private BigDecimal salary;
    
    @ExcelProperty(value = "入职日期", index = 6)
    @ColumnWidth(15)
    @DateTimeFormat("yyyy-MM-dd")
    @NotNull(message = "入职日期不能为空")
    private Date hireDate;
    
    @ExcelProperty(value = "状态", index = 7)
    @ColumnWidth(10)
    private String status;
    
    /**
     * 自定义转换:状态显示转换
     */
    public String getStatus() {
        if ("1".equals(status)) {
            return "在职";
        } else if ("0".equals(status)) {
            return "离职";
        }
        return status;
    }
}

4.2 复杂表头模型

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ContentFontStyle;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadStyle;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import lombok.Data;
import org.apache.poi.ss.usermodel.IndexedColors;

import java.math.BigDecimal;

/**
 * 销售报表模型(复杂表头示例)
 */
@Data
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, 
          fillForegroundColor = 44) // 天蓝色背景
@HeadFontStyle(fontHeightInPoints = 11, bold = true)
public class SalesReportModel {
    
    // 第一级表头
    @ExcelProperty(value = {"销售报表", "产品信息", "产品ID"}, index = 0)
    private String productId;
    
    @ExcelProperty(value = {"销售报表", "产品信息", "产品名称"}, index = 1)
    private String productName;
    
    // 第二级表头合并
    @ExcelProperty(value = {"销售报表", "销售数据", "第一季度"}, index = 2)
    private BigDecimal q1Sales;
    
    @ExcelProperty(value = {"销售报表", "销售数据", "第二季度"}, index = 3)
    private BigDecimal q2Sales;
    
    @ExcelProperty(value = {"销售报表", "销售数据", "第三季度"}, index = 4)
    private BigDecimal q3Sales;
    
    @ExcelProperty(value = {"销售报表", "销售数据", "第四季度"}, index = 5)
    private BigDecimal q4Sales;
    
    @ExcelProperty(value = {"销售报表", "销售数据", "年度总计"}, index = 6)
    @ContentFontStyle(bold = true, color = 10) // 红色字体
    private BigDecimal totalSales;
    
    @ExcelProperty(value = {"销售报表", "评价", "客户评分"}, index = 7)
    private BigDecimal customerRating;
}

五、业务层实现

5.1 导入服务实现

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

/**
 * 用户信息导入服务
 */
@Slf4j
@Service
public class UserImportService {
    
    private final UserRepository userRepository;
    
    public UserImportService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    /**
     * 导入用户数据(小数据量)
     */
    public ImportResult importUsers(MultipartFile file) {
        try {
            List<UserInfoModel> userList = ExcelImportUtil.importExcel(file, 
                    UserInfoModel.class, "用户信息");
            
            int successCount = 0;
            int failureCount = 0;
            
            for (UserInfoModel userModel : userList) {
                try {
                    // 数据转换和保存
                    UserEntity userEntity = convertToEntity(userModel);
                    userRepository.save(userEntity);
                    successCount++;
                } catch (Exception e) {
                    log.error("保存用户数据失败: {}", userModel.getUsername(), e);
                    failureCount++;
                }
            }
            
            return new ImportResult(successCount, failureCount, 
                    "导入完成,成功:" + successCount + "条,失败:" + failureCount + "条");
                    
        } catch (Exception e) {
            throw new RuntimeException("导入用户数据失败", e);
        }
    }
    
    /**
     * 批量导入用户数据(大数据量)
     */
    public ImportResult importUsersInBatches(MultipartFile file, int batchSize) {
        ImportResult result = new ImportResult(0, 0, "");
        
        ExcelImportUtil.importExcelInBatches(file, UserInfoModel.class, batchSize, batch -> {
            try {
                List<UserEntity> entities = batch.stream()
                        .map(this::convertToEntity)
                        .toList();
                
                userRepository.saveAll(entities);
                result.setSuccessCount(result.getSuccessCount() + batch.size());
                
            } catch (Exception e) {
                log.error("批量保存用户数据失败", e);
                result.setFailureCount(result.getFailureCount() + batch.size());
            }
        });
        
        result.setMessage("批量导入完成,成功:" + result.getSuccessCount() + 
                "条,失败:" + result.getFailureCount() + "条");
        return result;
    }
    
    private UserEntity convertToEntity(UserInfoModel model) {
        UserEntity entity = new UserEntity();
        entity.setUsername(model.getUsername());
        entity.setEmail(model.getEmail());
        entity.setPhone(model.getPhone());
        entity.setAge(model.getAge());
        entity.setSalary(model.getSalary());
        entity.setHireDate(model.getHireDate());
        return entity;
    }
    
    /**
     * 导入结果类
     */
    @Data
    public static class ImportResult {
        private int successCount;
        private int failureCount;
        private String message;
        
        public ImportResult(int successCount, int failureCount, String message) {
            this.successCount = successCount;
            this.failureCount = failureCount;
            this.message = message;
        }
    }
}

5.2 导出服务实现

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

/**
 * 用户信息导出服务
 */
@Slf4j
@Service
public class UserExportService {
    
    private final UserRepository userRepository;
    
    public UserExportService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    /**
     * 导出所有用户数据
     */
    public void exportAllUsers(HttpServletResponse response) {
        try {
            List<UserInfoModel> userList = userRepository.findAll()
                    .stream()
                    .map(this::convertToModel)
                    .toList();
            
            ExcelExportUtil.exportExcelWithStyle(response, "用户信息导出", 
                    "用户信息", UserInfoModel.class, userList);
                    
        } catch (Exception e) {
            throw new RuntimeException("导出用户数据失败", e);
        }
    }
    
    /**
     * 大数据量导出(分页方式)
     */
    public void exportLargeUserData(HttpServletResponse response) {
        ExcelExportUtil.exportLargeData(response, "用户大数据导出", 
                "用户信息", UserInfoModel.class, new DataSupplier());
    }
    
    /**
     * 自定义数据供应器
     */
    private class DataSupplier implements ExcelExportUtil.DataSupplier<UserInfoModel> {
        private int page = 0;
        private final int size = 1000; // 每页1000条
        
        @Override
        public List<UserInfoModel> getNextBatch() {
            Page<UserEntity> pageData = userRepository.findAll(PageRequest.of(page, size));
            List<UserInfoModel> batch = pageData.getContent()
                    .stream()
                    .map(UserExportService.this::convertToModel)
                    .toList();
            
            page++;
            return batch;
        }
    }
    
    private UserInfoModel convertToModel(UserEntity entity) {
        UserInfoModel model = new UserInfoModel();
        model.setUserId(entity.getId());
        model.setUsername(entity.getUsername());
        model.setEmail(entity.getEmail());
        model.setPhone(entity.getPhone());
        model.setAge(entity.getAge());
        model.setSalary(entity.getSalary());
        model.setHireDate(entity.getHireDate());
        return model;
    }
}

六、控制器层

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;

/**
 * Excel 导入导出控制器
 */
@RestController
@RequestMapping("/api/excel")
@RequiredArgsConstructor
public class ExcelController {
    
    private final UserImportService userImportService;
    private final UserExportService userExportService;
    
    /**
     * 导入用户数据
     */
    @PostMapping("/import/users")
    public ApiResult importUsers(@RequestParam("file") MultipartFile file) {
        try {
            UserImportService.ImportResult result = userImportService.importUsers(file);
            return ApiResult.success(result);
        } catch (Exception e) {
            return ApiResult.error("导入失败: " + e.getMessage());
        }
    }
    
    /**
     * 批量导入用户数据
     */
    @PostMapping("/import/users-batch")
    public ApiResult importUsersBatch(@RequestParam("file") MultipartFile file,
                                    @RequestParam(value = "batchSize", defaultValue = "1000") int batchSize) {
        try {
            UserImportService.ImportResult result = userImportService.importUsersInBatches(file, batchSize);
            return ApiResult.success(result);
        } catch (Exception e) {
            return ApiResult.error("批量导入失败: " + e.getMessage());
        }
    }
    
    /**
     * 导出用户数据
     */
    @GetMapping("/export/users")
    public void exportUsers(HttpServletResponse response) {
        try {
            userExportService.exportAllUsers(response);
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    
    /**
     * 统一API响应格式
     */
    @Data
    public static class ApiResult {
        private boolean success;
        private String message;
        private Object data;
        
        public static ApiResult success(Object data) {
            ApiResult result = new ApiResult();
            result.success = true;
            result.message = "操作成功";
            result.data = data;
            return result;
        }
        
        public static ApiResult error(String message) {
            ApiResult result = new ApiResult();
            result.success = false;
            result.message = message;
            return result;
        }
    }
}

七、其他 Excel 处理方案对比

7.1 Apache POI

优点:

  • 功能最全面,支持所有 Excel 特性
  • 社区活跃,文档丰富
  • 支持老版本 Excel (.xls)

缺点:

  • 内存消耗大,处理大文件容易 OOM
  • API 相对复杂,学习曲线陡峭
  • 代码量较大

适用场景:

  • 需要处理复杂 Excel 格式
  • 需要支持 .xls 格式
  • 项目已经深度使用 POI

7.2 Hutool Excel

优点:

  • API 简单易用,学习成本低
  • 集成方便,依赖少
  • 功能实用,满足日常需求

缺点:

  • 功能相对简单,复杂场景支持不足
  • 性能不如 EasyExcel
  • 社区相对较小

适用场景:

  • 简单 Excel 操作需求
  • 快速开发项目
  • 小数据量处理

7.3 综合对比表格

特性

EasyExcel

Apache POI

Hutool Excel

内存占用

中等

性能

中等

中等

API 简洁性

功能全面性

中等

中等

学习曲线

社区支持

中等

中等

文档完善度

中等

中等

八、最佳实践提议

8.1 导入优化提议

  1. 数据校验:在导入前进行数据格式和业务规则校验
  2. 分批处理:大数据量采用分批处理,避免内存溢出
  3. 异常处理:完善的异常处理和错误信息反馈
  4. 进度监控:大文件导入提供进度监控功能

8.2 导出优化提议

  1. 分页查询:大数据量导出使用分页查询方式
  2. 样式优化:合理设置表格样式,提升用户体验
  3. 文件压缩:超大文件思考压缩处理
  4. 异步导出:长时间操作采用异步导出方式

8.3 安全思考

  1. 文件类型验证:严格验证上传文件类型
  2. 大小限制:设置合理的文件大小限制
  3. SQL 注入防护:避免通过 Excel 导入导致 SQL 注入
  4. 敏感信息过滤:导出时过滤敏感信息

总结

EasyExcel 是一个优秀的 Excel 处理工具,特别适合处理大数据量和性能敏感的场景。通过本文的完整示例和最佳实践,开发者可以快速掌握 EasyExcel 的使用方法,并根据实际需求选择合适的 Excel 处理方案。

在实际项目中,提议根据具体需求选择合适的技术方案,并结合本文提供的工具类和最佳实践,构建稳定高效的 Excel 导入导出功能。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...