Java Excel 多级菜单联动原理与实现(可扩展)

Java Excel级联菜单实现(可扩展)

为什么要写这篇文章呢,因为看到了有人在提问如何用Java做Excel的级联菜单效果。帖子详情:http://spring4all.com/forum-post/575
我之前也遇到过同样的场景,当时查了很多文档才搞定,为了让更多人可以直接使用代码,节省时间,所以决定写这篇文章。

实现效果

先上效果图,这里演示我做了三级下拉菜单的联动,不过我实现的版本可以支持任意级菜单的联动

实现原理介绍

名称管理器

Excel中有名称管理器的概念,什么意思呢。可以简单理解为,一个名称对应一组数据序列,举个例子,一个省会对映多个市,省名则是名称管理器的名字,对应的数据序列则是相关的市级城市名。

在创建下拉关系对映规则时,需要先根据数据的对映关系,创建所有名称管理器,然后再用数据有效性绑定名称管理器的名字就行了。
Java Excel 多级菜单联动原理与实现(可扩展)_第1张图片
可以在公式->名称管理器内查看我这边创建的名称管理器和对映关系。

数据有效性

名称管理器创建好了,怎么使用呢?这就要用到Excel的数据有效性了。
你可以任意选中一个单元格,在菜单栏上点击数据->有效性
Java Excel 多级菜单联动原理与实现(可扩展)_第2张图片
在数据有效性内设置如下
Java Excel 多级菜单联动原理与实现(可扩展)_第3张图片
来源参数说明
=INDIRECT() :Excel内置函数,可以返回单元格的值引用
dataSheet:数据页的名称
$A$1:数据页的单元格位置
$A$1 是什么值呢,其实就是province
Java Excel 多级菜单联动原理与实现(可扩展)_第4张图片
还记得名称管理器的province对应的数据序列吗,会被引用到这个单元格上,于是效果如下
Java Excel 多级菜单联动原理与实现(可扩展)_第5张图片
好了,基本原理就是这样,接下来我们看下怎么使用代码生成

设计与约定

易使用

只需要用到两个注解,分别是ExcelFile、ExcelValidation
ExcelFile代表需要生成文件
目前Excel是在本地创建的,不过可以根据我的源码修改成上传到服务器,如果你需要帮助,可以给我留言
ExcelValidation是打在字段上的,用来标注字段是否需要生成校验。
目前获取数据源的的方式比较单一,只支持静态无参方法,如果你的项目整合了Spring,也可以改从Bean方法内获取数据,如果你需要帮助,可以给我留言

低入侵

你可以选择copy源码或者打jar包的方式来使用,只需要在Excel实体对象上标注注解即可

约定

  1. 如果数据集返回的是List,代表是单独的、无依赖的列
  2. 如果数据集返回的是Map,key是依赖列的名称,value是key所对应的数据

小试牛刀

我的项目结构
excel-example是例子,准备的一些数据
excel-extend是具体的实现
Java Excel 多级菜单联动原理与实现(可扩展)_第6张图片

首先我在com.excel.service.ExcelExampleService准备了数据列表
queryProvinceList,获取省列表
queryMunicipalityList,获取市列表,Map的Key是所属的省
queryDistrictList,获取区列表,Map的Key是所属的市

public class ExcelExampleService {

    public static List<String> queryProvinceList() {
        return Arrays.asList("浙江省", "湖南省", "贵州省");
    }

    public static Map<String, List<String>> queryMunicipalityList() {
        Map<String, List<String>> map = new HashMap<>(4);
        map.put("浙江省", Arrays.asList("杭州市", "温州市", "宁波市"));
        map.put("湖南省", Arrays.asList("长沙市", "邵阳市", "常德市"));
        map.put("贵州省", Arrays.asList("贵阳市", "遵义市", "安顺市"));
        return map;
    }

    public static Map<String, List<String>> queryDistrictList() {
        Map<String, List<String>> map = new HashMap<>(8);
        map.put("杭州市", Arrays.asList("上城区", "下城区", "萧山区"));
        map.put("温州市", Arrays.asList("鹿城区", "龙湾区", "瓯海区", "洞头区"));
        map.put("长沙市", Arrays.asList("芙蓉区", "天心区", "岳麓区"));
        map.put("邵阳市", Arrays.asList("双清区", "大祥区", "北塔区"));
        map.put("贵阳市", Arrays.asList("南明区", "云岩区", "花溪区"));
        return map;
    }

}

然后配置DTO

