EasyExcel生成带下拉列表或二级级联列表的Excel模版+自定义校验导入数据(附仓库)


目录

  • EasyExcel生成带下拉列表或二级级联列表的Excel模版+自定义校验导入数据(附仓库)
    • 仓库
      • 1. 效果
      • 2. easyexcel
    • 前言
    • 一、依赖配置
    • 二、自定义注解相关
      • 1. @ExcelSelected 自定义导出下拉菜单注解
      • 2. 动态获取下拉列表列表数据的接口
    • 二、模版相关对象
      • 1. Excel模版对象
      • 2. 列的预处理对象
    • 三、导出
      • 1.数据预处理方法
      • 2 .SheetWriteHandler 自定义导出处理器
        • 2.1 ExcelUtil
        • 2.2 INDIREC函数
      • 3.导出模版v1
      • 4. 导出模版v2
      • 5.效果
    • 四、导入
      • 1. 导入接口
        • 1.1 v1
        • 1.2 v2
      • 2.自定义导入监听器
      • 3. 校验数据
        • 3.1 自定义校验注解
        • 3.2 自定义校验器
        • 3.3 动态获取数据处理器
        • 3.4 校验数据方法
        • 3.5 数据实体
      • 4. 校验结果
    • 结束

仓库

https://github.com/hp-coder/common-starters.git excel-common-spring-boot-starter
主要对easyexcel的使用aop简单封装, 满足当前项目需求基于行合并的操作, 其他导出合并逻辑加起来也不麻烦, 可以参考easyexcel-core里的LoopMergeStrategy处理器的写法来自定义

1. 效果

 //可以适用于存在一行有多行子记录形成合并行的情况//通过aop和注解配置添加自定义handler或增强逻辑等@PostMapping("/import")public R importV2(@RequestExcel(listener = MergeRowAnalysisEventListener.class,enhancement = {MergeRowReaderEnhance.class})//通过aop将处理的数据封装到参数中Map<Integer, List<ImportDTO>> map) {xxService.validateImport(map);return xxService.import(map);}//使用@ExcelMerge实现根据单元格数据的是否相同来合并行//通过aop根据返回类型封装处理的数据,通过注解配置添加自定义handler或增强逻辑等@ResponseExcel(name = "数据导出",sheets = {@Sheet(sheetName = "测试sheet")},enhancement = {MergeRowWriterEnhance.class})@GetMapping("/export")public List<ExportV2DTO> exportV2(ParamDTO dto) {return xxService.prepareExportData(dto);}

2. easyexcel

https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write

前言

项目需要一个导入数据的功能, 该数据对象在设计之初, 其中不少属性都是枚举值, 在UI上可以通过Select框选择, 那么对于Excel来说也是有类似功能可以实现达到限制用户提交参数的目的, 虽然接口里也会校验数据的准确性, 但是在一开始就能告知用户可选项是那些是一个更好的体验.

需要提前说明的是, 对于级联等复杂操作都需要对excel有一定的了解, 例如实际上在Win Excel的里操作, 概念, 函数等, 由于太懒, 自己去了解也是点到为止, 只实现了二级级联, 理论上更多级其实也是创建名称管理器,INDIRECT函数关联值, 这里就不再赘述了.

注意: select 的 value === label , label是中文属性值, 这是比较简单的方式, 如果要转整形存库里, 可以在代码里转, jpa有converter, mbp有typehandler应该都可以处理这种场景

以下的代码在仓库中基本都有, 只是部分类定义有所调整, 逻辑还是相同的
enough with all the chit-chat


一、依赖配置

推荐使用pig4cloud的excel-starter, 看源码是基于easyexcel, 封装的比较好, 我也是通过类似的逻辑编写了我自己的starter模块

本例使用maven, gradle以后学会了再加上

<properties><pig4cloud.version>1.2.4</pig4cloud.version>
</properties><dependency><groupId>com.pig4cloud.excel</groupId><artifactId>excel-spring-boot-starter</artifactId><version>${pig4cloud.version}</version>
</dependency>

