EasyExcel 导入导出封装工具

时间:2025-03-10 14:20:29

封装

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import cn.it.learning.constant.ExportConstant;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import com.alibaba.excel.write.style.column.SimpleColumnWidthStyleStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;

/**
 * @Author it-learning
 * @Description excel导出工具包
 * @Date 2022/2/25 18:05
 * @Version 1.0
 */
@Slf4j
@Component
public class ExcelExportUtil<T> {


    /**
     * excel文件导出(可以包含多个sheet页),固定表头(通过实体指定属性的方式)
     *
     * @param response
     * @param fileName   导出文件名
     * @param head       导出表头(多个sheet页就是多个集合元素)
     * @param exportData 需要导出数据
     * @param sheetNames sheet页的名称,为空则默认以:sheet + 数字规则命名
     */
    public static <T> void exportFile(String fileName, List<T> head, List<List<T>> exportData, List<String> sheetNames, HttpServletResponse response) {
        if (Objects.isNull(response) || StrUtil.isBlank(fileName) || CollUtil.isEmpty(head)) {
            log.info("ExcelExportUtil exportFile required param can't be empty");
            return;
        }
        ExcelWriter writer = null;
        try {
            response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
            response.setCharacterEncoding(ExportConstant.UTF_8);
            response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.XLSX_SUFFIX);
            // 设置导出的表格样式
            HorizontalCellStyleStrategy horizontalCellStyleStrategy = getExportDefaultStyle();
            writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(horizontalCellStyleStrategy).build();
            for (int itemIndex = 0; itemIndex < exportData.size(); itemIndex++) {
                // 表头数据
                Object headData = head.get(itemIndex);
                // sheet页的数据
                List<T> list = exportData.get(itemIndex);
                WriteSheet sheet = EasyExcel.writerSheet(itemIndex, CollUtil.isEmpty(sheetNames) ? ExportConstant.SHEET_NAME + itemIndex + 1 : sheetNames.get(itemIndex)).head(headData.getClass()).build();
                writer.write(list, sheet);
            }
        } catch (Exception e) {
            log.error("ExcelExportUtil exportFile in error:{}", e);
        } finally {
            if (null != writer) {
                writer.finish();
            }
        }
    }


    /**
     * 导出动态表头数据(支持多表单),表头列不固定,根据程序或者读取数据库生成
     *
     * @param fileName   导出文件名
     * @param head       导出表头列
     * @param exportData 需要导出的数据
     * @param sheetNames sheet页名称
     * @param response   响应流
     */
    public static <T> void exportWithDynamicData(String fileName, List<List<List<String>>> head, List<List<List<T>>> exportData, List<String> sheetNames, HttpServletResponse response) throws IOException {
        ExcelWriter writer = null;
        try {
            response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
            response.setCharacterEncoding(ExportConstant.UTF_8);
            response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.XLSX_SUFFIX);
            // 设置默认样式的excel表格对象
            HorizontalCellStyleStrategy horizontalCellStyleStrategy = getExportDefaultStyle();
            AbstractColumnWidthStyleStrategy columnWidthStyleStrategy = new SimpleColumnWidthStyleStrategy(ExportConstant.DEFAULT_CELL_LENGTH);
            writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(horizontalCellStyleStrategy).registerWriteHandler(columnWidthStyleStrategy).build();
            for (int i = 0; i < exportData.size(); i++) {
                List<List<T>> tableData = exportData.get(i);
                WriteSheet sheet = EasyExcel.writerSheet(i, CollectionUtil.isEmpty(sheetNames) ? ExportConstant.SHEET_NAME + i + 1 : sheetNames.get(i)).head(head.get(i)).build();
                writer.write(tableData, sheet);
            }
        } finally {
            if (Objects.nonNull(writer)) {
                writer.finish();
            }
        }
    }

    /**
     * 下载指定路径下的模板
     *
     * @param filePath 文件所在路径(包含模板名称):一般是放在项目的resources目录下的template
     * @param fileName 下载时默认的文件名称
     * @param response 响应流
     */
    public static void downloadTemplate(String filePath, String fileName,String fileSuffix, HttpServletResponse response) {
        try {
            // 设置浏览器以附件形式读取响应流中的数据
            response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE);
            response.setCharacterEncoding(ExportConstant.UTF_8);
            response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + fileSuffix);
            byte[] bytes = ResourceUtil.readBytes(filePath);
            response.getOutputStream().write(bytes);
        } catch (Exception e) {
            log.error("ExcelExportUtil downloadTemplate in error:{}", e);
        }
    }

    /**
     * 配置默认的excel表格样式对象
     *
     * @return
     */
    private static HorizontalCellStyleStrategy getExportDefaultStyle() {
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        //设置头字体
        WriteFont headWriteFont = new WriteFont();
        headWriteFont.setBold(true);
        headWriteCellStyle.setWriteFont(headWriteFont);
        //设置头居中
        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        //内容策略
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        //设置 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
        return horizontalCellStyleStrategy;
    }

}


