1 摘要

Excel 导入导出在实际项目中应用非常广泛,Apache POI 是主要的文档操作工具。EasyExcel 是由阿里开源的,是基于 POI 做的优化封装,专门做 Excel 的导入和导出,使得开发者能够快速上手 Excel 的导入和导出功能。本文将介绍基于 SpringBoot 集成 EasyExcel 实现简易的导入和导出功能。

EasyExcel 官网: https://easyexcel.opensource.alibaba.com

2 核心 Maven 依赖

./demo-easyexcel/pom.xml
        <!-- validate 参数校验 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>${springboot.version}</version>
        </dependency>

        <!-- easyExcel excel 导入导出 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>${easyexcel.version}</version>
        </dependency>

        <!-- hutool 工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>

其中 EasyExcel 的版本如下:

        <easyexcel.version>4.0.1</easyexcel.version>

3 核心业务逻辑

3.1 导入

easyexcel-import-flow

3.2 导出

easyexcel-export-flow

4 核心代码

4.1 导入导出工具类

由于 EasyExcel 封装的非常好,因此导入导出的方法很简洁。

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/common/util/EasyExcelUtil.java
package com.ljq.demo.springboot.easyexcel.common.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;

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

/**
 * @Description: excel 导入导出工具类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
public class EasyExcelUtil {

    private EasyExcelUtil() {
    }

    /**
     * 读取 excel
     *
     * @param inputStream 文件流
     * @param clazz excel 解析参数对象类
     * @param readListener Excel 读取监听器
     * @return
     */
    public static <T> List<T> readExcel(InputStream inputStream, Class<T> clazz, ReadListener<T> readListener) {
        return EasyExcel.read(inputStream, clazz, readListener)
                .sheet()
                .doReadSync();
    }

    /**
     * 导出 excel
     *
     * @param response
     * @param filename
     * @param sheetName
     * @param head
     * @param data
     * @param <T>
     * @throws IOException
     */
    public static <T> void writeExcel(HttpServletResponse response, String filename, String sheetName,
                                 Class<T> head, List<T> data) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        filename = URLEncoder.encode(filename, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + filename);
        // 输出 Excel
        EasyExcel.write(response.getOutputStream(), head)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .sheet(sheetName).doWrite(data);
    }
}

4.2 导入

4.2.1 导入参数接收对象

导入基础参数对象: 所有的导入参数对象都继承该类,基础类中包含一些公共的属性,行数这个字段在导入校验失败的时候,能够准确定位到报错的行数信息

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/model/vo/BaseImportVo.java
package com.ljq.demo.springboot.easyexcel.model.vo;

import lombok.Data;

import java.io.Serializable;

/**
 * @Description: 导入基础参数对象
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Data
public class BaseImportVo implements Serializable {

    private static final long serialVersionUID = -1737973316111453122L;

    /**
     * 行号,从 1 开始
     */
    private Integer rowNo;

}

学生导入参数接收对象

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/model/vo/StudentImportVo.java
package com.ljq.demo.springboot.easyexcel.model.vo;

import cn.hutool.core.date.DatePattern;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.Data;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;

/**
 * @Description: 学生导入对象
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Data
@ExcelIgnoreUnannotated
public class StudentImportVo extends BaseImportVo {

    private static final long serialVersionUID = -1342688939679766826L;

    /**
     * 学生姓名
     */
    @ExcelProperty(index = 0)
    @NotBlank(message = "学生姓名不能为空")
    private String studentName;

    /**
     * 学号
     */
    @ExcelProperty(index = 1)
    @NotBlank(message = "学号不能为空")
    private String studentNo;

    /**
     * 生日
     */
    @ExcelProperty(index = 2)
    @DateTimeFormat(value = DatePattern.NORM_DATE_PATTERN)
    @NotNull(message = "生日不能为空")
    private Date birthday;

    /**
     * 年级
     */
    @ExcelProperty(index = 3)
    @NotNull(message = "年级不能为空")
    @Min(value = 1, message = "年级不能小于1")
    @Max(value = 12, message = "年级不能大于12")
    private Integer grade;

    /**
     * 性别
     */
    @ExcelProperty(index = 4)
    @NotBlank(message = "性别不能为空")
    private String sex;

    /**
     * 是否住校,true:住校,false:非住校
     */
    @ExcelProperty(index = 5)
    private Boolean inSchoolFlag;

    /**
     * 备注
     */
    private String remark;


}

