文章目录
一、简介
在使用EasyExcel设置下拉数据时,每次都要创建一个SheetWriteHandler
组件确实比较繁琐。为了优化这个过程,我们可以通过自定义注解来简化操作,使得只需要在需要添加下拉数据的字段上添加注解即可。
注解实现三种方式可供选择
- 方式一:固定值
- 方式二:动态获取复杂数据
- 方式三:通过码值获取码值表的数据列表
二、关键组件
1、ExcelSelected注解
- 用于在数据模型类中标注需要添加下拉列表的字段及其属性
- 三种方式都是通过此注解实现
/**
* 定义Excel列下拉列表属性的注解。
*/
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelSelected {
/**
* 方式一:固定的下拉选项
*/
String[] source() default {};
/**
* 方式二:提供动态下拉选项的类
*/
Class<? extends ExcelDynamicSelect>[] sourceClass() default {};
/**
* 方式三:基于码值从数据库查询数据
*/
String codeField() default "";
/**
* 下拉列表的起始行(默认从第二行开始)。
*/
int firstRow() default 1;
/**
* 下拉列表的结束行(默认到第65536行)。
*/
int lastRow() default 65536;
}
2、ExcelDynamicSelect接口(仅用于方式二)
方式二
定义动态获取下拉列表数据的规范- 实现该接口的类可以从数据库、外部服务或其他动态来源获取数据
/**
* 动态下拉列表数据提供者接口。
*/
public interface ExcelDynamicSelect {
/**
* 获取动态生成的下拉列表选项。
*
* @return 下拉选项数组。
*/
String[] getSource();
}
3、ExcelSelectedResolve类
- 负责解析
ExcelSelected
注解,获取下拉列表的具体数据
/**
* 根据 ExcelSelected 注解解析下拉列表数据源。
*/
@Data
@Slf4j
public class ExcelSelectedResolve {
/**
* 下拉选项数组。
*/
private String[] source;
/**
* 下拉列表的起始行。
*/
private int firstRow;
/**
* 下拉列表的结束行。
*/
private int lastRow;
/**
* 解析下拉列表数据来源
*
* @param excelSelected 下拉框注解对象
* @return 下拉框选项数组
*/
public String[] resolveSelectedSource(ExcelSelected excelSelected) {
if (excelSelected == null) {
return null;
}
// 方式一:获取固定下拉框的内容
String[] source = excelSelected.source();
if (source.length > 0) {
return source;
}
// 方式二:获取动态下拉框的内容
Class<? extends ExcelDynamicSelect>[] classes = excelSelected.sourceClass();
if (classes.length > 0) {
try {
ExcelDynamicSelect excelDynamicSelect = classes[0].newInstance();
String[] dynamicSelectSource = excelDynamicSelect.getSource();
if (dynamicSelectSource != null && dynamicSelectSource.length > 0) {
return dynamicSelectSource;
}
} catch (InstantiationException | IllegalAccessException e) {
log.error("解析动态下拉框数据异常", e);
}
}
// 方式三:获取码值下拉数据(动态下拉)
String codeField = excelSelected.codeField();
if (ObjectUtils.isNotEmpty(codeField)) {
try {
// 这里就是通过码值查询码值表,写死了,每次传码值查询即可
String[] codeFieldSource = SpringUtil.getBean(xxxService.class)
.selectByCode(codeField);
if (ObjectUtils.isNotEmpty(codeFieldSource)) {
return codeFieldSource;
}
} catch (Exception e) {
log.error("解析动态下拉框(码值)数据异常", e);
}
}
return null;
}
}
4、SelectedSheetWriteHandler类
- SheetWriteHandler实现类,在Sheet创建后设置下拉列表
- 在隐藏的sheet中存储下拉选项,然后设置数据验证以实现下拉功能
- 最后这里添加了
阻止输入非下拉选项的值
的校验
/**
* 处理Excel下拉列表的SheetWriteHandler实现类。
*/
@Slf4j
@Data
public class SelectedSheetWriteHandler implements SheetWriteHandler {
// 存储列索引与对应下拉列表解析器的映射
private Map<Integer, ExcelSelectedResolve> selectedMap = new HashMap<>();
/**
* 构造方法,解析表头类中的下拉列表注解信息。
*
* @param head 表头类。
*/
public SelectedSheetWriteHandler(Class<?> head) {
// 获取所有声明的字段
Field[] fields = head.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
// 获取 ExcelSelected 注解
ExcelSelected selected = field.getAnnotation(ExcelSelected.class);
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
if (selected != null) {
ExcelSelectedResolve resolve = new ExcelSelectedResolve();
// 解析下拉列表数据源
String[] source = resolve.resolveSelectedSource(selected);
if (source != null && source.length > 0) {
resolve.setSource(source);
resolve.setFirstRow(selected.firstRow());
resolve.setLastRow(selected.lastRow());
// 使用注解中的索引或字段顺序作为列索引
if (property != null && property.index() >= 0) {
selectedMap.put(property.index(), resolve);
} else {
selectedMap.put(i, resolve);
}
}
}
}
}
/**
* 在创建Sheet之前调用的方法。
*/
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
// 此处无需操作,保持空实现
}
/**
* 在Sheet创建后调用的方法,用于设置Excel下拉列表。
*
* @param writeWorkbookHolder 写入的工作簿持有者。
* @param writeSheetHolder 写入的Sheet持有者。
*/
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet sheet = writeSheetHolder.getSheet();
Workbook workbook = sheet.getWorkbook();
// SXSSFWorkbook 是 Apache POI 库中用于处理大文件的一种特殊工作簿类型
SXSSFWorkbook sw = (SXSSFWorkbook) workbook;
// 1.创建一个隐藏的sheet,名称为hidden,用于存储下拉列表选项
String hiddenName = "hidden";
XSSFSheet hiddenSheet = sw.getXSSFWorkbook().createSheet(hiddenName);
// 将隐藏的sheet设置为不可见
workbook.setSheetHidden(workbook.getSheetIndex(hiddenName), true);
// 创建数据验证辅助器
DataValidationHelper helper = sheet.getDataValidationHelper();
// 为每个需要下拉列表的列创建数据验证
selectedMap.forEach((index, selectedResolve) -> {
// 设置下拉列表的范围:起始行,结束行,起始列,结束列
CellRangeAddressList rangeList = new CellRangeAddressList(
selectedResolve.getFirstRow(),
selectedResolve.getLastRow(),
index,
index
);
// 在隐藏的sheet中生成下拉列表选项值
String[] values = selectedResolve.getSource();
generateSelectValue(hiddenSheet, index, values);
// 获取Excel列标,例如A, B, AA
String excelLine = getExcelLine(index);
// 引用隐藏sheet中的单元格区域,例如hidden!$H$1:$H$50
String refers = hiddenName + "!$" + excelLine + "$1:$" + excelLine + "$" + values.length;
// 使用引用的内容作为下拉列表的值
DataValidationConstraint constraint = helper.createFormulaListConstraint(refers);
DataValidation validation = helper.createValidation(constraint, rangeList);
// 设置验证属性,阻止输入非下拉选项的值
validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
validation.setShowErrorBox(true);
validation.setSuppressDropDownArrow(true);
validation.createErrorBox("提示", "请输入下拉选项中的内容");
// 将验证添加到当前的sheet中
sheet.addValidationData(validation);
});
}
/**
* 获取Excel列标(例如:A-Z, AA-ZZ)。
*
* @param num 列索引,从0开始。
* @return Excel列标字符串。
*/
public static String getExcelLine(int num) {
StringBuilder line = new StringBuilder();
// 计算列标,使用字母表示,例如 A, B, ..., Z, AA, AB, ...
int first = num / 26;
int second = num % 26;
if (first > 0) {
line.append((char) ('A' + first - 1));
}
line.append((char) ('A' + second));
return line.toString();
}
/**
* 在隐藏的sheet中生成下拉列表选项值。
*
* @param sheet 隐藏的sheet对象。
* @param col 列索引。
* @param values 下拉列表选项值数组。
*/
private void generateSelectValue(Sheet sheet, int col, String[] values) {
// 将下拉列表选项值写入隐藏的sheet中,每个选项值占用一行
for (int i = 0, length = values.length; i < length; i++) {
Row row = sheet.getRow(i);
if (row == null) {
row = sheet.createRow(i);
}
// 在指定列中创建单元格并设置下拉列表选项值
row.createCell(col).setCellValue(values[i]);
}
}
}
三、实际应用
- 包含三种方式,固定值、动态获取、码值数据库获取
@Data
public class Employee {
@ExcelProperty(value = "用户编号")
private Integer id;
@ExcelProperty(value = "姓名")
private String name;
@ExcelProperty(value = "性别")
@ExcelSelected(source = {"男", "女"})
private String gender;
@ExcelProperty(value = "职位")
@ExcelSelected(sourceClass = {PositionDynamicSelect.class})
private String position;
@ExcelProperty(value = "国家")
@ExcelSelected(codeField = "country_code")
private String country;
}
- 方式二的动态获取数据
public class PositionDynamicSelect implements ExcelDynamicSelect {
@Override
public String[] getSource() {
// 动态生成职位列表
return new String[]{"软件工程师", "项目经理", "人事专员", "财务分析师"};
}
}
- 测试类
public class EmployeeExcelTest {
public static void main(String[] args) {
String fileName = "/Users/xuchang/Documents/employee.xlsx";
EasyExcel.write(fileName, Employee.class)
.registerWriteHandler(new SelectedSheetWriteHandler(Employee.class))
.sheet().doWrite((Collection<?>) null);
}
}
- 下拉效果
- 输入非下拉框数据效果
总结
- 方式一只需要添加注解
@ExcelSelected(source = {"x1", "x2"})
即可 - 方式二在查询复杂的情况下使用,每个下拉都需要创建一个ExcelDynamicSelect的实现类,并添加注解
@ExcelSelected(sourceClass = {xxx.class})
- 方式三只需要添加注解
@ExcelSelected(codeField = "xxx_code")
,所有系统应该都有码值表,在ExcelSelectedResolve类中已写好通过码值查询数据的方法 - 同样也支持@ExcelSelected注解的扩展,添加属性,然后在ExcelSelectedResolve类中去添加获取下拉数据的方法。