阿里开源的EasyExcel(导入、导出)

本文介绍了在Java中处理Excel文件时,ApachePOI和EasyExcel的区别,重点讲解了EasyExcel的内存优化特性以及如何使用EasyExcel进行数据导入导出,包括自定义转换类和在SpringBoot项目中的实现方法。

一、说明:

众所周知,Excel文件分为03版本excel和07版本excel文件

03版最多容量为65535*256,而07版单张sheet为1048576*16384,总容量无限制

03版的后缀名为.xls,07版的后缀是.xlsx

 二、对Excel文件处理

在Java领域中,对excel文件进行解析、生成比较闻名的框架是Apache poi和阿里的EasyExcel。但俩者有显著的区别。

POI技术在解析Excel文件时,是一次性将表里的数据加载到内存中,这很可能会导致内存溢出的问题,尤其是当excel的存在大数据量时,就很大可能会超出可用内存的内存限制。

所以POI技术非常消耗内存。

EasyExcel在解析文件时,是从磁盘上一行一行的进行读取数据,并通过设置监听器(观察者的模式)进行通知。因此,大大减少了占用内存。

三、使用EasyExcel

在了解的俩者区别后,我选择了使用EasyExcel来对excel文件进行处理

3.1导入依赖

在jdk版本为1.8及1.8以上时,导入EasyExcel的版本最好为3.xx的版本。

        //通用版本
        <dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>3.1.4</version>
		</dependency>
        
        //若依赖中存在导入或不使用Slf4j依赖则需要将其中封装的取消导入
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>easyexcel</artifactId>
			<version>3.1.4</version>
			<exclusions>
				<exclusion>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-api</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

3.2实体


	/**
	 * 用户ID
	 */
	@TableId(type = IdType.AUTO)
	@ExcelIgnore
	private Long userId;
	/**
	 * 用户名
	 */
	@ExcelProperty("用户名")
	private String username;
	/**
	 * 密码
	 */
	@ExcelIgnore
	private String password;
    /**
	 * 性别
	 */
	@ExcelProperty(value = "性别",converter = SexUtil.class)
	private int gender;
	/**
	 * 手机号
	 */
	@ExcelProperty("手机号")

	/**
	 * 邮箱
	 */
	@ExcelProperty("邮箱")
	private String email;
	/**
	 * 真实姓名
	 */
	@ExcelProperty("姓名")
	private String realName;
	/**
	 * 状态  0:禁用   1:正常
	 */
	@ExcelIgnore
	private Integer status;

	/**
	 * 创建时间
	 */
//	@ExcelIgnore
	@DateTimeFormat(value = "yyyy-MM-dd")
	@ExcelProperty(value = "创建时间",converter = DateConverter.class )
	private Date createTime;

	@TableLogic(value = "0", delval = "1")
	@ExcelIgnore
	private int del;

对于导入导出的显示在excel字段 可以添加注解:@ExcelProperty、@ExcelIgnore

前者是需要将字段进行处理,后者是不需要显示在excel则添加。如果一个属性什么注解都没有,则将显示原本的如:属性为:email,则在excel也显示 email。添加了@ExcelProperty("邮箱") 则显示 邮箱。

3.3自定义转换类

 在进行对性别属性的数据库存储时,我采用了存储数字(0:女、1:男)这种方式存储的原因在此我就不多说了,懂的都懂。那为什么我要在此说明这个属性呢?因为在进行文件的导入导出时,总不能对性别进行导入1、0或者导出1、0供用户查看吧?所以在此仍需写一个处理性别数据转换的类。在EasyExcel提供了Converter接口。在处理文件的导入导出时,此接口提供了俩个方法:convertToJavaData和convertToExcelData。具体细看如下

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public class SexUtil implements Converter<Integer> {
    /**
     * 导入时,对数据的男、女进行转换
     * @param cellData
     * @param contentProperty
     * @param globalConfiguration
     * @return
     * @throws Exception
     */
    @Override
    public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        String value = cellData.getStringValue();
        switch (value){
            case "男":
                return 1;
            case "女":
                return 0;
            default:
                return -1;
        }
    }

    /**
     * 导出时 对数据的1、0进行转换
     * @param value
     * @param contentProperty
     * @param globalConfiguration
     * @return
     * @throws Exception
     */
    @Override
    public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        switch (value){
            case 1:
                return new WriteCellData<>("男");
            case 0:
                return new WriteCellData<>("女");
            default:
                return new WriteCellData<>("未知");
        }
    }
}

 那怎么使用这个转换类?麻烦细看实体类中的性别字段