常用注解说明:

@ExcelIgnoreUnannotated: 忽略没有包含 Excel 注解的属性注解,作用在类上,用于忽略没有添加 EasyExcel 注解的字段,无论是导入还是导出,都会忽略。当我们在使用一个参数对象作为导入导出的java对象时,使用这个注解可以过滤不必要的字段。

@ExcelProperty: Excel 导入导出属性注解,作用在字段上,index 为列下表,从 0 开始计算;value 是表头数据,导入的时候可以不填,导出的时候建议填写

@DateTimeFormat: Excel 日期格式注解。eg: yyyy-MM-dd HH:mm:ss

上边可以看到,这个导入参数对象一部分字段还包含参数校验注解,这些注解在后边可以用来校验输入的参数是否合规。

4.2.2 导入监听类

在导入数据时,对整个解析过程进行监听,可以根据需要做响应的业务操作,不写的话会使用系统默认的监听类。

这里给出的是一个公共的监听类,可以对参数进行校验。理论上是每一个导入功能都应该有特定的监听类,这个可以根据业务需要灵活操作。监听类无法使用@Service 等注解来作为组件被 Spring 管理,需要每次手动 new 一个实例。

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/common/listener/CommonImportListener.java
package com.ljq.demo.springboot.easyexcel.common.listener;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.read.listener.ReadListener;
import com.ljq.demo.springboot.easyexcel.common.constant.ImportApiEnum;
import com.ljq.demo.springboot.easyexcel.common.exception.ServiceException;
import com.ljq.demo.springboot.easyexcel.common.util.ExcelHeaderValidateUtil;
import com.ljq.demo.springboot.easyexcel.model.vo.BaseImportVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;

/**
 * @Description: Excel 导入公共监听类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Slf4j
public class CommonImportListener<T> implements ReadListener<T> {


    /**
     * 解析表头
     *
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        log.debug("解析Excel表头");
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(attr)) {
            return;
        }
        HttpServletRequest request = attr.getRequest();
        String uri = request.getRequestURI();
        switch (ImportApiEnum.getByApiAddress(uri)) {
            case IMPORT_STUDENT_API:
                log.debug("解析学生信息表头");
                // 校验表头
                ExcelHeaderValidateUtil.validateImportStudent(headMap);
                break;
            case IMPORT_TEACHER_API:
                log.debug("解析教师信息表头");
                // 校验表头信息
                ExcelHeaderValidateUtil.validateImportTeacher(headMap);
                break;
            default:
                log.info("未知导入信息");
                throw new ServiceException("未知导入信息");
        }
    }

    /**
     * 解析数据
     *
     * @param t
     * @param analysisContext
     */
    @Override
    public void invoke(T t, AnalysisContext analysisContext) {
        // 设置行号
        if (t instanceof BaseImportVo) {
            ((BaseImportVo) t).setRowNo(analysisContext.readRowHolder().getRowIndex() + 1);
        }
    }

    /**
     * 全部解析完成后的操作
     *
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.debug("Excel 解析完成");

    }
}

invokeHead() 方法在解析表头时触发。这里作者添加了一个对导入表头的校验,对于不符合导入模板的Excel,直接抛出异常,不再继续往下解析。

invoke() 方法会在解析到数据时触发,每解析一条数据,就会触发一次。在这里作者为每一条数据添加了行数这一属性,这也是为后边的导入参数校验做铺垫。

doAfterAllAnalysed() 方法会在所有的解析结束后触发。可以根据需要做业务处理。

4.2.3 导入表头参数校验工具类

针对每一次进行导入的方法执行,需要校验表头和导入模板的是否一致,避免用户随意丢文档进行导入,产生脏数据。

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/common/util/ExcelHeaderValidateUtil.java
package com.ljq.demo.springboot.easyexcel.common.util;

import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.ljq.demo.springboot.easyexcel.common.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Objects;

/**
 * @Description: Excel 表头校验工具类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Slf4j
public class ExcelHeaderValidateUtil {

    /**
     * 导入学生表头长度
     */
    private static final int IMPORT_STUDENT_HEADER_SIZE = 6;
    /**
     * 校验导入教师表头长度
     */
    private static final int IMPORT_TEACHER_HEADER_SIZE = 5;

    private ExcelHeaderValidateUtil() {
    }

    /**
     * 校验导入学生表头
     *
     * @param headMap
     */
    public static void validateImportStudent(Map<Integer, ReadCellData<?>> headMap) {
        if (CollUtil.isEmpty(headMap) || headMap.size() < IMPORT_STUDENT_HEADER_SIZE) {
            throw new ServiceException("导入文件与模板不符");
        }
        // 校验表头
        if (Objects.equals("学生姓名", headMap.get(0).getStringValue())
                && Objects.equals("学号",headMap.get(1).getStringValue())
                && Objects.equals("生日",headMap.get(2).getStringValue())
                && Objects.equals("年级",headMap.get(3).getStringValue())
                && Objects.equals("性别",headMap.get(4).getStringValue())
                && Objects.equals("是否住校",headMap.get(5).getStringValue())) {
            return;
        }
        throw new ServiceException("导入文件与模板不符");
    }

    /**
     * 校验导入教师表头
     *
     * @param headMap
     */
    public static void validateImportTeacher(Map<Integer, ReadCellData<?>> headMap) {
        if (CollUtil.isEmpty(headMap) || headMap.size() < IMPORT_TEACHER_HEADER_SIZE) {
            throw new ServiceException("导入文件与模板不符");
        }
        // TODO 校验表头

    }

}

