【EasyExcel详细步骤】(内附源码)

页面预览

数据导出

image-20230211132347549

数据导入

image-20230211132320823

第01章-Alibaba EasyExcel

1、EasyExcel介绍

1.1、EasyExcel的作用

  • 数据导入:减轻录入工作量
  • 数据导出:统计信息归档
  • 数据传输:异构系统之间数据传输

1.2、EasyExcel的特点

  • 快速:快速的读取excel中的数据。

  • 简洁:映射excel和实体类,让代码变的更加简洁。

  • 大文件:在读写大文件的时候使用磁盘做缓存,更加的节约内存。

2、快速开始

参考文档地址:https://easyexcel.opensource.alibaba.com/index.html

service-cmn模块添加依赖

<!--easyexcel-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
</dependency>

2.1、最简单的写

在测试目录下创建以下文件

实体类:

package com.atguigu.syt.cmn.easyexcel;

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {

    @ExcelProperty("字符串标题")
    private String string;

    @ExcelProperty("日期标题")
    private Date date;

    @ExcelProperty("数字标题")
    private Double doubleData;
    
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String ignore;
}

测试用例:

package com.atguigu.syt.cmn.easyexcel;

public class ExcelWriteTest {

    /**
     * 最简单的写
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void simpleWrite07() {
        // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
        // 写法2
        String fileName = "d:/simpleWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 如果这里想使用03 则 传入excelType参数即可
        EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
    }

    @Test
    public void simpleWrite03() {
        String fileName = "d:/simpleWrite" + System.currentTimeMillis() + ".xls";
        EasyExcel.write(fileName, DemoData.class).excelType(ExcelTypeEnum.XLS).sheet("模板").doWrite(data());
    }

    private List<DemoData> data() {
        List<DemoData> list = ListUtils.newArrayList();

        //算上标题,最多可写65536行(.xls)
        //算上标题,最多可写1048576行(.xlsx)
        //java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65535)
        for (int i = 0; i < 10; i++) {
            DemoData data = new DemoData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56);
            list.add(data);
        }
        return list;
    }
}

2.2、最简单的读

在测试目录下创建以下文件

监听器:

package com.atguigu.syt.cmn.easyexcel;

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {

    /**
     * 每隔3条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 3;

    /**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

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

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

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

测试用例:

package com.atguigu.syt.cmn.easyexcel;

public class ExcelReadTest {

    /**
     * 最简单的读
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
     * <p>
     * 3. 直接读即可
     */
    @Test
    public void simpleRead() {

        // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
        // 写法3:
        String fileName = "d:/demo.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();

    }
}

第02章-数据导出

1、后端接口

参考文档:写Excel | Easy Excel (alibaba.com)

1.1、Controller

AdminRegionController

/**
     * 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
     *
     * @since 2.1.1
     */