import cn.it.learning.refactor.ThrowingConsumer;
import cn.it.learning.util.excel.ExcelImportCommonListener;
import com.alibaba.excel.EasyExcel;

import java.io.InputStream;
import java.util.List;

/**
 * @Author it-learning-diary
 * @Description 导入excel模板
 * @Date 2022/4/24 14:09
 * @Version 1.0
 */
public class ExcelImportUtil<T> {

    /**
     * 通用导入excel文件方法
     *
     * @param fileStream 导入的文件流
     * @param rowDto 接收excel每行数据的实体
     * @param rowAction 将接收到的实体进行自定义的业务处理逻辑方法
     * @param <T> 实体类型
     */
    public static <T> void importFile(InputStream fileStream, T rowDto, ThrowingConsumer<List<T>> rowAction) {
        // 获取excel通用监听器
        ExcelImportCommonListener<T> commonListener = new ExcelImportCommonListener<>(rowAction);
        // 读取excel文件并导入
        EasyExcel.read(fileStream, rowDto.getClass(), commonListener).sheet().doRead();
    }

}

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.it.learning.constant.ImportConstant;
import cn.it.learning.refactor.ThrowingConsumer;
import cn.it.learning.valid.ImportValid;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ConverterUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * @Author it-learning-diary
 * @Description 数据导入通用监听器
 * @Date 2022/4/24 14:27
 * @Version 1.0
 */
@Slf4j
public class ExcelImportCommonListener<T> implements ReadListener<T> {

    /**
     * 转换后插入数据表的实体
     */
    private List<T> persistentDataList = Lists.newArrayList();

    /**
     * 具体数据落库的业务逻辑方法:此处的逻辑是将数据从excel中读取出来后,然后进行自己的业务处理,最后进行落库操作
     * 不懂的可以参考:UserServiceImpl下的saveUserList方法案例
     */
    private final ThrowingConsumer<List<T>> persistentActionMethod;

    /**
     * 异常日志记录(可用于记录解析excel数据时存储异常信息,用于业务的事务的回滚)
     */
    private List<String> errorLogList = new ArrayList<>();

    /**
     * 用于测试异常变量
     */
    private Long count = 1000L;

