Easyexcel工具导入导出Excel

1.简介

EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到几M,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便 --(摘自官方GitHub)

Easyexcel官网:https://alibaba-easyexcel.github.io/index.html
Easyexcel Github地址:https://github.com/alibaba/easyexcel
MyBatis-Plus官网:https://mp.baomidou.com/guide/

本文是基于官方示例编写的,使用Springboot(2.2.2.RELEASE)、Mybatis-Plus(3.1.2)、EasyExcel(2.1.6)和Thymeleaf前端框架,实现了Excel导入导出功能,包括日期、数字的格式化以及自定义格式转换

2.实现Easyexcel导入导出

本文仅展示了主流程相关代码,若需要完整的Demo,可以自行下载(链接)

1.1 创建实体类User

@Data是Lombok的Jar包内的,Jar包安装成功仍然报错的话是因为IDE需要安装Lombok相关插件
@ExcelProperty(index=0)注解用来指定哪些字段和excel列对应,其中index表示在excel中第几列

用于Excel和Java之间数据暂存交互:

package com.lee.demo.easyexcel.entity;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.format.NumberFormat;
import com.lee.demo.easyexcel.utils.SexConverter;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDate;
import java.util.Date;

/**
 * @ClassName: com.lee.demo.easyexcel.entity.User
 * @Author: Jimmy
 * @Date: 2020/1/11 23:32
 * @Description: 用户实体类
 * 1.@Data是Lombok的Jar包内的,报错的话是因为IDE需要安装Lombok相关插件
 * 2.@ExcelProperty(index=0)注解用来指定哪些字段和excel列对应,其中index表示在excel中第几列
 */
@Data
public class User implements Serializable {
    private String id;
    @ExcelProperty(index=1)
    private String username;
    @ExcelProperty(index=2)
    private String nickname;
    @ExcelProperty(index=3)
    private String password;
    @ExcelProperty(index=4)
    private String identitynum;
    @ExcelProperty(index=5, converter = SexConverter.class)
    private String sex;
    @ExcelProperty(index=6)
    private String age;
    @ExcelProperty(index=7)
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    private Date birthday;
    @ExcelProperty(index=8)
    @NumberFormat("#.##厘米")
    private Double height;
    @ExcelProperty(index=9)
    @NumberFormat("#.##斤")
    private Double weight;
    @ExcelProperty(index=10)
    private String telephone;
    @ExcelProperty(index=11)
    private String email;
    @ExcelProperty(index=12)
    private String address;
}

1.2 创建Controller层

用于提供web端接口,供前端调用:

package com.lee.demo.easyexcel.controller;

import com.lee.demo.easyexcel.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ClassName: UserController
 * @Author: Jimmy
 * @Date: 2020/1/11 23:44
 * @Description: TODO
 */
@RestController
@RequestMapping("/user")
public class UserController {

    private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

    /**
     * 文件上传解析并导入数据库
     * @param excel
     * @throws IOException
     */
    @PostMapping("import")
    public void importExcel(MultipartFile excel) throws IOException {
        try {
            userService.importExcel(excel);
            LOGGER.info("excel导入数据库成功!");
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("excel导入数据库失败!");
        }
    }

    /**
     * 查询数据库数据并导出Excel
     * @param response
     * @throws IOException
     */
    @GetMapping("export")
    public void exportExcel(HttpServletResponse response) throws IOException {
        try {
            userService.exportExcel(response);
            LOGGER.info("导出Excel成功!");
        } catch (Exception e) {
            e.printStackTrace();
            LOGGER.error("导出Excel失败!");
        }
    }

}

1.3 创建Service实现类

用于调用Easyexcel,实现代码主要的逻辑业务:

package com.lee.demo.easyexcel.service.impl;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lee.demo.easyexcel.dao.UserDao;
import com.lee.demo.easyexcel.entity.User;
import com.lee.demo.easyexcel.service.UserService;
import com.lee.demo.easyexcel.utils.ExcelListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @ClassName: UserServiceImpl
 * @Author: Jimmy
 * @Date: 2020/1/12 13:59
 * @Description: TODO
 */
@Transactional
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, User> implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    public void importExcel(MultipartFile excel) throws IOException {

        long startTime=System.currentTimeMillis();   //获取开始时间

        /**
         * 写法一:
         * 这里需要指定用哪个class去读,读取第一个sheet后文件流会自动关闭
         */
        EasyExcel.read(excel.getInputStream(), User.class, new ExcelListener()).sheet().doRead();

        /**
         * 写法二:
         * 该写法要记得手动关闭reader,读的时候会创建临时文件,到时磁盘会崩的
         */
        /*
        ExcelReader excelReader = EasyExcel.read(excel.getInputStream(), User.class, new ExcelListener()).build();
        ReadSheet readSheet = EasyExcel.readSheet(0).build(); //readSheet(sheetNo)表示读取第几个sheet,0表示第一个sheet
        excelReader.read(readSheet);
        excelReader.finish();
        */

        long endTime=System.currentTimeMillis(); //获取结束时间
        System.out.println("程序运行时间: "+(endTime-startTime)/1000+"秒");

    }

    @Override
    public void exportExcel(HttpServletResponse response) throws IOException {

        // 这里注意 有同学反映使用swagger 会导致各种问题,请直接用浏览器或者用postman
        try {
            long startTime=System.currentTimeMillis();   //获取开始时间

            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
            String fileName = URLEncoder.encode("测试", "UTF-8");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

            /**
             * 写法一:
             * 这里需要指定用哪个class去写,写入第一个sheet后文件流会自动关闭
             */
            //EasyExcel.write(response.getOutputStream(), User.class).sheet("模板").doWrite(data());

            /**
             * 写法二:
             * 该写法要记得手动关闭writer,不然会报异常
             */
            ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), User.class).build();
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            excelWriter.write(data(), writeSheet);
            // 千万别忘记finish 会帮忙关闭流
            excelWriter.finish();

            long endTime=System.currentTimeMillis(); //获取结束时间
            System.out.println("程序运行时间: "+(endTime-startTime)/1000+"秒");
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    /**
     * 使用MybatisPlus的条件查询方法查询所有数据
     * @return
     */
    private List<User> data() {
        QueryWrapper<User> queryWrapper = Wrappers.query();
        List<User> userList = userDao.selectList(queryWrapper);
        return userList;
    }

}

