SpringBoot集成EasyExcel

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 简述在SpringBoot中集成EasyExcel完成数据导入功能。

[toc]

EasyExcel介绍

优缺点

  • 速度快,对于大批量数据导入可忽略性能问题。实测百万级别的表,一次性导入20W数据仅需20s,而解析excel数据到javalist仅用了4s。
  • 自带逐行导入功能,能够实现类似分页导入的功能
  • 使用方便,例如封装了EasyExcel.write,传一个HttpServletResponse 对象进去可以直接给调用者响应一个excel
  • 不适合用来处理花里胡哨的、复杂的excel模板,可拓展性相对ApachePoi有阉割

示例代码

功能较为简单,主要是演示实际开发中如何编写代码。

excel示例

excel-template

业务流程

客户端请求服务端,传递一个文件(excel或者csv),服务的读取文件中的数据并保存到数据库,对于文件中的错误数据或者库里已经存在重复id的数据将不会导入,错误数据会写入一个新的excel文件,并在请求响应的时候反馈给客户端。

pom


<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.0</version>
</dependency>

全量读取excel

顾名思义就是一次性将excel中的所有数据读取到java list中

case1-controller

  • 需要全局增加异常捕获和统一响应

@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/import")
public class ImportController {
    private ImportService importService;
    /**
     * 方式一:
     * 1:采用名称的方式映射表格列和实体
     */
    @PostMapping("/case1")
    public void importCase1(@RequestParam("file") MultipartFile file, HttpServletResponse response) throws IOException {
        FileTypeEnum fileType = FileTypeEnum.getFileType(file.getOriginalFilename());
        List<DataImportCase1Dto> data = new ArrayList<>();
        try {
            if (FileTypeEnum.CSV.equals(fileType)) {
                //CSV格式需要显示指定ExcelType
                data = EasyExcel.read(file.getInputStream()).excelType(ExcelTypeEnum.CSV).head(DataImportCase1Dto.class).sheet().doReadSync();
            } else if (FileTypeEnum.EXCEL.equals(fileType)) {
                data = EasyExcel.read(file.getInputStream()).head(DataImportCase1Dto.class).sheet().doReadSync();
            }
        } catch (Exception e) {
            log.error("importCase1-error,e={},stackTrace={}", e.getMessage(), e.getStackTrace());
            throw new RuntimeException("导入失败");
        }
        importService.importCase1(data, response);
    }
}

case1-service


@Slf4j
@Service
public class ImportService {
    /**
     *
     * @param data
     * @param response
     * @throws IOException
     */
    public void importCase1(List<DataImportCase1Dto> data, HttpServletResponse response) throws IOException {
        Assert.notEmpty(data, "参数不可为空");
        //todo:
        //1:校验data中的数据是否重复,例如编号是否重复
        //2:校验data中的编号是否与库里数据重复
        //3:校验业务数据是否符合要求
        //4:执行入库
        //5:收集不符合要求的数据,反馈给调用者一个excel
        List<DataImportResultDto> errorList = new ArrayList<>();
        errorList = data.stream().map(x -> {
            DataImportResultDto error = new DataImportResultDto();
            BeanUtils.copyProperties(x, error);
            error.setExplain("这里是错误描述");
            return error;
        }).collect(Collectors.toList());
        if (!CollectionUtils.isEmpty(errorList)) {
            EasyExcel.write(response.getOutputStream())
                    //.head(DataImportResultDto.head())
                    .head(DataImportResultDto.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("错误数据")
                    .doWrite(errorList);
        }
    }
}

case1-导入dto