    /**
     * 构造函数(不包含异常信息)
     *
     * @param persistentActionMethod 从excel读取到的数据到落库的业务逻辑
     */
    public ExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod) {
        this.persistentActionMethod = persistentActionMethod;
    }

    /**
     * 构造函数(不包含异常信息)
     *
     * @param persistentActionMethod 从excel读取到的数据到落库的业务逻辑
     */
    public ExcelImportCommonListener(ThrowingConsumer<List<T>> persistentActionMethod, List<String> errorLogLIst) {
        this.persistentActionMethod = persistentActionMethod;
        this.errorLogList = errorLogLIst;
    }

    /**
     * 在转换异常 获取其他异常情况下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
     * 根据自己需要看是否抛出异常,如果没有特殊要求,则可以不修改。
     *
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
                    excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData().getStringValue());
            if (Objects.nonNull(errorLogList)) {
                // 记录异常日志
                String errorLog = "第" + excelDataConvertException.getRowIndex() + "行,第" + excelDataConvertException.getColumnIndex()
                        + "列解析异常,数据为:" + excelDataConvertException.getCellData().getStringValue() + "";
                errorLogList.add(errorLog);
            }
        }
    }

    /**
     * 返回每个sheet页的表头,根据自己实际业务进行表头字段等校验逻辑,如果没有则保持不动
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        Map<Integer, String> headMapping = ConverterUtils.convertToStringMap(headMap, context);
        log.info("表头数据: " + StrUtil.toString(headMapping));
        if (CollUtil.isEmpty(headMapping)) {
            errorLogList.add("The header of file can't be empty!");
        }
    }

    /**
     * 解析excel表中每行数据
     *
     * @param t
     * @param analysisContext
     */
    @Override
    public void invoke(T t, AnalysisContext analysisContext) {
        try {
            // 如果对实体需要设置其他额外属性,可以通过反射方式,如下面的relationId属性
            Class<?> aClass = t.getClass();
            Field relationIdField = aClass.getDeclaredField("relationId");
            relationIdField.setAccessible(Boolean.TRUE);
            relationIdField.set(t, count++);
        } catch (Exception e) {
            log.error("in error{}", e);
            errorLogList.add("The Row Data inject relationId field in error");
        }
        // 校验导入字段
        ImportValid.validRequireField(t,errorLogList);
        if (Objects.isNull(errorLogList) || CollUtil.isEmpty(errorLogList)) {
            persistentDataList.add(t);
            // 当数据达到最大插入数量后则进行落库操作,防止大数量情况下OOM
            if (persistentDataList.size() >= ImportConstant.MAX_INSERT_COUNT) {
                // 进行业务数据插入
                this.persistentDataToDb(persistentDataList);
                // 清空集合
                persistentDataList.clear();
            }
        }
    }


    /**
     * 所有数据解析完后,回调
     *
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 在此处调用落库操作,防止有剩余数据未落库
        if (CollUtil.isNotEmpty(persistentDataList)) {
            persistentDataToDb(persistentDataList);
        }
    }

    /**
     * 将数据持久化到数据库中
     *
     * @param data
     */
    private void persistentDataToDb(List<T> data) {
        // 对数据分组,批量插入
        List<List<T>> dataList = ListUtil.split(data, ImportConstant.MAX_INSERT_COUNT);
        dataList.stream().forEach(persistentActionMethod);
    }
}


使用示例:


/**
     * 导入用户数据案例
     *
     * @param file
     */
    @Transactional(rollbackFor = Exception.class)
    public void uploadUserListDemoWithExcel(MultipartFile file, String username) throws Exception {
        // 此处先校验导入的文件类型是否为excel
        String type = FileTypeUtil.getType(file.getInputStream());
        if (StrUtil.isBlank(type) || type.contains(ImportConstant.XLS_TYPE) || type.contains(ImportConstant.XLSX_TYPE)) {
            // 返回校验失败信息
            return;
        }
        User user = new User();
        user.setId(100);
        user.setName("外层");
        user.setPassword("外层");
        userService.save(user);
        // 调用统一导入方法
        ExcelImportUtil.importFile(file.getInputStream(), new UserDto(), UserServiceImpl::saveUserList);
    }

 /**
     * 导出案例
     *
     * @param response
     */
    public void exportUserListDemoWithExcel(HttpServletResponse response) {
        // 表头(使用excel中的注解定义,如果表头不固定,请使用进行导出)
        List<UserExportVo> head = Stream.of(new UserExportVo()).collect(Collectors.toList());
        // 数据(使用两层list为了兼容多个sheet页,如果是不同的sheet页则放在不同的List集合中)
        List<List<UserExportVo>> exportDataList = new ArrayList<>();
        List<UserExportVo> exportItem = new ArrayList<>();
        // 查询数据
        List<User> dbData = userService.list();
        // 将数据转换成导出需要的实际数据格式,此处只是演示
        for (User user : dbData) {
            UserExportVo vo = new UserExportVo();
            BeanUtil.copyProperties(user, vo);
            exportItem.add(vo);
        }
        exportDataList.add(exportItem);
        // sheet页名称-自定义,如果没有则传空
        List<String> sheetNameList = Stream.of("sheet1").collect(Collectors.toList());
        ExcelExportUtil.exportFile("user", head, exportDataList, sheetNameList, response);
    }

参考文档:
it-wheels-king

并发编程实战:多线程+EasyExcel,20ms内极速导入百万级大数据报表

EasyExcel模板填充技术的魅力