1.4 创建ExcelListener监听器

需要继承AnalysisEventListener类,用于监听Excel的解析情况,它提供了一些父类方法,能够很方便的在解析后加入业务代码,比如新增至数据库中:

package com.lee.demo.easyexcel.utils;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.fastjson.JSON;
import com.lee.demo.easyexcel.entity.User;
import com.lee.demo.easyexcel.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @ClassName: ExcelListener
 * @Author: Jimmy
 * @Date: 2020/1/12 14:08
 * @Description: 模板读取类
 */
@Component
public class ExcelListener extends AnalysisEventListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(ExcelListener.class);

    @Autowired
    private static UserService userService;

    public ExcelListener() {
    }

    /**
     * 此处必须使用构造器来注入spring管理的类,不然使用@Autowired会注入失败,会报userService类NullPointerException
     * @param userService
     */
    @Autowired
    public ExcelListener(UserService userService) {
        this.userService = userService;
    }


    /**
     * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 3000;
    List<User> list = new ArrayList<User>();

    /**
     * 这个每一条数据解析都会来调用
     */
    @Override
    public void invoke(Object object, AnalysisContext context) {
        LOGGER.info("解析到一条数据:{}", JSON.toJSONString(object));
        list.add((User)object);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM(Out Of Memory)
        if (list.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            list.clear();
        }
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        //这里也要保存数据,确保批量新增最后一批遗留的数据也存储到数据库
        saveData();
        LOGGER.info("所有数据解析完成并存储成功!");
    }

    /**
     * 使用MybatisPlus的批量新增方法插入数据
     */
    public void saveData() {
        userService.saveBatch(this.list,BATCH_COUNT);
    }

    public List<User> getList() {
        return list;
    }

    public void setList(List<User> list) {
        this.list = list;
    }
}

此处必须使用构造器来注入Spring管理的Service类,且ExcelListener类也必须加上@Component注解来让Spring管理,不然使用@Autowired会注入失败,会报userService类NullPointerException。

1.5 日期、数字或者自定义类型的格式转换

1)日期

Easyexcel暂不支持JDK8的LocalDate类型,所以在格式化日期时间字段的时候还是得用Date类型:

    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    private Date birthday;
2)数字

字段类型不能为String,不然会转换失败:

    @NumberFormat("#.##厘米")
    private Double height;
    @ExcelProperty(index=9)
    @NumberFormat("#.##斤")
    private Double weight;
3)自定义类型

自定义"性别"转换器:

package com.lee.demo.easyexcel.utils;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * @ClassName: GenderConverter
 * @Author: Jimmy
 * @Date: 2020/1/14 10:59
 * @Description: 性别 自定义转换器
 */
public class SexConverter implements Converter<String> {

    public static final String MALE = "男";
    public static final String FEMALE = "女";

    @Override
    public Class supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    /**
     * Excel转Java,用于Excel导入操作
     * @param cellData
     * @param excelContentProperty
     * @param globalConfiguration
     * @return
     * @throws Exception
     */
    @Override
    public String convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        String stringValue = cellData.getStringValue();
        if (MALE.equals(stringValue)){
            return "0";
        }else {
            return "1";
        }
    }

    /**
     * Java转Excel,用于Excel导出操作
     * @param value
     * @param excelContentProperty
     * @param globalConfiguration
     * @return
     * @throws Exception
     */
    @Override
    public CellData convertToExcelData(String value, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        CellData cellData = new CellData(value);
        if (value.equals("0")){
            cellData.setStringValue(MALE);
        }else if (value.equals("1")){
            cellData.setStringValue(FEMALE);
        }
        return cellData;
    }
}

1.6 测试

导出测试:

执行Junit测试实例中的initializeDatas()方法初始化100万条数据:
Easyexcel工具导入导出Excel_第1张图片
启动服务,访问http://localhost:8080/index打开测试页面:
Easyexcel工具导入导出Excel_第2张图片
点击"Excel导出",可发现当前数据量"导出时间"大概在90秒左右:
在这里插入图片描述
Easyexcel工具导入导出Excel_第3张图片

导入测试:

通过sql脚本删除表数据:

DELETE FROM `user`

打开测试页面,"选择文件"后点击"Excel导入",可以发现当前数据量"导入时间"大概在736秒左右:
在这里插入图片描述
Easyexcel工具导入导出Excel_第4张图片

导入、导出时间和硬件计算速度、网络等因素有关,测试仅作参考。总而言之,相比EasyExcel的底层POI相比,EasyExcel的解析性能是大大超过原生POI的,而且内存占用率极低。

3.示例开发所遇问题:

3.1 文件上传时报错

org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (48207514) exceeds the configured maximum (10485760)

解决方案:参考我另一博文(传送门)

3.2 userService自动注入失败,报空指针异常

解决方案:参考博文(传送门)

你可能感兴趣的:(POI)