Excel导入导出

简介

EasyPoi

EasyExcel

以上两种大家都可以根据业务情况自行选择使用,在使用之前请详细阅读文档,熟悉之后再看本章节的使用说明
注意:同一工程里面只能选择使用一种,避免冲突。

EasyPoi 使用

使用easyPoi无需额外引入maven包
什么场景该用那个方法

  • 导出
    1.正规excel导出 (格式简单,数据量可以,5W以内吧)
    注解方式: ExcelExportUtil.exportExcel(ExportParams entity, Class pojoClass,Collection dataSet)
    2.不定多少列,但是格式依然简单数据库不大
    自定义方式: ExcelExportUtil.exportExcel(ExportParams entity, List entityList,Collection<?> dataSet) 3.数据量大超过5W,还在100W以内 注解方式 ExcelExportUtil.exportBigExcel(ExportParams entity, Class<?> pojoClass,IExcelExportServer server, Object queryParams)
    自定义方式: ExcelExportUtil.exportBigExcel(ExportParams entity, List excelParams,IExcelExportServer server, Object queryParams)
    4.样式复杂,数据量尽量别大
    模板导出 ExcelExportUtil.exportExcel(TemplateExportParams params, Map<String, Object> map)
    5.一次导出多个风格不一致的sheet
    模板导出 ExcelExportUtil.exportExcel(Map<Integer, Map<String, Object>> map,TemplateExportParams params)
    6.一个模板但是要导出非常多份
    模板导出 ExcelExportUtil.exportExcelClone(Map<Integer, List<Map<String, Object>>> map,TemplateExportParams params)
    7.模板无法满足你的自定义,试试html
    自己构造html,然后我给你转成excel ExcelXorHtmlUtil.htmlToExcel(String html, ExcelType type)
    8.数据量过百万级了.放弃excel吧,csv导出
    注解方式: CsvExportUtil.exportCsv(CsvExportParams params, Class<?> pojoClass, OutputStream outputStream)
    自定义方式: CsvExportUtil.exportCsv(CsvExportParams params, List entityList, OutputStream outputStream)
    9.word导出
    模板导出: WordExportUtil.exportWord07(String url, Map<String, Object> map)

  • 导入
    如果想提高性能 ImportParams 的concurrentTask 可以帮助并发导入,仅单行,最小1000
    excel有单个的那种特殊读取,readSingleCell 参数可以支持

    1. 不需要检验,数据量不大(5W以内)
        注解或者MAP: ExcelImportUtil.importExcel(File file, Class<?> pojoClass, ImportParams params)
    2. 需要导入,数据量不大
        注解或者MAP: ExcelImportUtil.importExcelMore(InputStream inputstream, Class<?> pojoClass, ImportParams params)
    3. 数据量大了,或者你有特别多的导入操作,内存比较少,仅支持单行
        SAX方式  ExcelImportUtil.importExcelBySax(InputStream inputstream, Class<?> pojoClass, ImportParams params, IReadHandler handler)
    4. 数据量超过EXCEL限制,CSV读取
        小数据量: CsvImportUtil.importCsv(InputStream inputstream, Class<?> pojoClass,CsvImportParams params)
              大数据量: CsvImportUtil.importCsv(InputStream inputstream, Class<?> pojoClass,CsvImportParams params, IReadHandler readHandler)
    

如何使用

  1. 基于官方文档,定义一个excel映射类
  2. 服务层,实现Excel导出
  3. 实现excel导入
@Data
public class UserEntity implements Serializable {

    @Excel(name = "用户账号", height = 20, width = 30, isImportField = "true_st")
    private String userId;

    @Excel(name = "用户名称", height = 20, width = 30, isImportField = "true_st")
    private String username;

    @Excel(name = "学生性别", replace = { "男_1", "女_2" }, suffix = "生", isImportField = "true_st")
    private String sex;