二、自定义注解相关

EasyExcel提供了很多处理器接口, 写sheet, row, cell等;
对于下拉列表数据就可以通过自定义注解获取select列表数据,级联等, 再通过自定义的sheetWriteHandler处理数据, 增加约束等excel配置, 然后输出excel文件

1. @ExcelSelected 自定义导出下拉菜单注解

这里想如果要优化可以把EasyExcel的@ExcelProperty给包进来

/*** 添加下拉列表注解* <p>* 支持静态,动态字符串数据源** 以及二级级联下拉列表数据源*** @author HP* @date 2022/11/7*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Inherited
public @interface ExcelSelect {/*** 静态数据 只支持 字符串数组,如果要使用级联,请使用处理器获取数据*** @return*/String[] staticData() default {};/*** 关联父列名称*** @return*/String parentColumn() default "";/*** 动态数据*** @return*/Class<? extends ColumnDynamicSelectDataHandler> handler() default DefaultColumnDynamicSelectDataHandler.class;/*** 主要是提供一些简单参数*** @return*/String parameter() default "";/*** 设置下拉框的起始行,默认为第二行*/int firstRow() default 1;/*** 设置下拉框的结束行,默认为最后一行*/int lastRow() default 0x10000;
}

2. 动态获取下拉列表列表数据的接口

为了适配级联数据为Map<ParentValue,List>的情况, 这里最终抽象为一个function, 实现后获取数据返回即可, 不赘述

import java.util.function.Function;/*** @author HP* @date 2022/11/7*/
public interface ColumnDynamicSelectDataHandler<T, R> {Function<T, R> source();
}

二、模版相关对象

1. Excel模版对象

这里只列出有代表性的字段, 主要分为没有下拉选项的, 有下拉选项(固定值), 有下拉选项(动态获取), 有下拉选项(需要关联级联, 动态获取)

@Data
public class xxxxImportTemplateDTO implements Serializable {@ExcelProperty(value = "普通列")private String common;@ExcelSelect(staticData = "[\"A\", \"B\", \"C\", \"D\", \"E\"]")@ExcelProperty(value = "单列select")private String singleSelect;@ExcelSelect(handler = ExcelTestSourceHandler.class)@ExcelProperty(value = "动态单列select")private String dynamicSingleSelect;//parent字段定义父列名称@ExcelSelect(parentColumn = "父列", handler = ExcelChildSourceHandler.class)@ExcelProperty(value = "级联子列")private String child;@ExcelSelect(handler = ExcelParentSourceHandler.class)@ExcelProperty(value = "父列")private String parent;private static final long serialVersionUID = 1L;
}

2. 列的预处理对象

主要作用保存对模版中每列解析后获取的索引,父类,下拉列表等信息, 利用这些信息在easyexcel的SheetWriteHandler中生成约束, 名称, 下拉菜单等配置

@Data
public class ExcelSelectDataColumn<T> {private T source;private String column;private int columnIndex;private String parentColumn;private int parentColumnIndex;private int firstRow;private int lastRow;public T resolveSource(ExcelSelect excelSelect) {if (excelSelect == null) {return null;}// 获取固定下拉框的内容final String[] staticData = excelSelect.staticData();if (ArrayUtil.isNotEmpty(staticData)) {return (T) Arrays.asList(staticData);}// 获取动态下拉框的内容final Class<? extends ColumnDynamicSelectDataHandler> handlerClass = excelSelect.handler();if (Objects.nonNull(handlerClass)) {final ColumnDynamicSelectDataHandler handler = SpringUtil.getBean(handlerClass);return (T) handler.source().apply(StrUtil.isNotEmpty(excelSelect.parameter()) ? excelSelect.parameter() : null);}return null;}
}

三、导出

1.数据预处理方法

