使用阿里 Easy Excel 实现 Excel 导入导出

2,754 阅读3分钟

简介

EasyExcel 是一个基于 Java 的简单、省内存的读写 Excel 的开源项目。在尽可能节约内存的情况下支持读写百M的 Excel。 github地址:github.com/alibaba/eas…

建议先看下官方文档,对 Easy Excel 有个大概的了解。本文旨在通过一个实践案例为具体应用这个库做 Excel 读写抛砖引玉。

前言

  • 本实践案例实现了动态头部,多 sheet 页导入导出功能,部分数据持久化,准备导出数据代码已省略。
  • 部分代码和业务需求耦合,请自行对照替换。
  • 其余未覆盖功能,如有需要请参考官网文档。
  • 代码可能存在部分漏洞,尽情谅解,欢迎指正其中的问题。

Controller 层接口

@PostMapping("/importRules")
@ApiOperation(value = "导入", notes = "@author:zy")
public ResponseMessage<ExcelImportVO> importRules(
    @ApiParam(value = "导入文件", required = true) 
    @RequestPart MultipartFile file) 
{
    return ResponseMessage.success(this.ruleService.importRules(file));
}

@PostMapping("/exportRules")
@ApiOperation(value = "导出", notes = "@author:zy")
public void exportRules(HttpServletResponse response,
    @ApiParam(value = "导出质量规则的 ID", required = true) 
    @RequestBody List<String> ruleIds) 
{
    this.ruleService.exportRules(response, ruleIds);
}

Service 层逻辑

/**
 * 导入
 *
 * @param file 导入的 Excel 文件
 * @return ExcelImportVO
 */
@Override
@Transactional(rollbackFor = Exception.class)
public ExcelImportVO importRules(MultipartFile file) {
    final RuleImportListener listener = new RuleImportListener(this);
    // 获取文件全名
    final String originalName = file.getOriginalFilename();
    // 文件名后缀
    final String extension = FilenameUtils.getExtension(originalName);
    // 格式校验
    if (Constants.FILE_EXTENSION_XLSX.equals(extension) || Constants.FILE_EXTENSION_XLS.equals(extension)) {
        try (final InputStream inputStream = file.getInputStream()) {
            // 读 Excel 文件
            ExcelUtil.readExcel(inputStream, listener);
        } catch (IOException e) {
            final String errorMsg = "解析 Excel 文档出错";
            log.error(errorMsg, e);
            throw new BusinessException(errorMsg);
        }
    } else {
        throw new BusinessException("导入的文件格式错误");
    }
    // 统计结果,根据业务场景替换
    final ExcelImportVO vo = new ExcelImportVO();
    return vo;
}

/**
 * 导出 Excel
 *
 * @param response HttpServletResponse
 * @param ruleIds  待导出质量规则的 ID
 */
@Override
@Transactional(rollbackFor = Exception.class)
public void exportRules(HttpServletResponse response, List<String> ruleIds) {
    // 准备要导出的数据,根据具体业务场景替换
    final List<ExcelExportDTO> exportData = getExportData(ruleIds);
    final Date date = new Date();
    String fileName = "质量规则-" + DateUtil.format(date, "yyyyMMdd");

    log.info("------> 导出质量规则开始");
    ExcelUtil.exportExcel(response, exportData, fileName);
    log.info("------> 导出质量规则完成");
}

Service 用到的类

RuleImportListener,读 Excel 时调用该类中的方法

package com.carnation.service.rule;


import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Excel 的读取类
 *
 * @author zy
 * @date 2021/4/25
 * @since 1.0.0
 */
public class ImportListener extends AnalysisEventListener<Map<Integer, String>> {

    List<Map<Integer, String>> list = new LinkedList<>();

    private ImportService importService;

    public RuleImportListener(RuleService ruleService) {
        this.ruleService = ruleService;
    }

    /**
     * 读一行调用一次该方法
     *
     * @param data            读到的每一行数据
     * @param analysisContext AnalysisContext
     */
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
        list.add(data);
    }

    /**
     * 读完一个 sheet 调用
     *
     * @param analysisContext AnalysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        if (list.size() > 1) {
            // 导入模板中的描述信息行不导入
            list.remove(0);
            // 读完一个 sheet 保存一次数据,根据具体业务需要实现数据持久化即可
            this.importService.saveData(list, analysisContext.readSheetHolder().getSheetName());
        }
        list.clear();
    }
}

ExcelUtil,对 Easy Excel 读写的包装工具类

package com.carnation.utils;

import com.alibaba.excel.EasyExcelFactory;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.carnation.common.exception.BusinessException;
import com.carnation.domain.dto.rule.ExcelExportDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;

/**
 * @author zy
 * @date 2021/4/28
 * @since 1.0.0
 */