    @Excel(name = "入校时间", format = "yyyy-MM-dd", isImportField = "true_st", width = 20)
    private Date createTime;
}
/**
* 导出用户信息
* @throws IOException
*/
@Override
public void userInfoExport() {
    // 数据库查询数据记录
    List<UserEntity> users = userMapper.userInfoExport();
    // 导出excel,传入参数 title,sheetName
    Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams("计算机一班学生","学生"),
                                                    UserEntity.class, users);
    // excel文件路径, 建议使用Java临时目录 System.getProperty("java.io.tmpdir")
    FileOutputStream fos = null;
    try {
        fos = new FileOutputStream("D:/home/excel/testStudentList.xls");
        workbook.write(fos);
        fos.close();
    } catch (FileNotFoundException e) {
        throw new YErrorException("导出用户信息时找不到文件!");
    } catch (IOException e) {
        throw new YErrorException("导出用户信息发生io错误!");
    } catch (Exception e) {
        throw new YErrorException("导出用户信息时出错!");
    }
}
/**
* 导入用户信息
* @throws IOException
*/
@Override
public void userInfoImport() {
    try {
        ImportParams params = new ImportParams();
        params.setHeadRows(2);
        // excel文件路径, 建议使用Java临时目录 System.getProperty("java.io.tmpdir")
        List<UserEntity> list = ExcelImportUtil.importExcel(
            new File("D:/home/excel/testStudentList.xls"), UserEntity.class, params);
        //将导入的数据存入数据库......
        //打印出来看看
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        list.forEach(in-> System.out.println(in));
    } catch (Exception e) {
        throw new YErrorException("导入用户信息时出错!");
    }
}

EasyExcel 使用

使用easyExcel需要引入如下依赖包
基础依赖

  1. 项目pom.xml中增加依赖
<!-- excel -->
<dependency>
  <groupId>com.ly.mssp</groupId>
  <artifactId>ly-ac-core-excel</artifactId>
  <version>${cloud.core.version}</version>
</dependency>

Excel准备

  1. 以Notice模块为例,基于官方文档,我们编写一个EasyExcel格式的bean
@Data
@ColumnWidth(25)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class NoticeExcel {

   @ColumnWidth(15)
   @ExcelProperty("标题")
   private String title;

   @ExcelIgnore
   @ColumnWidth(10)
   @ExcelProperty("类型")
   private Integer category;

   @ColumnWidth(20)
   @ExcelProperty("类型名称")
   private String categoryName;

   @ColumnWidth(10)
   @ExcelProperty("发布日期")
   private Date releaseTime;

   @ColumnWidth(30)
   @ExcelProperty("内容")
   private String content;

}
  1. 针对EasyExcel,ly-ac-core-excel 提供了轻量级的工具类ExcelUtil,具体代码如下,大家可以在阅读完官方文档后再阅读封装源码
package com.ly.cloud.core.excel.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.DateUtils;
import com.ly.cloud.core.excel.listener.DataListener;
import com.ly.cloud.core.excel.listener.ImportListener;
import com.ly.cloud.core.excel.support.ExcelException;
import com.ly.cloud.core.excel.support.ExcelImporter;
import lombok.SneakyThrows;
import org.apache.commons.codec.Charsets;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;

/**
 * Excel工具类
 *
 * @author zhumin
 * @apiNote https://www.yuque.com/easyexcel/doc/easyexcel
 */
public class ExcelUtil {