import com.excel.annotation.ExcelFile;
import com.excel.annotation.ExcelValidation;
import lombok.Data;

/**
 * @author ximu
 * @date 2022/4/6
 * @description
 */
@ExcelFile(fileHeadTemplate = "province|municipality|district", fileMappingTemplate = "province=所属省|municipality=所属市|district=所属区",
        datasheetHidden = false, enableDataValidation = true)
@Data
public class ExcelExportDTO {

    /**
     * 所属省
     */
    @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryProvinceList")
    private String province;

    /**
     * 所属市
     */
    @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryMunicipalityList", beforeFieldName = "province")
    private String municipality;

    /**
     * 所属区
     */
    @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryDistrictList", beforeFieldName = "municipality")
    private String district;

}

启动项目,创建Excel

public class T_Main {

    public static void main(String[] args) throws IOException {
        ExcelExportDTO excelExportDTO = new ExcelExportDTO();
        String excel = ExcelUtil.createExcel(Arrays.asList(excelExportDTO));
        System.out.println(excel);
    }

}

创建成功后,sheet1可以正常的选择下拉菜单
你会发现还有一页数据页,可以隐藏起来
使用 ExcelFile 注解属性 datasheetHidden 配置
默认是 true 隐藏的
Java Excel 多级菜单联动原理与实现(可扩展)_第7张图片
如果用户随意输入值的话,是可以强制校验的
Java Excel 多级菜单联动原理与实现(可扩展)_第8张图片
通过 ExcelFile 注解 showErrorBox 属性控制

实现代码

注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author ximu
 * @date 2021/8/29
 * @description 标记类为一个Excel文件
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ExcelFile {

    /**
     * 文件全名
     */
    String fileName() default "excel.xlsx";

    /**
     * 页名称
     */
    String sheetName() default "sheet1";

    /**
     * excel使用的属性排列格式模板 格式: 字段名1{fileTemplateSplit}字段名2{fileTemplateSplit}字段名3
     * 

* 示例:id|name|age *

* 说明表头index0=id,index1=name */ String fileHeadTemplate() default ""; /** * excel属性与表头映射模版 格式: 字段1{fileMappingSplit}映射名称1{fileTemplateSplit}字段1{fileMappingSplit}映射名称1 *

* 示例:id=学号|name=学生姓名 *

* 说明id属性映射表头为学号,name属性映射表头为学生姓名 */ String fileMappingTemplate() default ""; /** * 模版字段分隔符,默认无需调整 */ String fileTemplateSplit() default "\\|"; /** * 属性名称映射分隔符,默认无需调整 */ String fileMappingSplit() default "\\="; /** * 启用数据校验,只有当值为true时,数据页才会创建 */ boolean enableDataValidation() default false; /** * 数据页名称 */ String dataSheetName() default "dataSheet"; /** * 隐藏数据页 */ boolean datasheetHidden() default true; /** * 校验用户输入是否合法 */ boolean showErrorBox() default true; }

import org.apache.poi.ss.usermodel.DataValidationConstraint;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author ximu
 * @date 2022/3/26
 * @description excel校验器
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelValidation {

    /**
     * 数据源方法全名
     */
    String datasourceMethod();

    /**
     * 前列字段
     * 

* 当为空字符串时,认定无前列依赖 */ String beforeFieldName() default ""; /** * 开始行 */ int firstRow() default 1; /** * 结束行 */ int lastRow() default 2000; /** * 校验类型 * * @see org.apache.poi.ss.usermodel.DataValidationConstraint.ValidationType */ int validationType() default DataValidationConstraint.ValidationType.LIST; }

工具类

import com.excel.annotation.ExcelFile;
import com.excel.annotation.ExcelValidation;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFDataValidationConstraint;
import org.apache.poi.xssf.usermodel.XSSFDataValidationHelper;
import org.apache.poi.xssf.usermodel.XSSFSheet;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author ximu
 * @date 2022/4/6
 * @description Excel工具类
 */
public class ExcelUtil {

