Excel动态下拉模板导出+自定义注解+策略模式

一、业务场景

​ 原本只是一个简单的模板导出功能,为了防止业务人员输入错误导致匹配不到数据库中的内容,采用了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先生出品

相关推荐
xcbeyond4 分钟前
Kubernetes 中 Java 应用性能调优指南:从容器化特性到 JVM 底层原理的系统化优化
java·jvm·云原生·kubernetes
蓝白咖啡20 分钟前
华为OD机试 - 王者荣耀匹配机制 - 回溯(Java 2024 D卷 200分)
java·python·算法·华为od·机试
一人の梅雨25 分钟前
西域平台关键字搜索接口开发指南
java·开发语言·数据库
triticale1 小时前
【图论】最短路径问题总结
java·开发语言·图论
暮辰7771 小时前
多JDK环境安装及切换使用
java
qq_447663051 小时前
Spring的事务处理
java·后端·spring
男Ren、麦根1 小时前
Java抽象类:深入理解与应用
java·开发语言
Foyo Designer1 小时前
【 <二> 丹方改良:Spring 时代的 JavaWeb】之 Spring Boot 中的消息队列:使用 RabbitMQ 实现异步处
java·spring boot·程序人生·spring·职场和发展·rabbitmq·java-rabbitmq
小钊(求职中)1 小时前
七种分布式ID生成方式详细介绍--Redis、雪花算法、号段模式以及美团Leaf 等
java·spring boot·分布式·spring·mybatis
martian6652 小时前
分布式并发控制实战手册:从Redis锁到ZK选主的架构之道
java·开发语言·redis·分布式·架构