3.4IDEA中的导入实现

controller类

    /**
     * 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    @PostMapping("/upload")
    @ResponseBody
    public R upload(MultipartFile file) throws IOException {
        EasyExcel.read(file.getInputStream(), EmpEntity.class, new         
                        UploadEmpListener(empService)).sheet().doRead();
        return R.ok();
    }

根据EasyExcel官网提供信息:使用read方法  并且设置一个监听类。

EasyExcel.read(file.getInputStream(), EmpEntity.class, new         
                        UploadEmpListener(empService)).sheet().doRead();

至于参数传service还是dao就看具体的用法

 监听类

package com.wedu.modules.emp.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson.JSON;
import com.wedu.modules.emp.entity.EmpEntity;
import com.wedu.modules.emp.service.EmpService;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
public class UploadResignListener implements ReadListener<EmpEntity> {
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<EmpEntity> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private EmpEntity empService;


    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param empService
     */
    public UploadResignListener(EmpEntity empService) {
        this.empService= empService;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(ResignEntity data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
//         达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            log.info("准备清理!");
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

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

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        empService.saveBatch(cachedDataList);
        log.info("存储数据库成功!");
    }
}

我使用的是SpringBoot+Mybatis-plus 所以我直接使用了mybatis-plus提供的批量插入方法

 3.5Vue的导入实现

我使用的Element-UI提供的组件

            <el-upload
              action="http://localhost:8080/emp/info/upload"
              :headers="tokenInfo"
              style="display: inline-block"
              :show-file-list="true"
              :before-upload="headUpload"
              :on-success="handleExcelImportSuccess"
              name="file">
              <el-button type="primary" style="margin: 10px " >
                <template #icon>
                  <el-icon><Upload /></el-icon>
                </template>
                导入
             </el-button>
             </el-upload>

action:请求的路径

:headers 我使用了jwt令牌,携带了token往后端进行判断。看各位具体使用,不一定使用

:show-file-list="true" 为true时,则显示上传的文件

:before-upload="headUpload" 顾名思义是上传前的操作