每一个导入都需要对应的一个表头校验方法。

4.2.4 表头参数校验辅助类

这是一个用于辅助表头参数校验的枚举类,将需要导入的接口信息列为枚举,根据导入接口的不同,执行对应的表头校验。

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/common/constant/ImportApiEnum.java
package com.ljq.demo.springboot.easyexcel.common.constant;

import cn.hutool.core.util.StrUtil;
import lombok.Getter;

/**
 * @Description: 导入接口地址枚举类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Getter
public enum ImportApiEnum {

    /**
     * 导入接口枚举
     */
    IMPORT_STUDENT_API("/api/easyexcel/student/import", "学生信息导入"),
    IMPORT_TEACHER_API("/api/easyexcel/teacher/import", "教师信息导入"),

    UNKNOWN_API("", "未知");
    ;

    /**
     * 接口地址
     */
    private final String apiAddress;

    /**
     * 接口描述
     */
    private final String desc;

    ImportApiEnum(String apiAddress, String desc) {
        this.apiAddress = apiAddress;
        this.desc = desc;
    }

    /**
     * 根据接口地址获取枚举
     *
     * @param apiAddress
     * @return
     */
    public static ImportApiEnum getByApiAddress(String apiAddress) {
        if (StrUtil.isBlank(apiAddress)) {
            return UNKNOWN_API;
        }
        for (ImportApiEnum apiEnum : ImportApiEnum.values()) {
            if (apiEnum.getApiAddress().equals(apiAddress) || apiAddress.contains(apiEnum.getApiAddress())) {
                return apiEnum;
            }
        }
        return UNKNOWN_API;
    }
}
4.2.5 导入数据参数校验类

导入的每一行数据都可以进行过参数校验,使用Java 提供的校验注解,和通过 Controller 发起的请求类似,这里手动进行参数校验。

./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/common/util/ValidateUtil.java
package com.ljq.demo.springboot.easyexcel.common.util;

import com.ljq.demo.springboot.easyexcel.model.vo.BaseImportVo;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @Description: 参数校验工具类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
public class ValidateUtil {

    private ValidateUtil() {
    }

    /**
     * 校验
     * @param t
     * @param <T>
     * @return
     */
    public static <T> List<String> valid(T t){
        Validator validatorFactory = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<T>> errors = validatorFactory.validate(t);
        return errors.stream().map(error -> error.getMessage()).collect(Collectors.toList());
    }