  • 通过 com.alibaba.excel.annotation.ExcelProperty 注解将excel列映射到dto属性
  • 建议所有的字段都使用string类型,这种做法增强了功能的可用性。例如:如果用户将double 类型的数据填写了中文,如果dto直接定义double,那么将发生异常,并且其他数据也无法进行导入操作。虽然数据不符合业务要求,但起码在excel转list的时候程序不应报异常,对于业务数据的校验可以放到service中去做。
  • dto不可加链式访问注解 @Accessors(chain = true),加了之后读取到的数据都是null,读写反射对象用到了Cglib,加了此注解后无法被Cglib读取
  • 若excel中的列名和dto中的属性名一样,无需指定ExcelProperty,如本示例中的address属性

@Data
public class DataImportCase1Dto {
    @ExcelProperty("编号")
    private String id;
    @ExcelProperty("名称")
    private String name;
    private String address;
    @ExcelProperty("类型")
    private String type;
    @ExcelProperty("宽")
    private String width;
    @ExcelProperty("高")
    private String height;
    @ExcelProperty("来源")
    private String source;
    @ExcelProperty("备注")
    private String remark;
}

case1-错误数据dto

在导入完毕之后,将错误数据收集起来,放在一个新的excel中,响应给调用方


@Data
public class DataImportResultDto {
    @ExcelProperty("编号")
    private String id;
    @ExcelProperty("名称")
    private String name;
    private String address;
    @ExcelProperty("类型")
    private String type;
    @ExcelProperty("宽")
    private String width;
    @ExcelProperty("高")
    private String height;
    @ExcelProperty("来源")
    private String source;
    @ExcelProperty("备注")
    private String remark;
    @ExcelProperty("错误描述")
    private String explain;
    public static List<List<String>> head() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("编号");
        List<String> head1 = new ArrayList<String>();
        head1.add("名称");
        List<String> head2 = new ArrayList<String>();
        head2.add("地址");
        List<String> head3 = new ArrayList<String>();
        head3.add("类型");
        List<String> head4 = new ArrayList<String>();
        head4.add("宽");
        List<String> head5 = new ArrayList<String>();
        head5.add("高");
        List<String> head6 = new ArrayList<String>();
        head6.add("来源");
        List<String> head7 = new ArrayList<String>();
        head7.add("备注");
        List<String> head8 = new ArrayList<String>();
        head8.add("错误描述");
        list.add(head0);
        list.add(head1);
        list.add(head2);
        list.add(head3);
        list.add(head4);
        list.add(head5);
        list.add(head6);
        list.add(head7);
        list.add(head8);
        return list;
    }
}

FileTypeEnum


@AllArgsConstructor
@Slf4j
@Getter
public enum FileTypeEnum {
    /**
     * csv
     */
    CSV("csv", "csv"),
    /**
     * excel
     */
    EXCEL("xlsx", "excel"),
    ;
    private final String type;
    private final String describe;
    public static FileTypeEnum getFileType(String fileName) {
        if (StringUtils.hasText(fileName)) {
            String[] array = fileName.split("\\.");
            if (array.length > 0) {
                for (FileTypeEnum item : FileTypeEnum.values()) {
                    if (item.getType().equals(array[array.length - 1])) {
                        return item;
                    }
                }
            }
        }
        return null;
    }
}

逐行读取excel

一行一行的读取excel,官方给出的示例中引入了一个缓存的概念,每读一行将结果缓存起来,当缓存数据量达到指定阀值的时候批量操作一次入库

逐行读取的优点有:

  • 尽可能避免了OOM问题
  • 分批之后对数据库更加友好,要知道实际生产中往往不是简单的导入,还有各种数据校验,而数据的校验会涉及很多db的读写操作
  • 能够更加精细化的监控和管理整个导入过程

case2-controller


    @PostMapping("/case2")
    public void importCase2(@RequestParam("file") MultipartFile file, HttpServletResponse response) throws IOException {
        FileTypeEnum fileType = FileTypeEnum.getFileType(file.getOriginalFilename());
        if (FileTypeEnum.CSV.equals(fileType)) {
            //CSV格式需要显示指定ExcelType
            EasyExcel.read(file.getInputStream(), new DemoDataListener(importService, response)).excelType(ExcelTypeEnum.CSV).head(DataImportCase2Dto.class).sheet().doReadSync();
        } else if (FileTypeEnum.EXCEL.equals(fileType)) {
            EasyExcel.read(file.getInputStream(), new DemoDataListener(importService, response)).head(DataImportCase2Dto.class).sheet().doReadSync();
        }
    }

case2-listener

有个很重要的点 DemoDataListener 不能被spring管理,也就是说不能添加 @Service 类似的注解。要每次读取excel都要new,然后里面用到spring容器中的组件可以通过构造方法传进去。如service等可以先在controller中注入,然后在 new listener 的时候通过构造函数传递进去。