    /**
     * 构建excel文件
     *
     * @param collection 数据集合
     * @return excel文件路径
     */
    public static String createExcel(Collection<?> collection) throws IOException {
        if (CollectionUtils.isEmpty(collection)) {
            throw new RuntimeException("excel数据不能为空!");
        }
        Object object = collection.stream().findFirst().get();
        Class<?> clazz = object.getClass();
        boolean annotationPresent = clazz.isAnnotationPresent(ExcelFile.class);
        if (!annotationPresent) {
            throw new RuntimeException("该对象不存在ExcelFile注解,不能生成Excel!");
        }
        ExcelFile annotation = clazz.getAnnotation(ExcelFile.class);
        // 取到模板
        String headTemplate = annotation.fileHeadTemplate();
        // 取到模板上的所有对象属性
        String[] split = headTemplate.split(annotation.fileTemplateSplit());
        // 取到模板属性与名称的映射关系
        String fileTemplateSplit = annotation.fileMappingTemplate();
        String[] mappingSplit = fileTemplateSplit.split(annotation.fileTemplateSplit());
        Map<String, String> nameMappingMap = Arrays.stream(mappingSplit).map(x -> x.split(annotation.fileMappingSplit())).collect(Collectors.toMap(x -> x[0], x -> x[1]));
        // 创建excel
        Workbook workbook = WorkbookFactory.create(true);
        // 创建excel页
        Sheet sheet = workbook.createSheet(annotation.sheetName());
        // 创建数据页
        Sheet dataSheet = null;
        if (annotation.enableDataValidation()) {
            // 创建数据页
            dataSheet = workbook.createSheet(annotation.dataSheetName());
            // 设置隐藏属性
            workbook.setSheetHidden(workbook.getSheetIndex(dataSheet), annotation.datasheetHidden());
        }
        Map<String, Field> fieldMap = ReflectionUtil.getFieldMap(object);
        int rowIndex = 0, colIndex = 0;
        // 填充表头
        for (String fieldName : split) {
            if (annotation.enableDataValidation()) {
                createColumnValidation(split, fieldMap.get(fieldName), workbook, sheet, dataSheet, colIndex, annotation.showErrorBox());
            }
            createCell(sheet, rowIndex, colIndex++, nameMappingMap.get(fieldName));
        }
        ++rowIndex;
        colIndex = 0;
        for (Object data : collection) {
            for (String fieldName : split) {
                Field field = fieldMap.get(fieldName);
                field.setAccessible(true);
                try {
                    Object val = field.get(data);
                    // 创建完之后列需要忘后移动 所以需要加一
                    createCell(sheet, rowIndex, colIndex++, val);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
            ++rowIndex;
            colIndex = 0;
        }
        FileOutputStream out = new FileOutputStream(annotation.fileName());
        workbook.write(out);
        out.close();
        return annotation.fileName();
    }

    private static void createColumnValidation(String[] split, Field field, Workbook workbook, Sheet sheet, Sheet dataSheet, int colIndex, boolean showErrorBox) {
        if (field == null || dataSheet == null) {
            return;
        }
        field.setAccessible(true);
        ExcelValidation excelValidation = field.getAnnotation(ExcelValidation.class);
        if (excelValidation == null) {
            return;
        }
        String datasourceMethod = excelValidation.datasourceMethod();
        Method method = ReflectionUtil.getMethod(datasourceMethod);
        Object invoke;
        try {
            invoke = method.invoke(null);
        } catch (IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            return;
        }
        String formulaIndirectFormat = "=INDIRECT(%s!$%s$%s)";
        // 判断是否有前置字段
        if (StringUtils.isBlank(excelValidation.beforeFieldName())) {
            if (!(invoke instanceof Collection)) {
                return;
            }
            Collection collection = (Collection) invoke;
            createNameManage(workbook, dataSheet, field.getName(), collection, colIndex);
            String formulaIndirect = String.format(formulaIndirectFormat, dataSheet.getSheetName(), getCellColumnFlag(1), colIndex + 1);
            createDataValidate(sheet, formulaIndirect, excelValidation.validationType(), excelValidation.firstRow(), excelValidation.lastRow(), colIndex, colIndex, showErrorBox);
        } else {
            if (!(invoke instanceof Map)) {
                return;
            }
            Map<String, Collection> map = (Map<String, Collection>) invoke;
            map.forEach((k, v) -> createNameManage(workbook, dataSheet, k, v, colIndex));
            int beforeColIndex = 0;
            for (int i = 0; i < split.length; i++) {
                if (split[i].equals(excelValidation.beforeFieldName())) {
                    beforeColIndex = i;
                }
            }
            for (int rowIndex = excelValidation.firstRow(); rowIndex <= excelValidation.lastRow(); rowIndex++) {
                String formulaIndirect = String.format(formulaIndirectFormat, sheet.getSheetName(), getCellColumnFlag(beforeColIndex + 1), rowIndex + 1);
                createDataValidate(sheet, formulaIndirect, excelValidation.validationType(), rowIndex, rowIndex, colIndex, colIndex, showErrorBox);
            }
        }
    }

    private static void createDataValidate(Sheet sheet, String formula, int validationType, int firstRow, int lastRow, int firstCol, int lastCol, boolean showErrorBox) {
        CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
        XSSFDataValidationHelper xssfDataValidationHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
        XSSFDataValidationConstraint xssfDataValidationConstraint = new XSSFDataValidationConstraint(validationType, formula);
        DataValidation validation = xssfDataValidationHelper.createValidation(xssfDataValidationConstraint, cellRangeAddressList);
        validation.createErrorBox("输入有误!", "请选择下拉菜单里面的选项!");
        validation.setEmptyCellAllowed(false);
        validation.setShowErrorBox(showErrorBox);
        sheet.addValidationData(validation);
    }

    private static void createNameManage(Workbook workbook, Sheet sheet, String nameString, Collection data, final int rowIndex) {
        final int size = workbook.getAllNames().size();
        int columnIndex = 0;
        String format = "%s!$%s$%s:$%s$%s";
        // 创建名称管理器
        Name name = workbook.createName();
        name.setNameName(nameString);
        String cellColumnFlag = getCellColumnFlag(columnIndex + 2);
        int nameManageRegan = CollectionUtils.isEmpty(data) ? 1 : data.size() + 1;
        String nameManageScope = String.format(format, sheet.getSheetName(), cellColumnFlag, size + 1, getCellColumnFlag(nameManageRegan), size + 1);
        name.setRefersToFormula(nameManageScope);
        createCell(sheet, size, columnIndex, nameString);
        if (CollectionUtils.isNotEmpty(data)) {
            for (Object val : data) {
                createCell(sheet, size, ++columnIndex, String.valueOf(val));
            }
        }
    }

    private static String getCellColumnFlag(int num) {
        String colFiled = "";
        int chuNum = 0;
        int yuNum = 0;
        if (num >= 1 && num <= 26) {
            colFiled = doHandle(num);
        } else {
            chuNum = num / 26;
            yuNum = num % 26;
            yuNum = yuNum == 0 ? 1 : yuNum;
            colFiled += doHandle(chuNum);
            colFiled += doHandle(yuNum);
        }
        return colFiled;
    }

    private static String doHandle(int num) {
        return String.valueOf((char) (num + 64));
    }

    /**
     * 创建单元格
     *
     * @param sheet    页
     * @param rowIndex 行号,从0开始
     * @param colIndex 列号,从0开始
     * @param val      单元格的值
     */
    private static void createCell(Sheet sheet, int rowIndex, int colIndex, Object val) {
        Row row = sheet.getRow(rowIndex);
        if (row == null) {
            row = sheet.createRow(rowIndex);
        }
        Cell cell = row.getCell(colIndex);
        if (cell == null) {
            cell = row.createCell(colIndex);
            cell.setCellType(CellType.STRING);
        }
        cell.setCellValue(val == null ? "" : val.toString());
    }

}

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author ximu
 * @date 2021/8/29
 * @description 反射工具
 */
public class ReflectionUtil {

    public static Map<String, Field> getFieldMap(Object object) {
        Map<String, Field> fieldMap = new ConcurrentHashMap<>();
        refReflectionField(object, fieldMap);
        return fieldMap;
    }

    private static void refReflectionField(Object object, Map<String, Field> fieldMap) {
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            fieldMap.put(field.getName(), field);
        }
        Class<?> superclass = object.getClass().getSuperclass();
        if (superclass != null && !"java.lang.Object".equals(superclass.getName())) {
            refReflectionField(superclass, fieldMap);
        }
    }

    public static Method getMethod(String methodFullName) {
        int lastIndex = methodFullName.lastIndexOf('.');
        String className = methodFullName.substring(0, lastIndex);
        String methodName = methodFullName.substring(lastIndex + 1);
        Method method = null;
        try {
            Class<?> clazz = Class.forName(className);
            Method[] methods = clazz.getMethods();
            for (Method methodObject : methods) {
                if (methodObject.getName().equals(methodName)) {
                    method = methodObject;
                    break;
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return method;
    }

}

POM依赖

<dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <!--  lombok  -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <!-- poi 相关 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>4.1.2</version>
        </dependency>
    </dependencies>

这篇文章就写到这里了,虽然还有很多不足,主要是把文件上传服务器和兼容Spring没做,如果有同学需要的话,我可以改造一下。

欢迎给我留言,收到后会第一时间回复~

你可能感兴趣的:(java,java)