    /**
     * 校验 Excel 导入对象
     * @param t
     * @param <T>
     * @return
     */
    public static <T> List<String> validExcel(T t){
        Validator validatorFactory = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<T>> errors = validatorFactory.validate(t);
        return errors.stream().map(error -> String.format("第%s行错误:%s",
                ((BaseImportVo)t).getRowNo(),error.getMessage())).collect(Collectors.toList());
    }

}

参数校验类添加了专门针对 Excel 导入的校验,可以标记出错的行数。

4.2.5 导入业务方法
./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/service/impl/StudentServiceImpl.java
package com.ljq.demo.springboot.easyexcel.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.ljq.demo.springboot.easyexcel.common.exception.ServiceException;
import com.ljq.demo.springboot.easyexcel.common.listener.CommonImportListener;
import com.ljq.demo.springboot.easyexcel.common.util.EasyExcelUtil;
import com.ljq.demo.springboot.easyexcel.common.util.ValidateUtil;
import com.ljq.demo.springboot.easyexcel.model.request.StudentExportRequest;
import com.ljq.demo.springboot.easyexcel.model.response.CommonImportResponse;
import com.ljq.demo.springboot.easyexcel.model.vo.StudentExportVo;
import com.ljq.demo.springboot.easyexcel.model.vo.StudentImportVo;
import com.ljq.demo.springboot.easyexcel.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.*;

/**
 * @Description: 学生业务接口实现类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Slf4j
@Service
public class StudentServiceImpl implements StudentService {


    /**
     * 导入学生信息
     *
     * @param file
     * @return
     */
    @Override
    public CommonImportResponse importStudent(MultipartFile file) {
        CommonImportResponse response = new CommonImportResponse();
        List<String> failReasonList = new ArrayList<>();
        try {
            // 解析 Excel
            List<StudentImportVo> studentList = EasyExcelUtil.readExcel(file.getInputStream(), StudentImportVo.class,
                    new CommonImportListener<>());
            // 判断是否解析到有效数据
            if (CollUtil.isEmpty(studentList)) {
                response.setFailReasonList(Collections.singletonList("导入失败,Excel 文档数据为空"));
                return response;
            }
            // 基本参数校验
            studentList.forEach(student -> failReasonList.addAll(ValidateUtil.validExcel(student)));
            if (CollUtil.isNotEmpty(failReasonList)) {
                response.setFailReasonList(failReasonList);
                return response;
            }
            // TODO 业务参数校验
            studentList.forEach(student -> {
                log.info("业务参数校验学生信息: {}", student);

            });

            // 校验通过的对象数
            response.setSuccessCount(studentList.size());

            // TODO 保存到数据库


        } catch (Exception e) {
            log.error("Excel 解析失败",e);
            // 判断是否为业务异常
            if (e instanceof ServiceException) {
                response.setFailReasonList(Collections.singletonList(e.getMessage()));
                return response;
            }
            response.setFailReasonList(Collections.singletonList("导入失败,无法解析 Excel 文档"));
        }
        return response;
    }


}

这里作为演示,就忽略了业务校验和保存到数据库的操作。

4.3 导出

导出的业务逻辑相对简单,根据查询结果,然后写入 Excel 即可

4.3.1 导出参数对象
./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/model/vo/StudentExportVo.java
package com.ljq.demo.springboot.easyexcel.model.vo;

import cn.hutool.core.date.DatePattern;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.Data;

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

/**
 * @Description: 学生导出对象
 * @Author: junqiang.lu
 * @Date: 2024/6/21
 */
@Data
@ExcelIgnoreUnannotated
public class StudentExportVo implements Serializable {

    private static final long serialVersionUID = 414773537047578760L;

    /**
     * 学生姓名
     */
    @ExcelProperty(index = 0, value = "学生姓名")
    private String studentName;

    /**
     * 学号
     */
    @ExcelProperty(index = 1, value = "学号")
    private String studentNo;

    /**
     * 生日
     */
    @ExcelProperty(index = 2, value = "生日")
    @DateTimeFormat(value = DatePattern.NORM_DATE_PATTERN)
    private Date birthday;