  private static <T> Map<Integer, ExcelSelectDataColumn> resolveExcelSelect(Class<T> dataClass) {Map<Integer, ExcelSelectDataColumn> selectedMap = new HashMap<>(16);Field[] fields = dataClass.getDeclaredFields();for (int i = 0; i < fields.length; i++) {Field field = fields[i];ExcelSelect selected = field.getAnnotation(ExcelSelect.class);ExcelProperty property = field.getAnnotation(ExcelProperty.class);if (selected != null) {ExcelSelectDataColumn excelSelectedResolve;if (StrUtil.isNotEmpty(selected.parentColumn())) {excelSelectedResolve = new ExcelSelectDataColumn<Map<String, List<String>>>();} else {excelSelectedResolve = new ExcelSelectDataColumn<List<String>>();}final Object source = excelSelectedResolve.resolveSource(selected);if (Objects.nonNull(source)) {if (property != null) {excelSelectedResolve.setParentColumn(selected.parentColumn());excelSelectedResolve.setColumn(property.value()[0]);excelSelectedResolve.setSource(source);excelSelectedResolve.setFirstRow(selected.firstRow());excelSelectedResolve.setLastRow(selected.lastRow());int index = property.index();if (index >= 0) {selectedMap.put(index, excelSelectedResolve);} else {index = i;selectedMap.put(index, excelSelectedResolve);}excelSelectedResolve.setColumnIndex(index);}}}}if (CollUtil.isNotEmpty(selectedMap)) {final Map<String, Integer> indexMap = selectedMap.values().stream().collect(Collectors.toMap(ExcelSelectDataColumn::getColumn, ExcelSelectDataColumn::getColumnIndex));selectedMap.forEach((k, v) -> {if (indexMap.containsKey(v.getParentColumn())) {v.setParentColumnIndex(indexMap.get(v.getParentColumn()));}});}return selectedMap;}

2 .SheetWriteHandler 自定义导出处理器

只是为了在具体某列加上对应的约束和下拉列表而已

import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.hp.excel.dto.ExcelSelectDataColumn;
import com.hp.excel.util.ExcelUtil;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;import java.util.List;
import java.util.Map;/*** @author HP* @date 2022/11/7*/
@RequiredArgsConstructor
public class SelectDataSheetWriteHandler implements SheetWriteHandler {private final Map<Integer, ExcelSelectDataColumn> selectedMap;/*** Called before create the sheet*/@Overridepublic void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {}/*** Called after the sheet is created*/@Overridepublic void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {// 这里可以对cell进行任何操作final Workbook workbook = writeWorkbookHolder.getWorkbook();Sheet sheet = writeSheetHolder.getSheet();DataValidationHelper helper = sheet.getDataValidationHelper();selectedMap.forEach((k, v) -> {// 设置下拉列表的行: 首行,末行,首列,末列if (StrUtil.isNotEmpty(v.getParentColumn())) {final Map<String, List<String>> data = (Map<String, List<String>>) v.getSource();ExcelUtil.addCascadeValidationToSheet(writeWorkbookHolder, writeSheetHolder, data, v.getParentColumnIndex(), k, v.getFirstRow(), v.getLastRow());} else {ExcelUtil.addSelectValidationToSheet(sheet, helper, k, v);}});}
}

2.1 ExcelUtil

import cn.hutool.core.collection.CollUtil;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import com.hp.excel.dto.ExcelSelectDataColumn;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;import java.util.List;
import java.util.Map;/*** @author HP* @date 2022/11/7*/
public class ExcelUtil {//二级级联public static void addCascadeValidationToSheet(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder, Map<String, List<String>> options, int parentColumnIndex, int childColumnIndex, int fromRow, int endRow) {final Workbook workbook = writeWorkbookHolder.getWorkbook();final Sheet sheet = writeSheetHolder.getSheet();DataValidationHelper helper = sheet.getDataValidationHelper();String hiddenSheetName = "sheet" + workbook.getNumberOfSheets();Sheet hiddenSheet = workbook.createSheet(hiddenSheetName);int rowIndex = 0;for (Map.Entry<String, List<String>> entry : options.entrySet()) {String parent = formatNameManager(entry.getKey());List<String> children = entry.getValue();if (CollUtil.isEmpty(children)) {continue;}int columnIndex = 0;Row row = hiddenSheet.createRow(rowIndex++);Cell cell = null;for (String child : children) {cell = row.createCell(columnIndex++);cell.setCellValue(child);}char lastChildrenColumn = (char) ((int) 'A' + (children.size() == 0 ? 1 : children.size()) - 1);createNameManager(workbook, parent, String.format(hiddenSheetName + "!$A$%s:$%s$%s", rowIndex, lastChildrenColumn, rowIndex));final DataValidationConstraint formulaListConstraint = helper.createFormulaListConstraint("INDIRECT($" + (char) ((int) 'A' + parentColumnIndex) + "2)");CellRangeAddressList regions = new CellRangeAddressList(fromRow, endRow, childColumnIndex, childColumnIndex);final DataValidation validation = helper.createValidation(formulaListConstraint, regions);validation.setErrorStyle(DataValidation.ErrorStyle.STOP);validation.setShowErrorBox(true);validation.setSuppressDropDownArrow(true);validation.createErrorBox("提示", "请输入下拉选项中的内容");sheet.addValidationData(validation);}hideSheet(workbook,1);}// 创建名称管理器private static Name createNameManager(Workbook workbook, String nameName, String formula) {Name name = workbook.createName();name.setNameName(nameName);name.setRefersToFormula(formula);return name;}//隐藏sheetprivate static void hideSheet(Workbook workbook, int start) {for (int i = start; i < workbook.getNumberOfSheets(); i++) {workbook.setSheetHidden(i, true);}}//名称管理器中名称不能有特殊字符private static String formatNameManager(String name) {name = name.replaceAll(" ", "").replaceAll("-", "_").replaceAll(":", ".");if (Character.isDigit(name.charAt(0))) {name = "_" + name;}return name;}//普通下拉列表public static void addSelectValidationToSheet(Sheet sheet, DataValidationHelper helper, Integer rowIndex, ExcelSelectDataColumn excelSelectDataColumn) {CellRangeAddressList rangeList = new CellRangeAddressList(excelSelectDataColumn.getFirstRow(), excelSelectDataColumn.getLastRow(), rowIndex, rowIndex);final List<String> source = (List<String>) excelSelectDataColumn.getSource();DataValidationConstraint constraint;// 设置下拉列表的值final String[] arr = source.toArray(new String[0]);constraint = helper.createExplicitListConstraint(arr);// 设置约束DataValidation validation = helper.createValidation(constraint, rangeList);// 阻止输入非下拉选项的值validation.setErrorStyle(DataValidation.ErrorStyle.STOP);validation.setShowErrorBox(true);validation.setSuppressDropDownArrow(true);validation.createErrorBox("提示", "请输入下拉选项中的内容");sheet.addValidationData(validation);}
}

2.2 INDIREC函数

当单元格的值为选中后, 如果名称管理器中能查到对应引用, 那么返回引用, 也就是一个列表

3.导出模版v1

如果只引入了EasyExcel包, 可以使用该方法生成文件

