SpringBoot 集成 easyexcel 实现导入导出功能
Excel 导入导出在实际项目中应用非常广泛,Apache POI 是主要的文档操作工具。EasyExcel 是由阿里开源的,是基于 POI 做的优化封装,专门做 Excel 的导入和导出,使得开发者能够快速上手 Excel 的导入和导出功能。本文将介绍基于 SpringBoot 集成 EasyExcel 实现简易的导入和导出功能。
文章目录
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 导入
3.2 导出
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 导入模板
5.2 测试用例明细
5.2.1 错误-模板不符合
测试数据截图:
返回结果:
{
"successCount": null,
"failReasonList": [
"导入文件与模板不符"
]
}
5.2.2 错误-参数不合规
测试数据截图:
返回结果:
{
"successCount": null,
"failReasonList": [
"第2行错误:学号不能为空",
"第4行错误:生日不能为空",
"第4行错误:性别不能为空",
"第4行错误:年级不能为空"
]
}
5.2.3 错误-空表格
测试数据截图:
返回结果:
{
"successCount": null,
"failReasonList": [
"导入失败,Excel 文档数据为空"
]
}
5.2.4 正确-合规数据
测试数据截图:
返回结果:
{
"successCount": 3,
"failReasonList": null
}
6 推荐参考资料
springboot使用EasyExcel导入数据(获取行号)
【原】记一次EasyExcel读取xlsx文件实体bean数据全部为null的问题
7 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo/tree/master/demo-easyexcel
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)