    /**
     * 年级
     */
    @ExcelProperty(index = 3, value = "年级")
    private Integer grade;

    /**
     * 性别
     */
    @ExcelProperty(index = 4, value = "性别")
    private String sex;

    /**
     * 是否住校,true:住校,false:非住校
     */
    @ExcelProperty(index = 5, value = "是否住校")
    private Boolean inSchoolFlag;

    /**
     * 备注
     */
    private String remark;


}

导出对象与导入对象的主要差别在于导出对象的属性上边需要 @ExcelPropertity 注解的 value 属性,用于指定导出表格的表头信息。

4.3.2 导出业务方法
./demo-easyexcel/src/main/java/com/ljq/demo/springboot/easyexcel/service/impl/StudentServiceImpl.java
package com.ljq.demo.springboot.easyexcel.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.ljq.demo.springboot.easyexcel.common.exception.ServiceException;
import com.ljq.demo.springboot.easyexcel.common.listener.CommonImportListener;
import com.ljq.demo.springboot.easyexcel.common.util.EasyExcelUtil;
import com.ljq.demo.springboot.easyexcel.common.util.ValidateUtil;
import com.ljq.demo.springboot.easyexcel.model.request.StudentExportRequest;
import com.ljq.demo.springboot.easyexcel.model.response.CommonImportResponse;
import com.ljq.demo.springboot.easyexcel.model.vo.StudentExportVo;
import com.ljq.demo.springboot.easyexcel.model.vo.StudentImportVo;
import com.ljq.demo.springboot.easyexcel.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.*;

/**
 * @Description: 学生业务接口实现类
 * @Author: junqiang.lu
 * @Date: 2024/6/20
 */
@Slf4j
@Service
public class StudentServiceImpl implements StudentService {

    /**
     * 导出学生信息
     *
     * @param request
     * @return
     */
    @Override
    public List<StudentExportVo> exportStudent(StudentExportRequest request) {
        log.info("导出学生信息: {}", request);

        // TODO 数据库查询

        List<StudentExportVo> studentList = new ArrayList<>();
        if (request.getCount() > 0) {
            for (int i = 0; i < request.getCount(); i++) {
                StudentExportVo student = new StudentExportVo();
                student.setStudentName("学生" + i);
                student.setStudentNo("S" + i);
                student.setBirthday(new Date());
                student.setGrade(RandomUtil.randomInt(1,13));
                student.setSex(RandomUtil.randomEle(Arrays.asList("男", "女")));
                student.setInSchoolFlag(RandomUtil.randomBoolean());
                student.setRemark("备注" + i);
                studentList.add(student);
            }
        }
        return studentList;
    }


}

这里主要为了演示,使用循环模拟的数据,正常项目中是查询数据库中的数据。

5 测试结果

5.1 导入模板

import_student_template.xlsx

5.2 测试用例明细

5.2.1 错误-模板不符合

测试数据截图:

easyexcel-demo-5-2-1-bed-header

返回结果:

{
    "successCount": null,
    "failReasonList": [
        "导入文件与模板不符"
    ]
}
5.2.2 错误-参数不合规

测试数据截图:

easyexcel-demo-5-2-2-bed-param

返回结果:

{
    "successCount": null,
    "failReasonList": [
        "第2行错误:学号不能为空",
        "第4行错误:生日不能为空",
        "第4行错误:性别不能为空",
        "第4行错误:年级不能为空"
    ]
}
5.2.3 错误-空表格

测试数据截图:

easyexcel-demo-5-2-3-null-record

返回结果:

{
    "successCount": null,
    "failReasonList": [
        "导入失败,Excel 文档数据为空"
    ]
}
5.2.4 正确-合规数据

测试数据截图:

easyexcel-demo-5-2-4-right-param

返回结果:

{
    "successCount": 3,
    "failReasonList": null
}

6 推荐参考资料

EasyExcel 官方教程

EasyExcel 官方注解说明

springboot使用EasyExcel导入数据(获取行号)

【原】记一次EasyExcel读取xlsx文件实体bean数据全部为null的问题

7 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo/tree/master/demo-easyexcel

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

Logo

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

更多推荐