 public static <T> WriteSheet writeSelectedSheet(Class<T> head, Integer sheetNo, String sheetName) {//数据预处理方法Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(head);return EasyExcel.writerSheet(sheetNo, sheetName).head(head)//注册自定义write处理器.registerWriteHandler(new SelectedSheetWriteHandler(selectedMap)).build();}
@PostMapping("/template")
public void template(HttpServletRequest request, HttpServletResponse response) {String filename = "文件名称";try {String userAgent = request.getHeader("User-Agent");if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {// 针对IE或者以IE为内核的浏览器:filename = java.net.URLEncoder.encode(filename, "UTF-8");} else {// 非IE浏览器的处理:filename = new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);}response.setContentType("application/vnd.ms-excel");response.setHeader("Content-disposition", String.format("attachment; filename=\"%s\"", filename + ".xlsx"));response.setHeader("Cache-Control", "no-cache");response.setHeader("Pragma", "no-cache");response.setDateHeader("Expires", -1);response.setCharacterEncoding("UTF-8");ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();WriteSheet writeSheet = EasyExcelUtil.writeSelectedSheet(TestImportTemplateDTO.class, 0, "模版");excelWriter.write(new ArrayList<String>(), writeSheet);excelWriter.finish();} catch (UnsupportedEncodingException e) {log.error("导出Excel编码异常", e);} catch (IOException e) {log.error("导出Excel文件异常", e);}
}

4. 导出模版v2

如果使用了我的包, 通过包里已经编写的增强类来预处理下拉数据并配置自定义处理器

@ResponseExcel(name = "excel模板",sheets = @Sheet(sheetName = "模版", sheetNo = 0),enhancement = DynamicSelectDataWriterEnhance.class
)
@PostMapping("/template")
public List<xxxxImportTemplateDTO> templateV2() {return Collections.singletonList(new xxxxImportTemplateDTO());
}

5.效果

  • 单列默认值写死直接生成
  • 单列动态通过处理器处理后获取
  • 父列通过处理器获取
  • 级联列根据父列选择的内容加载名称管理器中的值, 达到级联效果

  • 在wps excel的名称管理器中可见创建了两个名称,分别对应父列的值
    名称管理器中创建新名称不能有特殊字符
  • 实际就是在第二个sheet中横向的生成数据, 同时创建对应的名称管理器
    这里是为了展示, 没有调用隐藏sheet的方法, 如果调用了, sheet1将被隐藏
  • 在主sheet中创建了数据有效性验证约束, 当值不是名称管理中的对应列表下的值时, 报错
  • 下拉列表中引用了父列的值
  • 当输入了不在范围内的值时,报错提示

四、导入

因为我在项目中使用的涉及到存在合并行的情况, 所以使用了自定义的导入监听器, 该监听器主要是在处理完所有数据后把合并行归为一个集合, 只保留第一行的行号, 最终形成Map<Integer, List> map集合, 如果没有合并行, 可以忽略或者在自定义监听器中做校验

1. 导入接口

1.1 v1

只使用easyexcel的场景可以这样处理

 public AjaxResult import(MultipartFile file) {Map<Integer, List<ImportDTO>> map = new HashMap<>(16);EasyExcel.read(file.getInputStream(), ImportDTO.class, new EasyExcelUtil.ImportEventListener<>(map)).extraRead(CellExtraTypeEnum.MERGE).excelType(ExcelTypeEnum.XLSX).headRowNumber(1).sheet(0).doRead();xxxxService.validateImport(map);return xxxxService.import(map);
}

1.2 v2

使用我的包的场景可以这样处理

    @PostMapping("/import")public AjaxResult importProjectV2(@RequestExcel(listener = MergeRowAnalysisEventListener.class,enhancement = {MergeRowReaderEnhance.class})Map<Integer, List<xxxImportDTO>> map) {xxxtService.validateImport(map);return xxxService.import(map);}

2.自定义导入监听器

MergeRowAnalysisEventListener 或 EasyExcelUtil.ImportEventListener 作用都一样

非必要 或者清空extra方法,在这个监听器里做校验就行了

以下代码不难看出, 其实可以在EasyExcel处理数据的时候就校验数据, 并合理使用其抛异常方法抛出异常, 但是debug之后发现处理合并行方法 extra 在最后执行, 没利用到组件的 onException 方法, 所以推迟到组件处理完数据后再统一校验

public static class ImportEventListener<T> extends AnalysisEventListener<T> {private final Map<Integer, List<T>> map;private Integer headRowNumber = 1;public ImportEventListener(Map<Integer, List<T>> map) {this.map = map;}// 这个是每行的数据(每一行都会执行这个)@Overridepublic void invoke(T data, AnalysisContext context) {final List<T> list = new ArrayList<>();list.add(data);map.put(map.keySet().size(), list);}//所有数据处理之后@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {}//可以考虑有一条错就抛, 还是所有数据处理完,其中有异常,全部抛@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {throw exception;}// 这个是读取单元格和并时的信息@SneakyThrows@Overridepublic void extra(CellExtra extra, AnalysisContext context) {if (headRowNumber == null) {headRowNumber = context.readSheetHolder().getHeadRowNumber();}// 获取合并后的第一个索引Integer index = extra.getFirstRowIndex() - headRowNumber;final List<T> first = map.get(index);// 获取合并后的最后一个索引Integer lastRowIndex = extra.getLastRowIndex() - headRowNumber;for (int i = index + 1; i <= lastRowIndex; i++) {final List<T> c = map.get(i);if (CollUtil.isNotEmpty(c)) {first.addAll(c);map.remove(i);}}}}

3. 校验数据

基于javax.validation, org.hibernate.validator, 点进去看看就差不多清楚了

3.1 自定义校验注解

使用到自定义校验器, 和一个获取动态数据的处理器

import com.hp.excel.handler.ColumnDynamicSelectDataHandler;
import com.hp.excel.handler.DefaultColumnDynamicSelectDataHandler;
import com.hp.excel.valid.DynamicSelectDataValidator;import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;/*** 提供导入数据自定义数据源校验** 通过handler返回的数据集是否包含单元格数据判断是否合法**/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Constraint(validatedBy = DynamicSelectDataValidator.class)  // 校验器
public @interface DynamicSelectData {String message() default "请填写规定范围的值";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};String parameter() default "";Class<? extends ColumnDynamicSelectDataHandler> handler() default DefaultColumnDynamicSelectDataHandler.class;
}

3.2 自定义校验器

在校验时, 这个校验器会校验对应注解, 获取到注解的信息, 初始化动态数据处理器, 调用校验方法时查询数据并完成校验, 校验为false时, 会根据提示信息保存在ConstraintViolation对象中, 最后形成一个集合

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.hp.excel.annotation.DynamicSelectData;
import com.hp.excel.handler.ColumnDynamicSelectDataHandler;
import lombok.extern.slf4j.Slf4j;import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
import java.util.Objects;/*** @author HP* @date 2022/11/7*/
@Slf4j
public class DynamicSelectDataValidator implements ConstraintValidator<DynamicSelectData, String> {private String arg = null;private ColumnDynamicSelectDataHandler handler = null;@Overridepublic void initialize(DynamicSelectData data) {this.arg = data.parameter();final Class<? extends ColumnDynamicSelectDataHandler> sourceHandlerClass = data.handler();this.handler = SpringUtil.getBean(sourceHandlerClass);}@Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {if (StrUtil.isEmpty(value) || Objects.isNull(handler)) {return true;}try {final List<String> constrainSource = (List<String>) handler.source().apply(arg);return constrainSource.contains(value);} catch (Exception e) {return false;}}
}

3.3 动态获取数据处理器

只是获取合法数据集, 随便怎么写都行, 优化后这里的处理器和模版导出时的相同

3.4 校验数据方法

每一行校验一次, 每行所有列的问题返回一个map集合, 实际怎么封装校验结果可以自己修改

这里也能发现一个明显问题, 获取数据的方法每行都会被调用, 我的场景里数据量不是很大, 但是如果比较大的话, 还是需要准备一个上下文之类的存一下会一直重复的数据集, 避免每次都查询

public static <T> Optional<Map<String, Object>> validateData(T data) {//这里的默认validator可以改成静态成员变量, 避免每次行记录都调final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();//校验数据后的结果Set<ConstraintViolation<T>> set = validator.validate(data, Default.class);if (CollUtil.isEmpty(set)) {return Optional.empty();}List<String> columnExceptions = new ArrayList<>();for (ConstraintViolation<T> cv : set) {columnExceptions.add(cv.getMessage());}if (CollUtil.isEmpty(columnExceptions)) {return Optional.empty();}final Map<String, Object> rowExceptionMap = new HashMap<>(16);rowExceptionMap.put("exceptions", columnExceptions);return Optional.of(rowExceptionMap);
}

最后收集所有异常返回前端做提示

3.5 数据实体

这里只展示跟上述内容相关的注解, 其他例如NotEmpty, Digits, Email之类的注解有自己对应的校验器
如果写死的列表用正则就行了, 动态的数据配置处理器查询

@Data
public class TestImportDTO implements Serializable {@ExcelProperty(value = "普通列")private String common;@Pattern(regexp = "^(A|B|C|D)$", message = "校验信息")@ExcelSelect(staticData = "[\"A\", \"B\", \"C\", \"D\", \"E\"]")@ExcelProperty(value = "单列select")private String singleSelect;@DynamicSelectData(message = "动态单列select请填写给定的选项", handler = {DynamicDataConstrainSourceHandler.class}, parameter = "自定义参简单参数")@ExcelSelect(handler = ExcelTestSourceHandler.class)@ExcelProperty(value = "动态单列select")private String dynamicSingleSelect;//parent字段定义父列名称@DynamicSelectData(message = "级联子列请填写给定的选项", handler = {ChildConstrainSourceHandler.class})@ExcelSelect(parentColumn = "父列", handler = ExcelChildSourceHandler.class)@ExcelProperty(value = "级联子列")private String child;@DynamicSelectData(message = "父列请填写给定的选项", handler = {ChildConstrainSourceHandler.class})@ExcelSelect(handler = ExcelParentSourceHandler.class)@ExcelProperty(value = "父列")private String parent;private static final long serialVersionUID = 1L;
}

4. 校验结果

{"msg": "导入数据异常","code": 500,"data": [{"row": "第1条记录异常","exceptions": ["xxx不能为空"]},{"row": "第1条记录的第2条子记录记录异常","exceptions": ["子记录xxx请填写纯数字(整数位不超过10位,小数位不超过两位)"]},{"row": "第2条记录异常","exceptions": ["xxx不能为空"]},{"row": "第3条记录异常","exceptions": ["xxxx请填写纯数字(整数位不超过10位,小数位不超过两位)"]},{"row": "第3条记录的第3条子记录异常","exceptions": ["子记录xxx请填写纯数字(整数位不超过10位,小数位不超过两位)"]}]
}

结束

本文主要是介绍如何导出带下拉菜单和级联菜单的数据表, 利用自定义校验完成与此模版的整合校验, 保证数据完整性, 主要是理解整个的逻辑和概念, 我也根据pig4cloud的逻辑写了一套自己的包,增加了一些东西, 修改了一些定义, 仅供参考, 互相学习…peace out

EasyExcel生成带下拉列表或二级级联列表的Excel模版+自定义校验导入数据(附仓库)相关推荐

  1. easyexcel 检查表头是否匹配_利用easyexcel生成excel文件-自定义表头与数据栏对应的处理方式...

    前面几篇文章测试过用easyexcel生成动态表头,动态样式.特别是动态表头以及下面数据列表与表头字段的对应是采用注解方式实现的.但在实际工作中,有些到处是灵活生成的,也就是说对于同一个类,在不同的场 ...

  2. luckySheet+POI+EasyExcel实现在线excel模版的导出和数据填充

    luckySheet+POI+EasyExcel实现在线excel模版的导出和数据填充 业务需求 关键字 luckySheet POI EasyExcel 代码实现 前端luckySheet配置的ex ...

  3. easyexcel导入时读不到数据_java编程中通过easypoi导入excel文件并验证导入数据

    引言 现如今越来越多的web网站或者内部管理web系统都有自己的数据分析中心.其数据中心的数据有些来源于人工单独操作,某些来自人工搜集大量的信息后通过excel文件批量导入进系统.本博客将讲解在jav ...

  4. python图片保存为txt文件_python实现对文件中图片生成带标签的txt文件方法

    在深度学习中经常需要生成带标签的图片名称列表,xxxlist.txt文件,下面写一个简单的python脚本生成该文件列表. import os def generate(dir,label): fil ...

  5. 记录element-ui级联选择器,二级三级列表无法显示的解决办法

    <!-- 选择商品分类的级联选择框 --><el-cascader expand-trigger="hover" :options="catelist& ...

  6. 前端---二级级联下拉列表的实现

    写在前面:本是一前端小白,奈何工作需要,不得不硬着头皮上,但是感觉费劲又慢,光是一个小小的二级级联的东西就弄个半天,愁!特此总结! 前端代码: <select id="select1& ...

  7. Easyexcel生成excel并通过自定义注解实现下拉框以及动态下拉框(将数据库中的数据显示在excel下拉框中)

    首先需要定义excel实体类 @Data @ColumnWidth(22) @HeadRowHeight(30) public class ExcelProductDTO {//动态下拉框,可以查询数 ...

  8. Easyexcel生成excel直接发送邮件

    依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>sprin ...

  9. EasyExcel在项目中的应用-在web中导出带下拉框和批注的excel文件

    前言 ​ 好长一段时间没有更新博客了,最近刚刚找到实习工作,接触了企业中的项目,在这段时间的实习过程中,终于知道了企业级项目的体量和业务难度跟之前的小项目是完全不同的.10多天的适应期也逐渐让我找到了 ...

最新文章

  1. python对象编程例子-python编程进阶之类和对象用法实例分析
  2. mac 卸载 eclipse_Mac 新手准备工具集合
  3. matlab偶极矩电场强度分布图_1.2.10 电介质在外电场下的极化、电极化强度、电极化率...
  4. Qt / QMainWindow、QDialog、QWidget
  5. [ZJOI2010]网络扩容[网络流24题]
  6. junit 验证日志输出_JUnit规则–引发异常时执行附加验证
  7. html5 规定输入字段,HTML5 Input属性详解
  8. php71+yum源+epel,搭建CentOS在线yum源镜像服务器
  9. 设计模式之GOF23适配器模式
  10. MapGISnbsp;K9nbsp;SP3amp;nb…
  11. vue中如何使用h5自定义标签?
  12. python 使用 requests 库发送请求及设置代理
  13. 【Web】CSS(No.33)Css页面布局经典案例(三)《京东首页》
  14. 一个标星 5.2k+ 的牛逼开源商城系统
  15. 推荐一款文件分享工具-文叔叔
  16. 五招教你做好企业年终绩效考核,太实用了!
  17. Windows环境下OpenSSL下载安装及制作证书
  18. 【LIDC-IDRI】 CT 肺结节 XML 标记特征良恶性标签PKL转储(一)
  19. VC,MFC开发技巧收集
  20. Flow公链 |FCL1.0正式上线

热门文章

  1. nginx 访问状态统计 访问控制 虚拟主机
  2. {电脑救助站}常用知识3
  3. Mysql数据库管理系统原理及基本操作
  4. 河道水面漂浮物垃圾识别监测 yolov7
  5. Java switch和break用法
  6. HI3861学习笔记(19)——WiFi接口使用(STA和AP模式)
  7. Python30 网络编程通讯协议,1.学习网络编程的目的 2.什么是互联网 3.c/s结构 4.通讯基本要素 5.OSI模型...
  8. windows 安装 matplotlib 报错
  9. linux退出guest用户_用户、组及其它 Linux 特性 | Linux 中国
  10. 一位期货老将的经验之谈(转)