一句话总结

Excel 导出、导入时,根据注解自动添加单元格验证规则,避免用户因填写错误的枚举字段而反复修改 Excel

需求背景

对于 Java Web 项目,总是不可避免的出现 Excel 导入、导出的需求,而 Excel 导入导出时,枚举字段和枚举值的映射是非常常见的一种情况

例如:下面这张示例中的性别列

数据库表结构:

Excel 中用户需要输入:男,女,未知

常见的 Excel 框架都已经覆盖了枚举映射的功能,例如:EasyPOI

但是这种操作方式对于用户来说,并不是很方便,试想一下:假如用户在性别列输入了:男性,最终的结果一般就是程序抛出异常,用户得到提示:性别输入有误,贴心的开发者可能会加上:请输入 男 女 未知,做的更好一些的 可能在列头添加标签提示:该列仅能输入 男 女 未知,但是这种弱限制也无法从根本上解决问题

更好一点的解决方案是:利用 Excel 的数据验证功能,把单元格加上规则校验,让用户只能输入正确的枚举值,避免因一次输入错误而反复返工,浪费用户的时间和好心情

当用户输入了非枚举值之后,Excel 会提示用户输入不合规,禁止用户保存

这样的交互就能从源头保证用户输入正确的值

那这么友好的设计,在 Java 中如何能方便且可扩展性更强的实现呢?

需求实现

我这边的实现是基于 EasyPOI + 注解(EasyPOI 转换映射关系注解复用) + 反射 实现的,解决了以上需求痛点的同时,可以满足代码一处修改,多个功能都生效的目的

代码仓库

GayHub

实体类

@Excel 注解中的 replace 属性,该属性是 EasyPOI 用来做字段映射的,我这里复用他做 Excel 验证的可选项,另外一个就是 orderNum 属性,用该值来自动获取某个字段在 Excel 中的列的位置

@Data
public class Human extends BaseEntity { private Long id; @Excel(name = "姓名", orderNum = "1", width = 15)
private String name; @Excel(name = "年龄", orderNum = "2", width = 15)
private Integer age; @Excel(name = "性别", replace = {"男_1", "女_2", "未知_3"}, orderNum = "3", width = 15)
private Integer gender;
}

获取列名和列位置的映射

该类在初始化时,需要指定当前导出 Excel 对应的实体类的类类型,然后通过遍历类中字段的注解,生成字段和列排序(位置)的映射关系

public class FieldOrderMappingHelper<T> {
/**
* 支持的最大字段数
*/
private final static int MAX_LIST_SIZE = 26; public FieldOrderMappingHelper(Class<T> pojo) {
this.pojo = pojo;
initMap();
} /**
* 解析注释的 pojo 对象
*/
private Class<T> pojo; /**
* 字段和序号的映射关系
*/
private HashMap<String, Integer> fieldAndOrderMap; /**
* 功能:初始化类的字段内容,建立字段和序号以及字段和 excel 列名的映射关系
*
* @author kangshuai@gridsum.com
* @date 2021/4/9 12:06
*/
private void initMap() {
HashMap<String, Integer> fieldAndOrderMap = new HashMap<>(16);
HashSet<Integer> existOrderNumSet = new HashSet<>(16); List<FiledAndOrder> list = new ArrayList<>();
list = initList(list, pojo);
if (list.size() > MAX_LIST_SIZE) {
throw new RuntimeException(pojo.getName() + "目前最大支持 26 个字段,26+ 需要改代码");
} // 排序
list.sort(Comparator.comparing(FiledAndOrder::getOrder)); for (int i = 0; i < list.size(); i++) {
if (existOrderNumSet.contains(list.get(i).getOrder())) {
throw new RuntimeException(pojo.getName() + "类内部或与父类字段中存在重复的 excel 排序,请修改");
}
existOrderNumSet.add(list.get(i).getOrder());
fieldAndOrderMap.put(list.get(i).getFiledName(), i);
}
this.fieldAndOrderMap = fieldAndOrderMap;
} /**
* 功能:初始化类的字段信息,转换成 ArrayList
*
* @return java.util.List<com.gridsum.ad.ooh.project.entity.FiledAndOrder>
* @author kangshuai@gridsum.com
* @date 2021/4/9 12:09
*/
private List<FiledAndOrder> initList(List<FiledAndOrder> list, Class<?> pojoClass) {
if (Object.class.equals(pojoClass)) {
return list;
}
Field[] fields = pojoClass.getDeclaredFields();
for (Field f : fields) {
// 找到所有加了 Excel 注解的字段
Excel annotation = f.getAnnotation(Excel.class);
if (annotation == null) {
continue;
}
// 过滤隐藏行
if (annotation.isColumnHidden()) {
continue;
}
FiledAndOrder filedAndOrder = new FiledAndOrder(f.getName(), Integer.parseInt(annotation.orderNum()));
list.add(filedAndOrder);
}
// 递归查找父类
Class<?> superclass = pojoClass.getSuperclass();
return initList(list, superclass);
} public HashMap<String, Integer> getFieldAndOrderMap() {
return fieldAndOrderMap;
}
}

