一、前言
在一个后台管理系统中,对excel的操作在项目中的使用是难免的,之前为了解决带参数校验的excel导入导出我整合了一个小demo,比较简单可能现实代码也不是很完善,比如以下问题没有得到很好的解决
1.参数校验类型比较单一且不能进行组合校验。
2.实现ReadListener 在监听读取数据的时候同时进行校验代码比较复杂,也会影响本来的读取效率。
3.最终的检验结果获取不太优雅,结果就是一个拼接的字符串也不太符合面向对象的思想。
4.没有完整的仓库案例导致读者不能拿到完整的代码,和留言问题没有得到回复。本次将会在gitee上上传完整代码。
二、优化思路
针对上面的一些问题进行了本次的改造,同样使用的是阿里巴巴的开源框架easy-excel结合自定义注解实现,
本次采用了分布和结果封装,将读取数据和数据校验分开了,而不是在读取数据的同时进行数据校验,将校验结果用CheckResult来进行封装。实现了定位更加精确以至于某行,某个字段,错误原因,和错误信息都进行了记录。
三、结构说明
如图:

1.annotaion :
自定义注解配合属性实现检验规则的配置。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotNull {
String name() default ""; //字段名称
String message() default CheckResult.MSG_REQUIRE;//错误信息
String reason() default CheckResult.REASON_REQUIRE; // 错误原因
String cfName() default ""; // 条件必填的字段名称
String cfValue() default ""; // 条件必填的值,多个英文逗号隔开
boolean regex() default false;// 是否进行格式判断;
String regexValue() default CheckResult.REGEX_DEFAULT;// 格式正则表达式;
}
2.ExcelUtil:
提供了导入导出方法和一些配套实现的方法。
package com.mr.yushao.exceldemo.utils;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.*;
/**
* excel 导入导出工具类
*/
@Slf4j
public class ExcelUtil {
public static final String MEAT_DATA_LINE_NUMBER = "line_number";
public static final String MEAT_DATA_LINE_NUMBER_CAM = "lineNumber";
/**
* 按模板导出excel
*
* @param resp 响应流
* @param template 模板输入流
* @param filename 导出文件名称
* @param dataList 数据
* @param <T> 数据泛型
* @throws IOException IO异常
*/
public static <T> void exportWithTemplate(HttpServletResponse resp, InputStream template, String filename, List<T> dataList) throws IOException {
resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
resp.setCharacterEncoding("utf-8");
resp.setHeader("Content-disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
// 写入模板
ServletOutputStream outputStream = resp.getOutputStream();
ExcelWriter writer = EasyExcel.write(outputStream).withTemplate(template).build();
Map<String, List<T>> dataMap = new HashMap<>();
dataMap.put("data", dataList);
dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(0).build()));
writer.finish();
outputStream.close();
}
/**
* 按模板导出生成inputStream
*
* @param template 模板输入流
* @param dataList 数据
* @param <T> 数据泛型
* @throws IOException IO异常
*/
public static <T> InputStream exportWithTemplate(InputStream template, Collection<T> dataList) throws IOException {
// 写入模板
ByteArrayOutputStream os = new ByteArrayOutputStream();
ExcelWriter writer = EasyExcel.write(os).withTemplate(template).build();
Map<String, Collection<T>> dataMap = new HashMap<>();
dataMap.put("data", dataList);
dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(0).build()));
writer.finish();
byte[] buffer = os.toByteArray();
return new ByteArrayInputStream(buffer);
}
/**
* 按模板导出生成inputStream
*
* @param template 模板输入流
* @param dataList 数据
* @param <T> 数据泛型
* @param sheetNo 指定sheet
* @throws IOException IO异常
*/
public static <T> InputStream exportWithTemplate(InputStream template, Collection<T> dataList, Integer sheetNo) throws IOException {
// 写入模板
ByteArrayOutputStream os = new ByteArrayOutputStream();
ExcelWriter writer = EasyExcel.write(os).withTemplate(template).build();
Map<String, Collection<T>> dataMap = new HashMap<>();
dataMap.put("data", dataList);
dataMap.forEach((k, v) -> writer.fill(new FillWrapper(k, v), new FillConfig(), EasyExcel.writerSheet(sheetNo).build()));
writer.finish();
byte[] buffer = os.toByteArray();
return new ByteArrayInputStream(buffer);
}
/**
* 校验读取当前行的数据是否全部为空或者空串
*
* @param dto 当前行数据
* @param <T> 泛型
* @return 全部为空返回true反之false
*/
public static <T> Boolean checkAllNull(T dto) {
boolean flag = true;
Map<String, Object> tmp = JSON.parseObject(JSON.toJSONString(dto));
for (String s : tmp.keySet()) {
if (MEAT_DATA_LINE_NUMBER.equals(s)) {
continue;
}
if (StringUtils.hasText(Objects.nonNull(tmp.get(s)) ? tmp.get(s).toString() : "")) {
flag = false;
break;
}
}
return flag;
}
/**
* 读取Excel表格返回数据集合,默认读取第一个sheet表数据(不需要校验)
*
* @param inputStream 输入流
* @param clazz 数据映射类
* @param headRowNumber 标题占用的行数
* @return List结果集
*/
public static <T> List<T> readExcel(InputStream inputStream, Class<T> clazz, Integer headRowNumber) {
if (inputStream == null || clazz == null) {
return null;
}
ExcelListener<T> excelListener = new ExcelListener<>();
ExcelReaderBuilder read = EasyExcel.read(inputStream, clazz, excelListener);
read.sheet().headRowNumber(headRowNumber).doRead();
return excelListener.getDataList();
}
/**
* 读取Excel表格返回数据集合,支持校验数据格式
*
* @param is 输入流
* @param clazz excel每一行对应的实体类
* @param startRow 数据开始行号,第一行序号为0
* @param <T> 泛型
* @return 数据集合
*/
public static <T> List<T> readExcelForCheck(InputStream is, Class<T> clazz, Integer startRow) {
long start = System.currentTimeMillis();
List<T> list = new ArrayList<>();
EasyExcel.read(is, clazz, new ReadListener<T>() {
@Override
public void invoke(T t, AnalysisContext analysisContext) {
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
try {
Field declaredField = t.getClass().getDeclaredField(MEAT_DATA_LINE_NUMBER_CAM);
declaredField.setAccessible(true);
declaredField.set(t, rowIndex + 1);
} catch (NoSuchFieldException e) {
throw new RuntimeException("实体类不包含行号");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
if (!checkAllNull(t)) {
list.add(t);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
long end = System.currentTimeMillis();
log.info("excel读取完毕,共计{}条数据,耗时:{}", list.size(), end - start);
}
}).sheet().headRowNumber(startRow).doRead();
return list;
}
/**
* 读取文件解析监听类
*
* @param <T>
*/
public static class ExcelListener<T> extends AnalysisEventListener<T> {
/**
* 存放读取后的数据
*/
public List<T> dataList = new ArrayList<>();
/**
* 逐条读取数据
*/
@Override
public void invoke(T t, AnalysisContext analysisContext) {
dataList.add(t);
}
/**
* 解析读取数据总条数
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("读取数据条数:{}条!", dataList.size());
}
public List<T> getDataList() {
return this.dataList;
}
}
/**
* 模板准确性检查
*
* @param file excel 文件
* @param templateName 正确返回true,错误返回false
* @return
*/
public static Boolean templateCheck(MultipartFile file, String templateName) {
Workbook workbook;
String filename = file.getOriginalFilename();
try {
InputStream fis = file.getInputStream();
if (filename.endsWith(".xlsx")) {
workbook = new XSSFWorkbook(fis);
} else if (filename.endsWith(".xls")) {
workbook = new HSSFWorkbook(fis);
} else {
throw new IllegalArgumentException("不支持的文件类型");
}
} catch (IOException e) {
log.debug(e.getMessage());
return false;
}
return templateName.equals(workbook.getSheetName(0));
}
}
四、使用方法
1.导入excel并校验数据
step1:ExcelUtil读取excel数据—>readExcelForCheck()
step2:CheckResult执行校验数据的方法—>CheckResult.checkResultList()
准备待导入的excel数据如下:

根据表格的内容我们定义用于接收excel数据的实体类User:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
/**
* 行号,提示错误信息时展示行号
*/
@ExcelIgnore
private Integer lineNumber;
@NotNull(name = "姓名")
private String name;
@NotNull(name = "年龄")
private Integer age;
@NotNull(name = "性别")
private String sex;
@NotNull(name = "邮箱", regex = true, regexValue = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$\n")
private String email;
@NotNull(name = "地址", cfName = "sex", cfValue = "女", message = "性别为女,地址必填", regex = true)
private String address;
}
这时候只需要在要校验的字段上加@NotNull注解就可以了,
例子的规则:姓名—必填,年龄–必填,性别–必填,邮箱–必填、格式为邮箱,地址–当性别为‘女’的时候必填。
tips1:lineNumber只是为了标注excel的行号,对于实体类无实际意义,需要加@ExcelIgnore来忽略。
tips2: 当cfValue有多个值的时候有英文逗号隔开,例如“男,女”。
执行结果如下:

当然前端可以ui做出表格弹出更加美观!!!
controller代码如下:
package com.mr.yushao.exceldemo.controller;
import com.mr.yushao.exceldemo.utils.ExcelUtil;
import com.mr.yushao.exceldemo.vo.CheckResult;
import com.mr.yushao.exceldemo.vo.R;
import com.mr.yushao.exceldemo.vo.User;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/v1")
public class TestController {
@PostMapping("read")
public R readExcelAndCheck(MultipartFile file) {
try {
List<User> users = ExcelUtil.readExcelForCheck(file.getInputStream(), User.class, 1);
//检查格式
List<CheckResult> checkResults = CheckResult.checkResultList(users);
if (!CollectionUtils.isEmpty(checkResults)) {
return R.failer(checkResults);
}
return R.success();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@GetMapping("down")
public void export(HttpServletResponse response) {
try {
List<User> users = new ArrayList<>();
users.add(new User(1, "张三", 20, "男", "11@qq.com", "xxxxx"));
users.add(new User(2, "李四", 18, "男", "22@qq.com", "xxxxx"));
InputStream inputStream = this.getClass().getResourceAsStream("/static/exceltemplates/users.xlsx");
ExcelUtil.exportWithTemplate(response, inputStream, "users", users);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
2.导出数据
本次的导出需要准备一个模板,例如/static\exceltemplates\users.xlsx。虽然比起直接导出多了一个模板但是也有好处,当你模板比较复杂的时候如合并表头、提前准备的文字描述等都可以直接在模板中设定好,然后通过数据绑定来是实体集合按照对于的绑定关系导出到excel。
如何指定实体类和excel模板的导出关系?
只需要在模板中加上{data.字段名称}。{}为框架规定的标识符,data是自己设定的前缀,后面紧跟实体类的字段就完成了绑定。User实体与excel模板的绑定如下:

执行结果如下:

五、总结
本次的小案例可以直接做出jar包使用,就完成了一个待参数校验的导入导出工具包,当然校验规则可以在注解里面去丰富完善,相比上版的案例还是简单了不少哦。后续可能还会有其他方案更新!
代码仓库地址(click)
有问题留言大家一起交流学习。
932

被折叠的 条评论
为什么被折叠?