@Slf4j
public class DemoDataListener implements ReadListener<DataImportCase2Dto> {
    /**
     * 每隔 2 条存储数据库,实际使用中可以1000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 2;
    /**
     * 缓存的数据
     */
    private List<DataImportCase2Dto> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    private ImportService importService;
    private HttpServletResponse response;
    public DemoDataListener(ImportService service, HttpServletResponse res) {
        this.importService = service;
        this.response = res;
    }
    /**
     * 每解析一条数据都会回调到此方法
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context analysis context
     */
    @Override
    public void invoke(DataImportCase2Dto data, AnalysisContext context) {
        log.debug("DemoDataListener-invoke-data={}", JSON.toJSONString(data));
        //数据校验
        Boolean verifyResult = importService.verifyImportData(data);
        if (verifyResult) {
            cachedDataList.add(data);
            // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
            if (cachedDataList.size() >= BATCH_COUNT) {
                importService.importCase2(cachedDataList);
                // 存储完成清理 list
                cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
            }
        } else {
            log.debug("数据校验不通过,本行数据将放到错误数据excel中响应给调用者,即将开始解析下一行");
        }
    }
    /**
     * 所有数据解析完成会回调此方法
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        importService.importCase2(cachedDataList);
        //响应错误数据excel
        importService.responseErrorData(response);
        log.debug("DemoDataListener-所有数据解析完成");
    }
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("DemoDataListener-error,本行解析发生异常,但是可以继续解析下一行,msg={},stackTrace={}", exception.getMessage(), exception.getStackTrace());
        //如果是某一个单元格的转换异常 能获取到具体行号
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;
            log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
        }
    }
}

case2-service


@Slf4j
@AllArgsConstructor
@Service
public class ImportService {
    private UserService userService;
    /**
     * 错误数据集合
     */
    private List<DataImportResultDto> errorList = new CopyOnWriteArrayList<>();
    /**
     * @param data
     * @param response
     * @throws IOException
     */
    public void importCase1(List<DataImportCase1Dto> data, HttpServletResponse response) throws IOException {
        Assert.notEmpty(data, "参数不可为空");
        //todo:
        //1:校验data中的数据是否重复,例如编号是否重复
        //2:校验data中的编号是否与库里数据重复
        //3:校验业务数据是否符合要求
        //4:执行入库
        //5:收集不符合要求的数据,反馈给调用者一个excel
        List<DataImportResultDto> errorList = new ArrayList<>();
        errorList = data.stream().map(x -> {
            DataImportResultDto error = new DataImportResultDto();
            BeanUtils.copyProperties(x, error);
            error.setExplain("这里是错误描述");
            return error;
        }).collect(Collectors.toList());
        if (!CollectionUtils.isEmpty(errorList)) {
            EasyExcel.write(response.getOutputStream())
                    //.head(DataImportResultDto.head())
                    .head(DataImportResultDto.class)
                    .excelType(ExcelTypeEnum.XLSX)
                    .sheet("错误数据")
                    .doWrite(errorList);
        }
    }
    public void importCase2(List<DataImportCase2Dto> data) {
        log.debug("importCase2-分批导入,本次导入数据量={}", data.size());
    }
    public Boolean verifyImportData(DataImportCase2Dto data) {
        //1:校验data中的编号是否与库里数据重复
        //2:校验业务数据是否符合要求
        DataImportResultDto error = new DataImportResultDto();
        BeanUtils.copyProperties(data, error);
        error.setExplain("这里是错误描述");
        errorList.add(error);
        return false;
    }
    /**
     * 响应错误数据
     */
    public void responseErrorData(HttpServletResponse response) {
        if (errorList.size() > 0) {
            //生成一个新的excel,返回给调用者
            try {
                EasyExcel.write(response.getOutputStream())
                        //.head(DataImportResultDto.head())
                        .head(DataImportResultDto.class)
                        .excelType(ExcelTypeEnum.XLSX)
                        .sheet("错误数据")
                        .doWrite(errorList);
            } catch (IOException e) {
                log.error("responseErrorData-error,msg={},stackTrace={}", e.getMessage(), e.getStackTrace());
            }
        }
    }
}

case2导入dto


@Data
public class DataImportCase2Dto {
    @ExcelProperty("编号")
    private String id;
    @ExcelProperty("名称")
    private String name;
    private String address;
    @ExcelProperty("类型")
    private String type;
    @ExcelProperty("宽")
    private String width;
    @ExcelProperty("高")
    private String height;
    @ExcelProperty("来源")
    private String source;
    @ExcelProperty("备注")
    private String remark;
}