设置验证规则

setValidation 方法有两个参数,第一个是导出 Excel 对应的实体类的类类型,第二个是 FieldOrderMappingHelper.getFieldAndOrderMap() 获取到的列名和排序映射,该类通过反射字段上的注解,自动为生成的 workbook 添加验证规则

public class ExcelStyleHelper {

    /**
* 添加列值验证的最小行
*/
public static final int EXCEL_VALID_ROW_MIN = 1;
/**
* 添加列值验证的最大行
*/
public static final int EXCEL_VALID_ROW_MAX = (2 << 15) - 1;
/**
* Excel 对象
*/
private Workbook workbook;
/**
* Sheet 页,默认取第一个 sheet 页
*/
private Sheet sheet; public ExcelStyleHelper(Workbook workbook) {
this.workbook = workbook;
this.sheet = workbook.getSheetAt(0);
} /**
* 功能:单元格添加下拉框,仅支持 xls
*
* @author kangshuai@gridsum.com
* @date 2021/4/8 18:55
*/
public void setValidation(Class<?> pojoClass, HashMap<String, Integer> map) {
// 递归到 Object 就停下
if (Object.class.equals(pojoClass)) {
return;
}
// 获取所有的字段
Field[] fields = pojoClass.getDeclaredFields();
for (Field field : fields) {
Excel annotation = field.getAnnotation(Excel.class);
if (annotation == null) {
continue;
}
String[] replace = annotation.replace();
if (replace.length == 0) {
continue;
}
String[] textList = new String[replace.length];
for (int i = 0; i < replace.length; i++) {
textList[i] = replace[i].split("_")[0];
}
// 根据字段名获取他在 excel 中的列数(结合 excel 注解中的排序)
Integer col = map.get(field.getName());
setValid(textList, col, col);
}
// 递归父类的注解
Class<?> superclass = pojoClass.getSuperclass();
setValidation(superclass, map);
} /**
* 功能:设置验证区间
*
* @author kangshuai@gridsum.com
* @date 2021/4/9 15:11
*/
private void setValid(String[] textList, int firstCol, int endCol) {
// 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
CellRangeAddressList regions = new CellRangeAddressList(EXCEL_VALID_ROW_MIN, EXCEL_VALID_ROW_MAX, firstCol, endCol);
// 加载下拉列表内容
DVConstraint constraint = DVConstraint.createExplicitListConstraint(textList);
// 数据有效性对象
HSSFDataValidation dataList = new HSSFDataValidation(regions, constraint);
sheet.addValidationData(dataList);
}
}

示例导出代码

控制层代码如下

