为什么要写这篇文章呢,因为看到了有人在提问如何用Java做Excel的级联菜单效果。帖子详情:http://spring4all.com/forum-post/575
我之前也遇到过同样的场景,当时查了很多文档才搞定,为了让更多人可以直接使用代码,节省时间,所以决定写这篇文章。
先上效果图,这里演示我做了三级下拉菜单的联动,不过我实现的版本可以支持任意级菜单的联动
Excel中有名称管理器的概念,什么意思呢。可以简单理解为,一个名称对应一组数据序列,举个例子,一个省会对映多个市,省名则是名称管理器的名字,对应的数据序列则是相关的市级城市名。
在创建下拉关系对映规则时,需要先根据数据的对映关系,创建所有名称管理器,然后再用数据有效性绑定名称管理器的名字就行了。
可以在公式->名称管理器内查看我这边创建的名称管理器和对映关系。
名称管理器创建好了,怎么使用呢?这就要用到Excel的数据有效性了。
你可以任意选中一个单元格,在菜单栏上点击数据->有效性
在数据有效性内设置如下
来源参数说明
=INDIRECT() :Excel内置函数,可以返回单元格的值引用
dataSheet:数据页的名称
$A$1:数据页的单元格位置
$A$1 是什么值呢,其实就是province
还记得名称管理器的province对应的数据序列吗,会被引用到这个单元格上,于是效果如下
好了,基本原理就是这样,接下来我们看下怎么使用代码生成
只需要用到两个注解,分别是ExcelFile、ExcelValidation
ExcelFile代表需要生成文件
目前Excel是在本地创建的,不过可以根据我的源码修改成上传到服务器,如果你需要帮助,可以给我留言
ExcelValidation是打在字段上的,用来标注字段是否需要生成校验。
目前获取数据源的的方式比较单一,只支持静态无参方法,如果你的项目整合了Spring,也可以改从Bean方法内获取数据,如果你需要帮助,可以给我留言
你可以选择copy源码或者打jar包的方式来使用,只需要在Excel实体对象上标注注解即可
我的项目结构
excel-example是例子,准备的一些数据
excel-extend是具体的实现
首先我在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 隐藏的
如果用户随意输入值的话,是可以强制校验的
通过 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;
}
}
<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没做,如果有同学需要的话,我可以改造一下。
欢迎给我留言,收到后会第一时间回复~