:on-success="handleExcelImportSuccess"  则是成功后的操作

 具体的我们可去Element-UI官网查看更多的使用。接着我们看看上传前需要什么操作

      headUpload(file) {
        const isExcel1 = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
        const isExcel2 = file.type === 'application/vnd.ms-excel';
        const isLt10M = file.size / 1024 / 1024 < 10;
      
        const filexlsx =!(file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        const filexls = !(file.type === 'application/vnd.ms-excel');
        if(filexlsx && filexls) {
            this.$message.error('上传Excel文件只能是xlsx/xls 格式!');
            return false;
        }
        if (!isLt10M) {
            this.$message.error('上传Excel文件大小不能超过 10MB!');
        }
        return (isExcel1 || isExcel2) && isLt10M; 
      }

先前我曾说明excel是有俩种版本,则分别对应了'application/vnd.ms-excel'、'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'

则在此方法中,我进行了上传前的判断,是否为这俩种excel中的其中一种,还有文件的大小是否超过10M

 然后就是上传后的处理,查看响应。由于后端我使用的是统一格式类进行返回。

    handleExcelImportSuccess(response,file){
          if(response.msg == 'success'){
            alert("导入成功")
          }
      }

3.6IDEA中的导出实现

 controller类

    @PostMapping("/down")
    public void create(@RequestBody Long[] userIds, HttpServletResponse response) throws IOException {
 
        List<SysUserEntity> list = empService.getDown(Arrays.asList(userIds));
        String fileName = "数据测试";
        ExcelUtils.createExcel(response,fileName,list,SysUserEntity.class);
    }

我是在前端选择要导出的数据,并只传回对应的id在到后端进行查询,查询在此就不展示了,相信对于各位看官都是轻松的事情。取到数据后,传到我自定义的工具类中。

工具类


import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
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.wedu.config.CustomCellWriteWidthConfig;

import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.VerticalAlignment;

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

public class ExcelUtils {

    public static <T> void createExcel(HttpServletResponse response, String fileName, List<T> list,Class<T> clazz) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
        response.setCharacterEncoding("utf-8");
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+","%20");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName+".xlsx");

        //定义ExcelWriterSheetBuilder
        ExcelWriterSheetBuilder excelWriterSheetBuilder = EasyExcel
                .write(response.getOutputStream())
                .head(clazz)
                .excelType(ExcelTypeEnum.XLSX)
                .registerWriteHandler(new CustomCellWriteWidthConfig())
                .sheet("sheet");
        //设置头样式
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
        WriteFont headWriteFont = new WriteFont();
        //设置表头字体
        headWriteFont.setFontName("宋体");
        //设置设置表头字体大小
        headWriteFont.setFontHeightInPoints((short) 13);
        //设置表头是否加粗
        headWriteFont.setBold(true);
        //填充到表头样式
        headWriteCellStyle.setWriteFont(headWriteFont);
        headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        headWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);



        //设置内容格式
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        WriteFont contentWriteFont = new WriteFont();
        // 设置内容样式
        contentWriteFont.setFontName("宋体");
        contentWriteFont.setFontHeightInPoints((short) 12);
        contentWriteCellStyle.setWriteFont(contentWriteFont);

        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
                new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
        //设计内容居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        //设置内容自动换行
//        contentWriteCellStyle.setWrapped(true);
        excelWriterSheetBuilder.registerWriteHandler(horizontalCellStyleStrategy);

        //调用doWrite方法
        excelWriterSheetBuilder.doWrite(list);
    }

}

无需返回值,在此后端的导出就结束了。

 3.7Vue的导出实现

      down(){
         var userIds = this.dataListSelections.map(item => {
            return item.userId
          })
        this.$http({
            url: this.$http.adornUrl('/emp/info/down'),
            method: 'post',
            responseType: 'blob',
            data: this.$http.adornData(userIds, false)
          }).then(response => {
         // 创建一个 URL 对象,指向返回的二进制数据
          const url = window.URL.createObjectURL(new Blob([response.data]));
          // 创建一个 <a> 元素,设置其属性,模拟点击下载
          const link = document.createElement('a');
          link.href = url;
          link.setAttribute('download', '员工信息.xlsx'); // 设置下载文件的默认名称
          document.body.appendChild(link);
          link.click();
          // 清理创建的 URL 对象
          window.URL.revokeObjectURL(url);
          }).catch(error => {
          console.error('下载失败', error);
          });
      }

首先我们应当遍历我们选中的id。我将选中的id都进行存储到dataListSelections的数组中

在<el-table>标签的属性中@selection-change="selectionChangeHandle"定义选中方法

然后 在return 中 定义 dataListSelections: [] 数组

selectionChangeHandle (val) {

        this.dataListSelections = val

 }

这样就能实现将id存储到数组中

 导出时仍需对响应进行处理,处理的不是后端,因为后端没有返回值。而是处理导出的文件。

到此,导入导出就成功实现了。我不推荐各位只是cv工程,还是可以多看看官网提供的使用方法。若小弟存在错误的地方,各位也可以提出来一起学习!哈哈

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值