工作流程

  • 解析Excel文件:EasyExcel 首先需要解析Excel文件,获取文件中的所有工作表和单元格。可以使用 POI 提供的 API 来解析 Excel 文件。
  • 格式化数据:一旦文件被解析,EasyExcel 可以将数据格式化为所需的格式。这可以通过使用 POI 提供的各种方法来完成,如设置单元格格式、填充数据等。
  • 生成 Java 对象:一旦数据被格式化,EasyExcel 可以将其转换为 Java 对象。这些对象可以包含数据、公式、图表等。
  • 写入 Excel 文件:最后,EasyExcel 可以将生成的 Java 对象写入到 Excel 文件中。这可以通过使用 POI 提供的 API 来完成,如创建工作簿、创建工作表、写入数据等。

代码

https://gitee.com/naylor_personal/ramble-spring-boot/tree/master/easyexcel

目录
相关文章
|
2月前
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
14天前
|
XML Java API
Spring Boot集成MinIO
本文介绍了如何在Spring Boot项目中集成MinIO,一个高性能的分布式对象存储服务。主要步骤包括:引入MinIO依赖、配置MinIO属性、创建MinIO配置类和服务类、使用服务类实现文件上传和下载功能,以及运行应用进行测试。通过这些步骤,可以轻松地在项目中使用MinIO的对象存储功能。
|
16天前
|
消息中间件 Java Kafka
什么是Apache Kafka?如何将其与Spring Boot集成?
什么是Apache Kafka?如何将其与Spring Boot集成?
47 5
|
18天前
|
消息中间件 Java Kafka
Spring Boot 与 Apache Kafka 集成详解:构建高效消息驱动应用
Spring Boot 与 Apache Kafka 集成详解:构建高效消息驱动应用
30 1
|
1月前
|
XML Java 数据库连接
SpringBoot集成Flowable:打造强大的工作流管理系统
在企业级应用开发中,工作流管理是一个核心组件,它能够帮助我们定义、执行和管理业务流程。Flowable是一个开源的工作流和业务流程管理(BPM)平台,它提供了强大的工作流引擎和建模工具。结合SpringBoot,我们可以快速构建一个高效、灵活的工作流管理系统。本文将探讨如何将Flowable集成到SpringBoot应用中,并展示其强大的功能。
138 1
|
1月前
|
存储 easyexcel Java
SpringBoot+EasyExcel轻松实现300万数据快速导出!
本文介绍了在项目开发中使用Apache POI进行数据导入导出的常见问题及解决方案。首先比较了HSSFWorkbook、XSSFWorkbook和SXSSFWorkbook三种传统POI版本的优缺点,然后根据数据量大小推荐了合适的使用场景。接着重点介绍了如何使用EasyExcel处理超百万数据的导入导出,包括分批查询、分批写入Excel、分批插入数据库等技术细节。通过测试,300万数据的导出用时约2分15秒,导入用时约91秒,展示了高效的数据处理能力。最后总结了公司现有做法的不足,并提出了改进方向。
|
19天前
|
消息中间件 监控 Java
您是否已集成 Spring Boot 与 ActiveMQ?
您是否已集成 Spring Boot 与 ActiveMQ?
36 0
|
1月前
|
JSON Java API
springboot集成ElasticSearch使用completion实现补全功能
springboot集成ElasticSearch使用completion实现补全功能
40 1
|
2月前
|
easyexcel Java UED
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
在SpringBoot环境中,为了优化大量数据的Excel导出体验,可采用异步方式处理。具体做法是将数据拆分后利用`CompletableFuture`与`ThreadPoolTaskExecutor`并行导出,并使用EasyExcel生成多个Excel文件,最终将其压缩成ZIP文件供下载。此方案提升了导出效率,改善了用户体验。代码示例展示了如何实现这一过程,包括多线程处理、模板导出及资源清理等关键步骤。
|
1月前
|
XML 存储 Java
SpringBoot集成Flowable:构建强大的工作流引擎
在企业级应用开发中,工作流管理是核心功能之一。Flowable是一个开源的工作流引擎,它提供了BPMN 2.0规范的实现,并且与SpringBoot框架完美集成。本文将探讨如何使用SpringBoot和Flowable构建一个强大的工作流引擎,并分享一些实践技巧。
103 0

热门文章

最新文章