    /**
     * 读取excel的所有sheet数据
     *
     * @param excel excel文件
     * @return List<Object>
     */
    public static <T> List<T> read(MultipartFile excel, Class<T> clazz) {
        DataListener<T> dataListener = new DataListener<>();
        ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz);
        if (builder == null) {
            return null;
        }
        builder.doReadAll();
        return dataListener.getDataList();
    }

    /**
     * 读取excel的指定sheet数据
     *
     * @param excel   excel文件
     * @param sheetNo sheet序号(从0开始)
     * @return List<Object>
     */
    public static <T> List<T> read(MultipartFile excel, int sheetNo, Class<T> clazz) {
        return read(excel, sheetNo, 1, clazz);
    }

    /**
     * 读取excel的指定sheet数据
     *
     * @param excel         excel文件
     * @param sheetNo       sheet序号(从0开始)
     * @param headRowNumber 表头行数
     * @return List<Object>
     */
    public static <T> List<T> read(MultipartFile excel, int sheetNo, int headRowNumber, Class<T> clazz) {
        DataListener<T> dataListener = new DataListener<>();
        ExcelReaderBuilder builder = getReaderBuilder(excel, dataListener, clazz);
        if (builder == null) {
            return null;
        }
        builder.sheet(sheetNo).headRowNumber(headRowNumber).doRead();
        return dataListener.getDataList();
    }

    /**
     * 读取并导入数据
     *
     * @param excel    excel文件
     * @param importer 导入逻辑类
     * @param <T>      泛型
     */
    public static <T> void save(MultipartFile excel, ExcelImporter<T> importer, Class<T> clazz) {
        ImportListener<T> importListener = new ImportListener<>(importer);
        ExcelReaderBuilder builder = getReaderBuilder(excel, importListener, clazz);
        if (builder != null) {
            builder.doReadAll();
        }
    }

    /**
     * 导出excel
     *
     * @param response 响应类
     * @param dataList 数据列表
     * @param clazz    class类
     * @param <T>      泛型
     */
    @SneakyThrows
    public static <T> void export(HttpServletResponse response, List<T> dataList, Class<T> clazz) {
        export(response, DateUtils.format(new Date(), DateUtils.DATE_FORMAT_14), "导出数据", dataList, clazz);
    }

    /**
     * 导出excel
     *
     * @param response  响应类
     * @param fileName  文件名
     * @param sheetName sheet名
     * @param dataList  数据列表
     * @param clazz     class类
     * @param <T>       泛型
     */
    @SneakyThrows
    public static <T> void export(HttpServletResponse response, String fileName, String sheetName, List<T> dataList, Class<T> clazz) {
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding(Charsets.UTF_8.name());
        fileName = URLEncoder.encode(fileName, Charsets.UTF_8.name());
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(dataList);
    }

    /**
     * 获取构建类
     *
     * @param excel        excel文件
     * @param readListener excel监听类
     * @return ExcelReaderBuilder
     */
    public static <T> ExcelReaderBuilder getReaderBuilder(MultipartFile excel, ReadListener<T> readListener, Class<T> clazz) {
        String filename = excel.getOriginalFilename();
        if (StringUtils.isEmpty(filename)) {
            throw new ExcelException("请上传文件!");
        }
        if ((!StringUtils.endsWithIgnoreCase(filename, ".xls") && !StringUtils.endsWithIgnoreCase(filename, ".xlsx"))) {
            throw new ExcelException("请上传正确的excel文件!");
        }
        InputStream inputStream;
        try {
            inputStream = new BufferedInputStream(excel.getInputStream());
            return EasyExcel.read(inputStream, clazz, readListener);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

}

Excel导出
一、初次尝试

  1. 在NoticeController下新建接口 exportNotice
@GetMapping("export-notice")
public void exportNotice(HttpServletResponse response) {
   List<NoticeExcel> list = new ArrayList<>();
   ExcelUtil.export(response, "通知公告导出数据", "通知公告数据表", list, NoticeExcel.class);
}
  1. 浏览器访问:http://localhost:8080/notice/export-notice,查看下载结果image.pngimage.png
  2. 发现发布日期换行了,我们调整 bean 的 @ColumnWidth 配置image.png
  3. 再次导出发现正常image.png

二、数据导出

  1. 改造 exportNotice 我们进行数据填充
@GetMapping("export-notice")
public void exportNotice(HttpServletResponse response) {
   List<NoticeExcel> list = new ArrayList<>();

   NoticeExcel ne1 = new NoticeExcel();
   ne1.setTitle("标题1");
   ne1.setCategoryName("发布通知");
   ne1.setReleaseTime(DateUtil.now());
   ne1.setContent("内容1");

   NoticeExcel ne2 = new NoticeExcel();
   ne2.setTitle("标题2");
   ne2.setCategoryName("批转通知");
   ne2.setReleaseTime(DateUtil.now());
   ne2.setContent("内容2");

   list.add(ne1);
   list.add(ne2);

   ExcelUtil.export(response, "通知公告导出数据", "通知公告数据表", list, NoticeExcel.class);
}
  1. 浏览器访问:http://localhost/blade-desk/notice/export-notice,查看下载结果,可以看到数据填充成功image.png
  2. 后续若需要对接至数据库数据,则只需要编写mapper,返回对应的List类型即可

Excel读取
一、开始尝试

  1. 在NoticeController下新建接口 readNotice, 读取excel文件并返回list数据
@PostMapping("read-notice")
public R<List<NoticeExcel>> readNotice(MultipartFile file) {
   List<NoticeExcel> list = ExcelUtil.read(file, NoticeExcel.class);
   return R.data(list);
}
  1. 使用postman调用接口并上传我们上一章节导出的excel文件,查看返回数据,发现数据正确,但是category为-1image.png
  2. 对数据进行优化,通过字典缓存,回插对应的字段值
@PostMapping("read-notice")
public R<List<NoticeExcel>> readNotice(MultipartFile file) {
   List<NoticeExcel> list = ExcelUtil.read(file, NoticeExcel.class);
   list.forEach(noticeExcel -> {
      String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
      noticeExcel.setCategory(Func.toInt(category));
   });
   return R.data(list);
}
  1. 重启服务再次调用,查看返回结果,category字段已经回插成功image.png

二、拓展用法

  1. 除此之外,工具类还支持指定sheet来进行数据读取,若不指定sheet则是读取所有sheet再返回List
  2. 我们先改造下导出的excel,增加sheet,增加一行image.pngimage.png
  3. 方法修改如下
@PostMapping("read-notice")
public R<List<NoticeExcel>> readNotice(MultipartFile file, Integer sheetNo) {
   List<NoticeExcel> list = ExcelUtil.read(file, sheetNo, NoticeExcel.class);
   list.forEach(noticeExcel -> {
      String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
      noticeExcel.setCategory(Func.toInt(category));
   });
   return R.data(list);
}
  1. 使用postman调用接口,指定sheet2(sheetNo从0开始,sheet2对应的就是1),可以看到数据返回成功。image.png
  2. 去掉sheetNo字段,再次调用postman,可以看到返回了两个sheet的一共五条数据
@PostMapping("read-notice")
public R<List<NoticeExcel>> readNotice(MultipartFile file) {
   List<NoticeExcel> list = ExcelUtil.read(file, NoticeExcel.class);
   list.forEach(noticeExcel -> {
      String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
      noticeExcel.setCategory(Func.toInt(category));
   });
   return R.data(list);
}

Excel导入
一、普通导入

  1. 在读取接口的基础上增加逻辑,代码如下
@PostMapping("write-notice")
public R<Boolean> writeNotice(MultipartFile file) {
   List<Notice> noticeList = new ArrayList<>();
   List<NoticeExcel> list = ExcelUtil.read(file, NoticeExcel.class);
   list.forEach(noticeExcel -> {
      String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
      noticeExcel.setCategory(Func.toInt(category));
      Notice notice = BeanUtil.copy(noticeExcel, Notice.class);
      noticeList.add(notice);
   });
   return R.data(noticeService.saveBatch(noticeList));
}
  1. 主要核心为这几行代码,其中使用了BeanUtil把NoticeExcel转换成了Notice,性能层面可以放心,它与new完再set相差无几,要注意的就是两个bean的字段需要保持一致,才可以转换成功。
  • List noticeList = new ArrayList<>();
  • Notice notice = BeanUtil.copy(noticeExcel, Notice.class);
  • noticeList.add(notice);
  • noticeService.saveBatch(noticeList)
  1. 重启服务,使用postman调用对应接口,可以看到返回成功image.png
  2. 刷新下数据库,可以看到5条数据已经入库成功image.png
  3. 以上为普通导入,仅作为一种demo方式快速实现功能,不推荐大家使用到实际业务。因为很多时候需要导入的Excel会非常大,java的List容量是有限的,若数据量太大将会导致OOM,所以这种方式不推荐,那么下面就来实操下针对实际业务场景的excel导入方案。

二、进阶导入

  1. 在开动之前,我们先看一下官方文档,找到监听器的例子image.pngimage.png
  2. 从例子可以看出,针对每一个Bean,都需要新建一个Listener,继承AnalysisEventListener,指定类型。这样调用API读取的时候,就会将数据写入监听器从而实现我们的业务逻辑
  3. ExcelUtil对其进行了封装,抽出了通用的Listener,这样不需要每个Bean都实现AnalysisEventListener,只需要编写最简的导入逻辑便可
  4. 实现主要逻辑代码如下,为了防止大数据量OOM,设定了每3000条便会执行一次入库操作。
/**
 * Excel监听器
 *
 * @author zhumin
 */
@Data
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ImportListener<T> extends AnalysisEventListener<T> {

    /**
     * 默认每隔3000条存储数据库
     */
    private int batchCount = 3000;
    /**
     * 缓存的数据列表
     */
    private List<T> list = new ArrayList<>();
    /**
     * 数据导入接口类
     */
    private final ExcelImporter<T> importer;

    /**
     * 每一条数据解析都会调用
     *
     * @param data            解析到的数据
     * @param analysisContext
     */
    @Override
    public void invoke(T data, AnalysisContext analysisContext) {
        // 校验数据
        boolean validator = importer.validator(data);
        if (validator) {
            list.add(data);
        }

        // 达到BATCH_COUNT,则调用importer方法入库,防止数据几万条数据在内存,容易OOM
        if (list.size() >= batchCount) {
            // 调用importer方法
            importer.save(list);
            // 存储完成清理list
            list.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 调用importer方法
        importer.save(list);
        // 存储完成清理list
        list.clear();
    }

}
/**
* 读取并导入数据
*
* @param excel    excel文件
* @param importer 导入逻辑类
* @param <T>      泛型
*/
public static <T> void save(MultipartFile excel, ExcelImporter<T> importer, Class<T> clazz) {
    ImportListener<T> importListener = new ImportListener<>(importer);
    ExcelReaderBuilder builder = getReaderBuilder(excel, importListener, clazz);
    if (builder != null) {
        builder.doReadAll();
    }
}
  1. 新建NoticeImporter,并迁移上一节相关代码
/**
 * 通知数据导入类
 *
 * @author zhumin
 */
@RequiredArgsConstructor
public class NoticeImporter implements ExcelImporter<NoticeExcel> {

   private final INoticeService service;


   @Override
   public boolean validator(NoticeExcel data){

       return true;
   }

   @Override
   public void save(List<NoticeExcel> data) {
      data.forEach(noticeExcel -> {
         String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
         noticeExcel.setCategory(Func.toInt(category));
         Notice notice = BeanUtil.copy(noticeExcel, Notice.class);
         service.save(notice);
      });
   }
}
  1. 改造readNotice方法,传入NoticeImporter
@PostMapping("write-notice")
public R<Boolean> writeNotice(MultipartFile file) {
   NoticeImporter importer = new NoticeImporter(noticeService);
   ExcelUtil.save(file, importer, NoticeExcel.class);
   return R.success("操作成功");
}
  1. 重启服务,调用postman查看结果,发现数据导入成功,业务也完全解耦

结语

  • ExcelUtil是针对EasyExcel常用功能的轻量级封装,但没有对每个API都进行封装
  • 希望大家可以完整过几遍EasyExcel官方的文档:https://www.yuque.com/easyexcel/doc/easyexcel
  • 后续如果有业务场景需要使用新的功能,可以在熟悉EasyExcel后拓展到ExcelUtil中
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