一、业务场景
原本只是一个简单的模板导出功能,为了防止业务人员输入错误导致匹配不到数据库中的内容,采用了excel下拉框选择的方式。
但是,由于下拉框中的内容可能存在变化,如果每次手动调整下拉框的内容会比较麻烦,后来便采取了动态生成模板的方式。
二、效果展示
三、实现思路
采用easyexcel导出功能,配合自定义注解@ExcelDataValidation动态获取数据,实现excel的数据验证功能
四、实现代码
1、完整代码
gitee地址
2、模板映射实体类
StudentExcelTemplateDTO.java
添加**@ExcelDataValidation**的属性会根据注解配置进行处理,例如 type = "select"为表格下拉类型,会去解析sourceClass配置类下的方法来获取下拉数据。
不同的列下拉数据相同时,需要将key设置同样的值
java
@Data
@Accessors(chain = true)
@SuppressWarnings("unused")
public class StudentExcelTemplateDTO {
@ExcelProperty(index = 0, value = "*Name")
@ExcelDataValidation(title = "Name", maxValue = "200")
@ColumnWidth(25)
@HeadFontStyle(color = Font.COLOR_RED)
private String name;
@ExcelProperty(index = 2, value = "*ProgramStart")
@ExcelDataValidation(title = "ProgramStart", type = "date", minValue = "1970/01/01", maxValue = "3099/01/01")
@ColumnWidth(20)
@HeadFontStyle(color = Font.COLOR_RED)
private String learnFrom;
@ExcelProperty(index = 7, value = "Curriculum1")
@ExcelDataValidation(title = "curriculum1", type = "select", sourceClass = CurriculumSelect.class, key = "curriculum")
@ColumnWidth(55)
@HeadFontStyle(color = Font.COLOR_NORMAL)
private String curriculum1;
// 其他属性
}
3、注解ExcelDataValidation
添加该注解的属性列会添加自定义的数据验证约束
java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ExcelDataValidation {
/**
* 标题,建议使用字段名
*/
String title() default "title";
/**
* 类型
*
* @see ExcelCellTypeEnum
*/
String type() default "text";
/**
* 多列下拉使用同一个下拉内容,同一个key下拉内容相同
* 注意:使用相同key的字段要放在一起
*/
String key() default "";
/**
* 固定下拉内容
*/
String[] source() default {};
/**
* 动态下拉内容
*/
Class<? extends ExcelDynamicSelect>[] sourceClass() default {};
/**
* 设置下拉框的起始行,默认为第二行
*/
int firstRow() default 1;
/**
* 设置下拉框的结束行,默认为最后一行
*/
int lastRow() default 0x10000;
/**
* 表格约束的最小值
*/
String minValue() default "";
/**
* 表格约束的最大值
*/
String maxValue() default "";
}
4、实现ExcelDynamicSelect
模拟查询数据库逻辑,用于获取动态下拉内容
java
@Service
public class CurriculumSelect implements ExcelDynamicSelect {
private static final CurriculumService curriculumService;
static {
curriculumService = SpringUtil.getBean(CurriculumService.class);
}
@Override
public String[] getSource() {
// 查询逻辑
return curriculumService.getCurriculums();
}
}
5、类型枚举
description信息用于excel中的错误提示
java
@Getter
@AllArgsConstructor
public enum ExcelCellTypeEnum {
TEXT(1, "text", "Please input text."),
DATE(2, "date", "Please input the date. Format:YYYY/MM/DD."),
POSITIVE_INTEGER(3, "positive_integer", "Please input positive integers."),
INTEGER(4, "integer", "Please input integers."),
SELECT(5, "select", "Please select from the dropdown menu."),
;
private final int id;
private final String type;
private final String description;
// 方法...
}
6、excel模板工具类
6-1、模板导出方法exportTemplate
java
@Slf4j
public class ExcelTemplateUtil {
public static void exportTemplate(String filename, Class<?> clazz, HttpServletResponse response) {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(filename, StandardCharsets.UTF_8));
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", -1);
response.setCharacterEncoding("UTF-8");
try (ExcelWriter excelWriter = EasyExcelFactory.write(response.getOutputStream()).useDefaultStyle(false).build()) {
// 解析表头类中的注解,获取数据
List<ExcelConstraintResolve> resolveList = resolveSelectedAnnotation(clazz);
// 创建即将导出的sheet页
WriteSheet writeSheet = EasyExcelFactory.writerSheet(0, "sheet1")
.head(clazz)
.registerWriteHandler(new ConstraintSheetWriteHandler(resolveList))
.build();
excelWriter.write(new ArrayList<>(), writeSheet);
excelWriter.finish();
} catch (IOException e) {
log.error("导出模板异常:", e);
}
}
}
6-2、方法:resolveSelectedAnnotation
用于解析注解**@ExcelDataValidation**中,下拉类型则动态获取数据
java
ExcelTemplateUtil.java
/**
* 解析表头类中的注解
*
* @param head 表头类
* @param <T> 泛型
*/
private static <T> List<ExcelConstraintResolve> resolveSelectedAnnotation(Class<T> head) {
List<ExcelConstraintResolve> result = new ArrayList<>();
// getDeclaredFields(): 返回全部声明的属性;getFields(): 返回public类型的属性
Field[] fields = head.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
// 解析注解信息
ExcelDataValidation dataValidation = field.getAnnotation(ExcelDataValidation.class);
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
if (Objects.isNull(dataValidation)) {
continue;
}
// 获取列索引
int index = (property != null && property.index() >= 0) ? property.index() : i;
// 处理下拉类型
if (StringUtils.equals(ExcelCellTypeEnum.SELECT.getType(), dataValidation.type())) {
handleSelectType(dataValidation, result, index);
}
// 处理其他类型
else {
result.add(getResolve(dataValidation, dataValidation.type(), null, index));
}
}
return result;
}
6-3、下拉类型处理方法 handleSelectType
java
ExcelTemplateUtil.java
/**
* 处理下拉类型
*/
private static void handleSelectType(ExcelDataValidation dataValidation, List<ExcelConstraintResolve> result, int index) {
// 自定义key
String key = dataValidation.key();
// 判断是否已经存在key,存在则直接将存在的复制一份
Map<String, ExcelConstraintResolve> keyResolveMap = getSelectTemplateMap(result, key);
if (CollUtil.isNotEmpty(keyResolveMap) && keyResolveMap.containsKey(key)) {
ExcelConstraintResolve tempResolve = keyResolveMap.get(key);
result.add(getResolve(dataValidation, ExcelCellTypeEnum.SELECT.getType(), tempResolve.getSource(), index));
return;
}
// 获取下拉框数据
String[] source = resolveSelectedSource(dataValidation);
if (source.length > 0) {
result.add(getResolve(dataValidation, ExcelCellTypeEnum.SELECT.getType(), source, index));
}
}
6-4、解析source或sourceClass的数据
java
/**
* 获取下拉框数据
*/
private static String[] resolveSelectedSource(ExcelDataValidation excelDataValidation) {
if (excelDataValidation == null) {
return new String[0];
}
// 获取固定下拉框的内容
String[] source = excelDataValidation.source();
if (source.length > 0) {
return source;
}
// 获取动态下拉框的内容
Class<? extends ExcelDynamicSelect>[] classes = excelDataValidation.sourceClass();
if (classes.length > 0) {
try {
ExcelDynamicSelect excelDynamicSelect = classes[0].getDeclaredConstructor().newInstance();
String[] dynamicSelectSource = excelDynamicSelect.getSource();
if (dynamicSelectSource != null && dynamicSelectSource.length > 0) {
return dynamicSelectSource;
}
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException |
IllegalAccessException e) {
log.error("解析动态下拉框数据异常", e);
}
}
return new String[0];
}
7、Handler类ConstraintSheetWriteHandler
实现SheetWriteHandler接口,在Excel文件写入过程中,对sheet的创建和处理进行自定义操作
使用策略模式处理不同的逻辑 BasicConstraintStrategyFactory.getStrategy(resolve.getType());
java
public record ConstraintSheetWriteHandler(List<ExcelConstraintResolve> resolveList) implements SheetWriteHandler {
/**
* Called before create the sheet
*/
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 当前版本暂不需要此功能
}
/**
* Called after the sheet is created
*/
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 这里可以对cell进行任何操作
Sheet sheet = writeSheetHolder.getSheet();
DataValidationHelper helper = sheet.getDataValidationHelper();
// 创建一个隐藏的sheet
Workbook workbook = writeWorkbookHolder.getWorkbook();
String sheetName = "hidden0";
Sheet hiddenSheet = workbook.createSheet(sheetName);
// 存储循环中带有自定义key的集合,用于后续循环从中获取
Map<String, ExcelConstraintResolve> tempKeyMap = new HashMap<>();
for (ExcelConstraintResolve resolve : resolveList) {
// 策略模式 执行不同的逻辑
ConstraintTypeStrategy strategy = BasicConstraintStrategyFactory.getStrategy(resolve.getType());
if (Objects.isNull(strategy)) {
log.info("不支持的类型 :{}", resolve.getType());
continue;
}
strategy.process(resolve, tempKeyMap, helper, hiddenSheet, sheet);
}
// 设置存储下拉列值得sheet为隐藏
this.setHiddenSheet(workbook, sheetName);
}
}
8、策略模式相关类
8-1、ConstraintTypeStrategy
java
public interface ConstraintTypeStrategy {
void process(ExcelConstraintResolve resolve, Map<String, ExcelConstraintResolve> tempKeyMap, DataValidationHelper helper, Sheet hiddenSheet, Sheet sheet);
}
8-2、BasicConstraintStrategyFactory
java
public class BasicConstraintStrategyFactory {
/**
* 策略集合
*/
static final Map<String, ConstraintTypeStrategy> CELL_TYPE_STRATEGY_MAP = new ConcurrentHashMap<>();
/**
* 暴露一个getStrategy方法,用于获取策略
*/
public static ConstraintTypeStrategy getStrategy(String type) {
return CELL_TYPE_STRATEGY_MAP.get(type);
}
}
9、下拉约束策略
使用@PostConstruct注解,在项目启动时将不同的策略实例加入到CELL_TYPE_STRATEGY_MAP中
java
@Component
public class SelectConstraintTypeStrategy extends BasicConstraintStrategyFactory implements ConstraintTypeStrategy {
@Override
public void process(ExcelConstraintResolve resolve, Map<String, ExcelConstraintResolve> tempKeyMap, DataValidationHelper helper, Sheet hiddenSheet, Sheet sheet) {
ExcelConstraintResolve tempResolve = StringUtils.isNotBlank(resolve.getKey()) ? tempKeyMap.get(resolve.getKey()) : null;
if (Objects.nonNull(tempResolve)) {
this.setSelectConstraint(resolve, helper, hiddenSheet, sheet, tempResolve);
} else {
if (StringUtils.isNotBlank(resolve.getKey())) {
tempKeyMap.put(resolve.getKey(), resolve);
}
// 创建下拉的行和单元格
this.createRowAndCell(hiddenSheet, resolve, resolve.getIndex());
// 创建下拉的约束
this.setSelectConstraint(resolve, helper, hiddenSheet, sheet, resolve);
}
}
/**
* 设置下拉框的约束
*/
private void setSelectConstraint(ExcelConstraintResolve resolve, DataValidationHelper helper, Sheet hiddenSheet, Sheet sheet, ExcelConstraintResolve tempResolve) {
// 创建下拉的约束
DataValidationConstraint constraint = this.getSelectDataValidationConstraint(resolve, tempResolve, hiddenSheet.getSheetName(), helper);
// 设置单元格验证
this.setCellValidation(resolve, helper, constraint, sheet, true, true);
}
/**
* 获取下拉框的数据验证约束
*/
public DataValidationConstraint getSelectDataValidationConstraint(ExcelConstraintResolve resolve, ExcelConstraintResolve tempResolve, String sheetName, DataValidationHelper helper) {
String excelLine = this.getExcelLine(tempResolve.getIndex());
String refers = "=" + sheetName + "!$" + excelLine + "$1:$" + excelLine + "$" + resolve.getSource().length;
return helper.createFormulaListConstraint(refers);
}
/**
* 返回excel列标A-Z-AA-ZZ
*
* @param num 列数
* @return java.lang.String
*/
private String getExcelLine(int num) {
String line = "";
int first = num / 26;
int second = num % 26;
if (first > 0) {
line = (char) ('A' + first - 1) + "";
}
line += (char) ('A' + second) + "";
return line;
}
/**
* 创建下拉的行和单元格
*/
private void createRowAndCell(Sheet hiddenSheet, ExcelConstraintResolve excelSelectedResolve, Integer index) {
for (int i = 0, length = excelSelectedResolve.getSource().length; i < length; i++) {
Row row = hiddenSheet.getRow(i);
if (null == row) {
row = hiddenSheet.createRow(i);
}
Cell cell = row.getCell(index);
if (null == cell) {
cell = row.createCell(index);
}
cell.setCellValue(excelSelectedResolve.getSource()[i]);
}
}
@PostConstruct
public void init() {
CELL_TYPE_STRATEGY_MAP.put(ExcelCellTypeEnum.SELECT.getType(), this);
}
}
完结。如有疑问麻烦留言或者私信。
vvv: H先生出品