@RestController
public class ExcelExportController { @GetMapping("excel")
public void excelExport(HttpServletResponse response) throws Exception {
List<Human> humanList = new ArrayList<>(); doWriteListToResponse(humanList, Human.class, response, "测试 Sheet", "测试 Excel.xls");
} /**
* 功能:将结果写入输出流
*
* @author kangshuai@gridsum.com
* @date 2021/4/14 14:46
*/
public <T> void doWriteListToResponse(List<T> list, Class<T> exportType, HttpServletResponse response, String sheetName, String excelName) throws IOException {
ExportParams ex = new ExportParams(null, sheetName, ExcelType.HSSF);
// 创建导出对象
Workbook workbook = ExcelExportUtil.exportExcel(ex, exportType, list);
// 初始化工具类
HashMap<String, Integer> map = new FieldOrderMappingHelper<>(exportType).getFieldAndOrderMap();
ExcelStyleHelper styleHelper = new ExcelStyleHelper(workbook);
// 添加规则
styleHelper.setValidation(exportType, map);
// 写入输出流,忽略此处硬编码
response.setHeader("Content-Disposition", "attachment;filename=" + new String(excelName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
response.setCharacterEncoding("UTF-8");
response.setContentType("application/x-download");
workbook.write(response.getOutputStream());
}
}

导出 Excel 模板自动生成规则,避免用户来回修改的更多相关文章

  1. ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案 try.dot.net 的正确使用姿势 .Net NPOI 根据excel模板导出excel、直接生成excel .Net NPOI 上传excel文件、提交后台获取excel里的数据

    ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案   ASP.NET Core 从2.2版本开始,采用了一个新的名为Endpoint的路由方案,与原来的方案在使用上差别不 ...

  2. Chimm.Excel —— 使用Java 操作 excel 模板文件生成 excel 文档

    Chimm.Excel -- 设置模板,填充数据,就完事儿了~ _____ _ _ _____ _ / __ \ | (_) | ___| | | | / \/ |__ _ _ __ ___ _ __ ...

  3. T4 模板自动生成带注释的实体类文件

    T4 模板自动生成带注释的实体类文件 - 只需要一个 SqlSugar.dll 生成实体就是这么简单,只要建一个T4文件和 文件夹里面放一个DLL. 使用T4模板教程 步骤1 创建T4模板 如果你没有 ...

  4. python制作简单excel统计报表3之将mysql数据库中的数据导入excel模板并生成统计图

    python制作简单excel统计报表3之将mysql数据库中的数据导入excel模板并生成统计图 # coding=utf-8 from openpyxl import load_workbook ...

  5. .Net NPOI 根据excel模板导出excel、直接生成excel

    一.根据Excel模板导出excel 1.导入NPOI.dll  2.DAL中添加类ExportExcel.cs using NPOI.SS.UserModel; using System; usin ...

  6. 根据模板导出Excel报表并生成多个Sheet页

    因为最近用报表导出比较多,所有就提成了一个工具类,本工具类使用的场景为  根据提供的模板来导出Excel报表 并且可根据提供的模板Sheet页进行复制 从而实现多个Sheet页的需求, 使用本工具类时 ...

  7. 使用JSExcelXML.js导出Excel模板

      github地址:https://github.com/464884492/JSExcelXml 业务系统显示效果图 导出模板图 功能描述    世间万物总是相生相克,既然我们的客户要求有导出Ex ...

  8. shell脚本模板----自动生成开头注释信息

    每当我们新建一个shell脚本都要去写一些繁琐的注释信息,这会浪费掉我们很多的时间,有没有感觉很痛苦呢? 哈哈 下面给大家分享一个shell脚本的模板文件,把它拷贝到用户的家目录下并命名成  .vim ...

  9. C#导出 Excel 时, 生成 CheckBox 控件

    在使用 Microsoft.Office.Interop.Excel 组件导出Excel 表格时,要把导出前的  CheckBox 控件一同导出到 excel 表格中,对于这个功能 看似很简单,但 M ...

随机推荐

  1. <JVM下篇:性能监控与调优篇>01-概述篇-02-JVM监控及诊断工具-命令行篇

    笔记来源:尚硅谷JVM全套教程,百万播放,全网巅峰(宋红康详解java虚拟机) 同步更新:https://gitee.com/vectorx/NOTE_JVM https://codechina.cs ...

  2. featuretools的几个高级特性

    摘要:记录工作中用到的featuretools的部分高级特性. 1.防止信息泄露 在调用dfs时,将主表的观测时间列连同id列作为cutoff_time,可以在构造特征时自动将子表中在cutoff_t ...

  3. 数据人必读!玩转数据可视化用这个就够了——高德LOCA API 2.0升级来袭!

    引言 "一图胜千言",大数据时代来临,数据与人们生活密切相关.复杂难懂且体量庞大的数据给人的感觉总是冷冰冰的,让人难以获取到重点信息,也找不出规律和特征,数据价值发挥不出来.空间数 ...

  4. HashMap底层原理分析

    本文将从以下方面结合源码进行分析:自动扩容.初始化与懒加载.哈希计算.位运算(默认采用JDK1.8).   自动扩容 扩容操作发生在putVal最后部分,在增加元素后才判断是否需要扩容,如果超过阈值, ...

  5. Spring Cloud Alibaba(10)---Sentinel控制台搭建+整合SpringCloudAlibaba

    上一篇博客讲了Sentinel一些概念性的东西 Spring Cloud Alibaba(9)---Sentinel概述 这篇博客主要讲 Sentinel控制台搭建,和 整合SpringCloudAl ...

  6. Codeforces Round #688 (Div. 2)

    A. Cancel the Trains 题意:给定两个数组,找出这两个数组中有多少重复元素,然后输出 思路:直接找 代码: 1 #include<iostream> 2 #include ...

  7. RabbitMQ高级特性

    消息的可靠投递 在使用Rabbitmq的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景.Rabbitmq为我们提供了两种方式用来控制消息的投递可靠性模式 confirm确认模式 return ...

  8. Map&Set的理解

    Set子接口 特点:无序.无下标.元素不可重复. 方法:全部继承自Collection中的方法. Set实现类 HashSet: 基于HashCode实现了不重复. 当存入元素的哈希码相同时,会调用e ...

  9. 《SystemVerilog验证-测试平台编写指南》学习 - 第3章 过程语句和子程序

    <SystemVerilog验证-测试平台编写指南>学习 - 第3章 过程语句和子程序 3.1 过程语句 3.2 任务.函数以及void函数 3.3 任务和函数概述 3.4 子程序参数 3 ...

  10. k8s用 ConfigMap 管理配置(13)

    一.ConfigMap介绍 Secret 可以为 Pod 提供密码.Token.私钥等敏感数据:对于一些非敏感数据,比如应用的配置信息,则可以用 ConfigMap ConfigMap 的创建和使用方 ...