public class ExcelUtil {

    private static final Logger log = LoggerFactory.getLogger(ExcelUtil.class);

    /**
     * 读 Excel 文件
     *
     * @param inputStream 数据源
     * @param listener    {@link com.alibaba.excel.read.listener.ReadListener} 的扩展类
     */
    public static void readExcel(InputStream inputStream, ReadListener listener) {
        ExcelReader excelReader = EasyExcelFactory.read(inputStream, listener).build();
        final List<ReadSheet> sheetList = excelReader.excelExecutor().sheetList();
        if (!CollectionUtils.isEmpty(sheetList)) {
            sheetList.forEach(readSheet -> {
                final String name = readSheet.getSheetName();
                log.info("------> 读取 sheet 页【{}】开始", name);
                excelReader.read(readSheet);
                log.info("------> 读取 sheet 页【{}】结束", name);
            });
        } else {
            final String error = "从 Excel 文档中未读取到 sheet 页,请检查文档内容";
            log.error(error);
            throw new BusinessException(error);
        }
        excelReader.finish();
    }

    /**
     * 写入数据到 Excel
     *
     * @param outputStream OutputStream
     * @param exportData   待写入 Excel 的数据
     */
    public static void writeExcel(OutputStream outputStream, List<ExcelExportDTO> exportData) {
        final ExcelWriter excelWriter = EasyExcelFactory.write(outputStream).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
        WriteSheet writeSheet;
        if (!CollectionUtils.isEmpty(exportData)) {
            for (int i = 0; i < exportData.size(); i++) {
                final ExcelExportDTO excelExportDTO = exportData.get(i);
                final String sheetName = excelExportDTO.getSheetName();
                log.info("------> 开始写入数据到 sheet 页【{}】", sheetName);
                // 每次写一个 sheet 都要创建 writeSheet,这里注意必须指定 sheetNo
                writeSheet = EasyExcelFactory.writerSheet(i, sheetName)
                        .head(excelExportDTO.getHead()).build();
                excelWriter.write(excelExportDTO.getData(), writeSheet);
                log.info("------> 写入数据到 sheet 页【{}】完成", sheetName);
            }
        }
        excelWriter.finish();
    }

    /**
     * 导出 Excel 文件
     *
     * @param response   HttpServletResponse
     * @param exportData 导出的数据
     * @param name       导出文件的名称
     */
    public static void exportExcel(HttpServletResponse response, List<ExcelExportDTO> exportData, String name) {
        response.reset();
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        // 这里 URLEncoder.encode 可以防止中文乱码
        try (ServletOutputStream outputStream = response.getOutputStream()) {
            String fileName = URLEncoder.encode(name, "UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
            ExcelUtil.writeExcel(outputStream, exportData);
        } catch (UnsupportedEncodingException e) {
            final String error = "构建下载文件名称出错";
            log.error(error, e);
            throw new BusinessException(error);
        } catch (IOException e) {
            final String error = "写入数据到导出 Excel 文件出错";
            log.error(error, e);
            throw new BusinessException(error);
        }
    }

    private ExcelUtil() {
    }
}

ExcelExportDTO,导出 Excel DTO 对象(仅供参考)

package com.carnation.domain.dto.rule;

import java.io.Serializable;
import java.util.List;

/**
 * 导出 Excel DTO
 *
 * @author zy
 * @date 2021/4/27
 * @since 1.0.0
 */
public class ExcelExportDTO implements Serializable {

    private static final long serialVersionUID = -9159070866860531020L;

    /**
     * sheet 页名称
     */
    private String sheetName;
    /**
     * 待写入每个 sheet 页的内容
     */
    private List<List<String>> data;
    /**
     * sheet 页 title 行内容
     */
    private List<List<String>> head;

    public String getSheetName() {
        return this.sheetName;
    }

    public void setSheetName(String sheetName) {
        this.sheetName = sheetName;
    }

    public List<List<String>> getData() {
        return this.data;
    }

    public void setData(List<List<String>> data) {
        this.data = data;
    }

    public List<List<String>> getHead() {
        return this.head;
    }

    public void setHead(List<List<String>> head) {
        this.head = head;
    }

    @Override
    public String toString() {
        return "RuleExportDTO{" +
                "sheetName='" + sheetName + '\'' +
                ", data=" + data +
                ", head=" + head +
                '}';
    }
}