@ApiOperation(value="导出")
@GetMapping(value = "/exportData")
public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {

    // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
    try {

        //int i = 9/0;
        List<RegionExcelVo> regionExcelVoList = regionService.findRegionExcelVoList();

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
        String fileName = URLEncoder.encode("数据字典", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        // 这里需要设置不关闭流
        EasyExcel.write(response.getOutputStream(), RegionExcelVo.class)
            .sheet("数据字典")
            .doWrite(regionExcelVoList);

    } catch (Exception e) {
        log.error(ExceptionUtils.getStackTrace(e));
        // 重置response
        response.reset();
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println("导出失败");
    }
}

1.2、Service

接口:RegionService

/**
     * 获取地区VO列表
     * @return
     */
List<RegionExcelVo> findRegionExcelVoList();

实现:RegionServiceImpl

@Override
public List<RegionExcelVo> findRegionExcelVoList() {

    List<Region> regionList = this.list();
    List<RegionExcelVo> regionExcelVoList = ListUtils.newArrayListWithCapacity(regionList.size());
    for(Region region : regionList) {
        RegionExcelVo regionExcelVo = new RegionExcelVo();
        BeanUtils.copyProperties(region, regionExcelVo);
        regionExcelVoList.add(regionExcelVo);
    }

    return regionExcelVoList;
}

2、前端页面

在cmn/region/list.vue文件中添加如下代码

2.1、html

在表格上方添加如下代码

<div style="margin-bottom:5px;">
    <el-button type="success" icon="el-icon-top" size="mini" @click="exportData"><i class="fa fa-plus" />导出</el-button>
</div>

2.2、脚本

在脚本中添加如下methods

引入模块:

import store from '@/store'
import axios from 'axios'

定义脚本:

   exportData() {
      //此种方式无法传递token
      //window.open(process.env.VUE_APP_BASE_API + '/admin/cmn/region/exportData')
       
      //使用当前方式传递token
      const config = {
          method: 'get',
          url: '/dev-api/admin/cmn/region/exportData',
          headers: {
            'token': store.getters.token //
          },
          responseType: 'blob'
        }
      axios(config).then(response => {
          const url = window.URL.createObjectURL(new Blob([response.data]));
          const link = document.createElement('a')
          link.href = url
          link.setAttribute('download', '数据字典.xlsx')
          document.body.appendChild(link)
          link.click()
      })
    },

第03章-数据导入

1、后端接口

1.1、Controller

AdminRegionController

@ApiOperation(value = "导入")
@ApiImplicitParam(name = "file", value = "文件", required = true)
@PostMapping("/importData")
public Result importData(MultipartFile file) {
    regionService.importData(file);
    return Result.ok();
}

1.2、Service

接口:RegionService

/**
     * 地区数据导入Excel
     * @param file
     */
void importData(MultipartFile file);

实现:RegionServiceImpl

@Override
public void importData(MultipartFile file) {

    try {
        long b = System.currentTimeMillis();
        EasyExcel.read(file.getInputStream(),RegionExcelVo.class, new RegionExcelListener(baseMapper)).sheet().doRead();
        long e = System.currentTimeMillis();

        log.info("用时:" +  (e - b) + "ms" );

    } catch (IOException e) {
        throw new GuiguException(ResultCodeEnum.FAIL, e);
    }
}

1.3、监听器

RegionExcelListener

package com.atguigu.syt.cmn.listener;

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class RegionExcelListener implements ReadListener<RegionExcelVo> {

    /**
     * 每隔3条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;

    /**
     * 缓存的数据
     */
    private List<RegionExcelVo> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private RegionMapper regionMapper;

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

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

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

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

1.4、Mapper

接口:RegionMapper

/**
     * 根据RegionExcelVo列表批量保存数据
     * @param cachedDataList
     */
void saveBatch(@Param("list") List<RegionExcelVo> cachedDataList);

实现:RegionMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.syt.cmn.mapper.RegionMapper">

    <insert id="saveBatch">
        INSERT INTO region ( id, code, parent_code, name, level )
        VALUES
        <foreach collection="list" item="region" separator=",">
            (
            #{region.id},
            #{region.code},
            #{region.parentCode},
            #{region.name},
            #{region.level}
            )
        </foreach>
    </insert>
</mapper>

1.5、service的pom.xml配置

程序发布时,默认情况下java目录下的xml文件不会被发布,需要进行以下配置

<build>
    <!-- 项目打包时会将java目录中的*.xml文件也进行打包 -->
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.yml</include>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

2、前端页面

在cmn/region/list.vue文件中添加如下代码

2.1、添加导入按钮

<el-button type="primary" icon="el-icon-bottom" size="mini" @click="importData"><i class="fa fa-plus" />导入</el-button>

2.2、添加导入弹出层

<el-dialog title="导入" :visible.sync="dialogImportVisible" width="480px">
    <el-form label-position="right" label-width="100px">
        <el-form-item label="请选择文件">
            <!-- 注意:这里使用headers属性传递了token -->
            <el-upload
                       v-loading="uploadLoading"
                       element-loading-text="数据导入中"
                       :headers="{ token }"
                       :multiple="false"
                       :on-progress="onUploadProgress"
                       :on-success="onUploadSuccess"
                       :on-error="onUploadError"
                       :action="VUE_APP_BASE_API + '/admin/cmn/region/importData'">
                <el-button size="small" type="primary">点击上传</el-button>
                <div slot="tip" class="el-upload__tip">
                    只能上传xls文件,且不超过1M
                </div>
            </el-upload>
        </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
        <el-button @click="dialogImportVisible = false">取消</el-button>
    </div>
</el-dialog>

2.3、添加导入弹出层属性

data:

dialogImportVisible: false,
uploadLoading: false, //文件上传状态
VUE_APP_BASE_API: process.env.VUE_APP_BASE_API, //接口地址
token: store.getters.token,

2.4、添加导入方法

methods:

    //显示一个文件选择对话框
    importData() {
      this.dialogImportVisible = true
    },
        
    //文件上传中
    onUploadProgress(){
      this.uploadLoading = true
    },        
        
    onUploadSuccess(response) {
        
      this.uploadLoading = false
        
      //文件导入成功
      if (response.code === 200) {
        this.$message.success(response.message)
        this.dialogImportVisible = false
        this.getDictList(1)
      } else {
        //导入失败
        this.$message.error(response.message)
        this.dialogImportVisible = false
      }
    },
        
    //导入失败
    onUploadError() {
      this.uploadLoading = false  
      this.$message.error('系统错误')
      this.dialogImportVisible = false
    },

源码:https://gitee.com/dengyaojava/guigu-syt-parent

posted @ 2023-06-12 19:16  自律即自由-  阅读(88)  评论(0编辑  收藏  举报