springboot3 使用 easyexcel 封装工具类实现 自定义表头 导出并实现 数据格式化转换 与 添加下拉框 操作
在现代企业应用中,数据导出功能是非常常见的需求。特别是在处理大量数据时,将数据导出为 excel
文件不仅方便用户查看和分析,还能提高数据处理的效率。apache poi
是一个常用的 java excel 处理库,但它在处理大数据量时性能较差。为此,阿里巴巴开源了 easyexcel
,这是一个基于 java
的简单、优雅的 excel
操作库,它在处理大数据量时表现优异。
本文将详细介绍如何使用 easyexcel
实现自定义表头导出,并实现数据格式化转换。
官网 : easyexcel
封装了很多导出方法针对不同的业务场景,这里直接展示自定义表头的代码 .
1. 使用示例
vo 示例(部分字段)
使用方法 export
用户自定义表头的 字段显示名称 , 需跟导出注解 @excelproperty
的 value
相对应 , 不然无法取到值
页面显示数据
导出 excel 数据
2. 工具类
/** * excel相关处理 * * @author 鲁子狄 */ @noargsconstructor(access = accesslevel.private) public class excelutil { /** * 导出 excel 文件 * * @param list 导出数据集合 * @param sheetname 工作表的名称 * @param heads 表头 * @param response 响应体 */ public static <t> void exportexcel(list<t> list, class<t> clazz, string sheetname, list<list<string>> heads, httpservletresponse response) { try { // 将对象列表转换为自定义的数据列表 list<list<object>> data = excelutil.convertobjectstoexcelrows(list, heads); // 重置响应体,设置响应头 excelutil.resetresponse(sheetname, response); // 获取响应输出流 servletoutputstream os = response.getoutputstream(); // 导出 excel 文件 excelutil.exportexcel(data, clazz, heads, sheetname, false, os, null); } catch (ioexception e) { throw new runtimeexception("导出 excel 异常"); } } /** * 重置响应体 */ private static void resetresponse(string sheetname, httpservletresponse response) throws unsupportedencodingexception { string filename = excelutil.encodingfilename(sheetname); fileutils.setattachmentresponseheader(response, filename); response.setcontenttype("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"); } /** * 导出 excel 文件 * * @param data 导出数据集合 * @param heads 表头 * @param sheetname 工作表的名称 * @param merge 是否合并单元格 * @param os 输出流 * @param options 级联下拉选内容 */ public static <t> void exportexcel(list<list<object>> data, class<t> clazz, list<list<string>> heads, string sheetname, boolean merge, outputstream os, list<dropdownoptions> options) { excelwritersheetbuilder builder = easyexcel.write(os, clazz) .autoclosestream(false) // 自动适配列宽 .registerwritehandler(new longestmatchcolumnwidthstylestrategy()) // 大数值自动转换,防止失真 .registerconverter(new excelbignumberconvert()) .sheet(sheetname) .head(heads); if (merge) { // 注册合并单元格处理器 builder.registerwritehandler(new cellmergestrategy(data, true)); } // 添加下拉框操作 builder.registerwritehandler(new exceldownhandler(options, heads)); // 写入数据 builder.dowrite(data); } /** * convertobjectstoexcelrows 将对象列表转换为 excel 行数据 * * @param list 对象集合 * @param heads 表头 * @return {@link list<list<object>>} excel 行数据 */ private static <t> list<list<object>> convertobjectstoexcelrows(list<t> list, list<list<string>> heads) { return list.stream() // 将对象转换为 map .map(excelutil::convertobjecttomap) .map(map -> { list<object> row = new arraylist<>(); for (list<string> header : heads) { // 假设表头只有一个元素 string key = header.get(0); // 根据表头获取对应的值 row.add(map.get(key)); } return row; }) .tolist(); } /** * convertobjecttomap 将对象转换为 map,键为 @excelproperty 注解中的列标题,值为字段值。 * * @param vo 对象 * @return {@link map<string,object>} 包含列标题和字段值的 map */ private static map<string, object> convertobjecttomap(object vo) { // 获取对象的所有字段 return arrays.stream(vo.getclass().getdeclaredfields()) // 过滤带有 @excelproperty 注解的字段 .filter(field -> field.isannotationpresent(excelproperty.class)) .map(field -> { try { // 设置字段可访问 field.setaccessible(true); // 获取字段名和值 string fieldname = field.getdeclaredannotation(excelproperty.class).value()[0]; object fieldvalue = field.get(vo); // 检查是否有 @exceldictformat 注解 exceldictformat dictformat = field.getdeclaredannotation(exceldictformat.class); if (dictformat != null && fieldvalue != null) { fieldvalue = convertexceldictformat(fieldvalue, dictformat); } return new abstractmap.simpleentry<>(fieldname, fieldvalue); } catch (illegalaccessexception e) { throw new illegalstateexception("获取字段值失败: " + field.getname(), e); } }) // 过滤掉值为 null 的条目 .filter(entry -> entry.getvalue() != null) .collect(collectors.tomap( // 键为字段名 abstractmap.simpleentry::getkey, // 值为字段值 abstractmap.simpleentry::getvalue, // 如果键重复,保留第一个值 (existing, replacement) -> existing, // 使用 hashmap 存储结果 hashmap::new )); } /** * converttoexceldata 根据 @exceldictformat 注解转换数据 * * @param value 字段值 * @param dictformat 注解信息 * @return 转换后的值 */ private static string convertexceldictformat(object value, exceldictformat dictformat) { string type = dictformat.dicttype(); string label; if (stringutils.isblank(type)) { label = convertbyexp(value.tostring(), dictformat.readconverterexp(), dictformat.separator()); } else { label = springutils.getbean(dictservice.class).getdictlabel(type, value.tostring(), dictformat.separator()); } return label; } } /** * 解析导出值 0=男,1=女,2=未知 * * @param propertyvalue 参数值 * @param converterexp 翻译注解 * @param separator 分隔符 * @return 解析后值 */ public static string convertbyexp(string propertyvalue, string converterexp, string separator) { stringbuilder propertystring = new stringbuilder(); string[] convertsource = converterexp.split(stringutils.separator); for (string item : convertsource) { string[] itemarray = item.split("="); if (stringutils.containsany(propertyvalue, separator)) { for (string value : propertyvalue.split(separator)) { if (itemarray[0].equals(value)) { propertystring.append(itemarray[1] + separator); break; } } } else { if (itemarray[0].equals(propertyvalue)) { return itemarray[1]; } } } return stringutils.stripend(propertystring.tostring(), separator); }
注意
- 在使用
easyexcel
导出 excel 文件时,converttoexceldata
方法的调用与否取决于你如何传递数据以及数据的结构。 - 传入单个对象或对象列表时
- 当你传入一个对象列表(如
list<user>
),easyexcel
会根据对象的属性和字段上的注解来决定如何处理每个字段。如果某个字段上有@exceldictformat
注解,并且你已经注册了相应的转换器(如exceldictconvert
),那么easyexcel
会在导出时调用converttoexceldata
方法。- 传入嵌套列表
list<list<object>>
时 (自定义表头使用) - 当你传入一个嵌套列表
list<list<object>>
时,easyexcel
不会自动解析对象的属性和字段注解。这是因为list<list<object>>
是一个二维数组,每个元素都是一个object
,easyexcel
无法知道这些object
应该如何转换。 - 因为自定义表头使用
list<list<object>>
, 所以我的解决方案(convertobjecttomap
方法)是:map
表示动态数据:将list<list<object>>
转换为list<map<string, object>>
,其中键是字段名,值是字段值。- 手动调用转换器:在导出数据之前,手动调用
converttoexceldata
方法进行数据转换。
- 传入嵌套列表
3. excelbignumberconvert 大数值自动转换,防止失真
/** * 大数值转换 * excel 数值长度位15位 大于15位的数值转换位字符串 */ @slf4j public class excelbignumberconvert implements converter<long> { @override public class<long> supportjavatypekey() { return long.class; } @override public celldatatypeenum supportexceltypekey() { return celldatatypeenum.string; } @override public long converttojavadata(readcelldata<?> celldata, excelcontentproperty contentproperty, globalconfiguration globalconfiguration) { return convert.tolong(celldata.getdata()); } @override public writecelldata<object> converttoexceldata(long object, excelcontentproperty contentproperty, globalconfiguration globalconfiguration) { if (objectutil.isnotnull(object)) { string str = convert.tostr(object); if (str.length() > 15) { return new writecelldata<>(str); } } writecelldata<object> celldata = new writecelldata<>(new bigdecimal(object)); celldata.settype(celldatatypeenum.number); return celldata; } }
4. cellmergestrategy 注册合并单元格处理器
** * 列值重复合并策略 */ @slf4j public class cellmergestrategy extends abstractmergestrategy implements workbookwritehandler { private final list<cellrangeaddress> celllist; private final boolean hastitle; private int rowindex; public cellmergestrategy(list<?> list, boolean hastitle) { this.hastitle = hastitle; // 行合并开始下标 this.rowindex = hastitle ? 1 : 0; this.celllist = handle(list, hastitle); } @override protected void merge(sheet sheet, cell cell, head head, integer relativerowindex) { //单元格写入了,遍历合并区域,如果该cell在区域内,但非首行,则清空 final int rowindex = cell.getrowindex(); if (collutil.isnotempty(celllist)){ for (cellrangeaddress celladdresses : celllist) { final int firstrow = celladdresses.getfirstrow(); if (celladdresses.isinrange(cell) && rowindex != firstrow){ cell.setblank(); } } } } @override public void afterworkbookdispose(final workbookwritehandlercontext context) { //当前表格写完后,统一写入 if (collutil.isnotempty(celllist)){ for (cellrangeaddress item : celllist) { context.getwritecontext().writesheetholder().getsheet().addmergedregion(item); } } } @sneakythrows private list<cellrangeaddress> handle(list<?> list, boolean hastitle) { list<cellrangeaddress> celllist = new arraylist<>(); if (collutil.isempty(list)) { return celllist; } field[] fields = reflectutils.getfields(list.get(0).getclass(), field -> !"serialversionuid".equals(field.getname())); // 有注解的字段 list<field> mergefields = new arraylist<>(); list<integer> mergefieldsindex = new arraylist<>(); for (int i = 0; i < fields.length; i++) { field field = fields[i]; if (field.isannotationpresent(cellmerge.class)) { cellmerge cm = field.getannotation(cellmerge.class); mergefields.add(field); mergefieldsindex.add(cm.index() == -1 ? i : cm.index()); if (hastitle) { excelproperty property = field.getannotation(excelproperty.class); rowindex = math.max(rowindex, property.value().length); } } } map<field, repeatcell> map = new hashmap<>(); // 生成两两合并单元格 for (int i = 0; i < list.size(); i++) { for (int j = 0; j < mergefields.size(); j++) { field field = mergefields.get(j); object val = reflectutils.invokegetter(list.get(i), field.getname()); int colnum = mergefieldsindex.get(j); if (!map.containskey(field)) { map.put(field, new repeatcell(val, i)); } else { repeatcell repeatcell = map.get(field); object cellvalue = repeatcell.getvalue(); if (cellvalue == null || "".equals(cellvalue)) { // 空值跳过不合并 continue; } if (!cellvalue.equals(val)) { if ((i - repeatcell.getcurrent() > 1)) { celllist.add(new cellrangeaddress(repeatcell.getcurrent() + rowindex, i + rowindex - 1, colnum, colnum)); } map.put(field, new repeatcell(val, i)); } else if (i == list.size() - 1) { if (i > repeatcell.getcurrent() && ismerge(list, i, field)) { celllist.add(new cellrangeaddress(repeatcell.getcurrent() + rowindex, i + rowindex, colnum, colnum)); } } else if (!ismerge(list, i, field)) { if ((i - repeatcell.getcurrent() > 1)) { celllist.add(new cellrangeaddress(repeatcell.getcurrent() + rowindex, i + rowindex - 1, colnum, colnum)); } map.put(field, new repeatcell(val, i)); } } } } return celllist; } private boolean ismerge(list<?> list, int i, field field) { boolean ismerge = true; cellmerge cm = field.getannotation(cellmerge.class); final string[] mergeby = cm.mergeby(); if (strutil.isallnotblank(mergeby)) { //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真 for (string fieldname : mergeby) { final object valcurrent = reflectutil.getfieldvalue(list.get(i), fieldname); final object valpre = reflectutil.getfieldvalue(list.get(i - 1), fieldname); if (!objects.equals(valpre, valcurrent)) { //依赖字段如有任一不等值,则标记为不可合并 ismerge = false; } } } return ismerge; } @data @allargsconstructor static class repeatcell { private object value; private int current; } }
5. exceldownhandler 添加下拉框操作
/** * <h1>excel表格下拉选操作</h1> * 考虑到下拉选过多可能导致excel打开缓慢的问题,只校验前1000行 * <p> * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出 */ @slf4j public class exceldownhandler implements sheetwritehandler { /** * excel表格中的列名英文 * 仅为了解析列英文,禁止修改 */ private static final string excel_column_name = "abcdefghijklmnopqrstuvwxyz"; /** * 单选数据sheet名 */ private static final string options_sheet_name = "options"; /** * 联动选择数据sheet名的头 */ private static final string linked_options_sheet_name = "linkedoptions"; /** * 下拉可选项 */ private final list<dropdownoptions> dropdownoptions; /** * 表头 */ private final list<list<string>> heads; /** * 当前单选进度 */ private int currentoptionscolumnindex; /** * 当前联动选择进度 */ private int currentlinkedoptionssheetindex; private final dictservice dictservice; public exceldownhandler(list<dropdownoptions> options, list<list<string>> heads) { dropdownoptions = options; this.heads = heads; currentoptionscolumnindex = 0; currentlinkedoptionssheetindex = 0; dictservice = springutils.getbean(dictservice.class); } /** * <h2>开始创建下拉数据</h2> * 1.通过解析传入的@excelproperty同级是否标注有@dropdown选项 * 如果有且设置了value值,则将其直接置为下拉可选项 * <p> * 2.或者在调用excelutil时指定了可选项,将依据传入的可选项做下拉 * <p> * 3.二者并存,注意调用方式 */ @override public void aftersheetcreate(writeworkbookholder writeworkbookholder, writesheetholder writesheetholder) { sheet sheet = writesheetholder.getsheet(); // 开始设置下拉框 hssfworkbook datavalidationhelper helper = sheet.getdatavalidationhelper(); workbook workbook = writeworkbookholder.getworkbook(); fieldcache fieldcache = classutils.declaredfields(writeworkbookholder.getclazz(), writeworkbookholder); for (map.entry<integer, fieldwrapper> entry : fieldcache.getsortedfieldmap().entryset()) { integer index = entry.getkey(); fieldwrapper wrapper = entry.getvalue(); field field = wrapper.getfield(); // 循环实体中的每个属性 // 可选的下拉值 list<string> options = new arraylist<>(); if (field.isannotationpresent(exceldictformat.class)) { // 如果指定了@exceldictformat,则使用字典的逻辑 exceldictformat format = field.getdeclaredannotation(exceldictformat.class); string dicttype = format.dicttype(); string converterexp = format.readconverterexp(); if (stringutils.isnotblank(dicttype)) { // 如果传递了字典名,则依据字典建立下拉 collection<string> values = optional.ofnullable(dictservice.getalldictbydicttype(dicttype)) .orelsethrow(() -> new serviceexception(string.format("字典 %s 不存在", dicttype))) .values(); options = new arraylist<>(values); } else if (stringutils.isnotblank(converterexp)) { // 如果指定了确切的值,则直接解析确切的值 list<string> strlist = stringutils.splitlist(converterexp, format.separator()); options = streamutils.tolist(strlist, s -> stringutils.split(s, "=")[1]); } } else if (field.isannotationpresent(excelenumformat.class)) { // 否则如果指定了@excelenumformat,则使用枚举的逻辑 excelenumformat format = field.getdeclaredannotation(excelenumformat.class); list<object> values = enumutil.getfieldvalues(format.enumclass(), format.textfield()); options = streamutils.tolist(values, string::valueof); } if (objectutil.isnotempty(options)) { // 如过自定义表头不为空 if (collutil.isnotempty(heads)) { // 获取对应的下拉表头的下标 index = intstream.range(0, heads.size()) .filter(i -> wrapper.getheads()[0].equals(heads.get(i).get(0))) .findfirst().orelse(0); } // 仅当下拉可选项不为空时执行 if (options.size() > 20) { // 这里限制如果可选项大于20,则使用额外表形式 dropdownwithsheet(helper, workbook, sheet, index, options); } else { // 否则使用固定值形式 dropdownwithsimple(helper, sheet, index, options); } } } if (collutil.isempty(dropdownoptions)) { return; } dropdownoptions.foreach(everyoptions -> { // 如果传递了下拉框选择器参数 if (!everyoptions.getnextoptions().isempty()) { // 当二级选项不为空时,使用额外关联表的形式 dropdownlinkedoptions(helper, workbook, sheet, everyoptions); } else if (everyoptions.getoptions().size() > 10) { // 当一级选项参数个数大于10,使用额外表的形式 dropdownwithsheet(helper, workbook, sheet, everyoptions.getindex(), everyoptions.getoptions()); } else if (!everyoptions.getoptions().isempty()) { // 当一级选项个数不为空,使用默认形式 dropdownwithsimple(helper, sheet, everyoptions.getindex(), everyoptions.getoptions()); } }); } /** * <h2>简单下拉框</h2> * 直接将可选项拼接为指定列的数据校验值 * * @param celindex 列index * @param value 下拉选可选值 */ private void dropdownwithsimple(datavalidationhelper helper, sheet sheet, integer celindex, list<string> value) { if (objectutil.isempty(value)) { return; } markoptionstosheet(helper, sheet, celindex, helper.createexplicitlistconstraint(arrayutil.toarray(value, string.class))); } /** * <h2>额外表格形式的级联下拉框</h2> * * @param options 额外表格形式存储的下拉可选项 */ private void dropdownlinkedoptions(datavalidationhelper helper, workbook workbook, sheet sheet, dropdownoptions options) { string linkedoptionssheetname = string.format("%s_%d", exceldownhandler.linked_options_sheet_name, currentlinkedoptionssheetindex); // 创建联动下拉数据表 sheet linkedoptionsdatasheet = workbook.createsheet(workbookutil.createsafesheetname(linkedoptionssheetname)); // 将下拉表隐藏 workbook.setsheethidden(workbook.getsheetindex(linkedoptionsdatasheet), true); // 完善横向的一级选项数据表 list<string> firstoptions = options.getoptions(); map<string, list<string>> secoundoptionsmap = options.getnextoptions(); // 创建名称管理器 name name = workbook.createname(); // 设置名称管理器的别名 name.setnamename(linkedoptionssheetname); // 以横向第一行创建一级下拉拼接引用位置 string firstoptionsfunction = string.format("%s!$%s$1:$%s$1", linkedoptionssheetname, exceldownhandler.getexcelcolumnname(0), exceldownhandler.getexcelcolumnname(firstoptions.size()) ); // 设置名称管理器的引用位置 name.setreferstoformula(firstoptionsfunction); // 设置数据校验为序列模式,引用的是名称管理器中的别名 markoptionstosheet(helper, sheet, options.getindex(), helper.createformulalistconstraint(linkedoptionssheetname)); for (int columindex = 0; columindex < firstoptions.size(); columindex++) { // 先提取主表中一级下拉的列名 string firstoptionscolumnname = exceldownhandler.getexcelcolumnname(columindex); // 一次循环是每一个一级选项 int finali = columindex; // 本次循环的一级选项值 string thisfirstoptionsvalue = firstoptions.get(columindex); // 创建第一行的数据 optional.ofnullable(linkedoptionsdatasheet.getrow(0)) // 如果不存在则创建第一行 .orelseget(() -> linkedoptionsdatasheet.createrow(finali)) // 第一行当前列 .createcell(columindex) // 设置值为当前一级选项值 .setcellvalue(thisfirstoptionsvalue); // 第二行开始,设置第二级别选项参数 list<string> secondoptions = secoundoptionsmap.get(thisfirstoptionsvalue); if (collutil.isempty(secondoptions)) { // 必须保证至少有一个关联选项,否则将导致excel解析错误 secondoptions = collections.singletonlist("暂无_0"); } // 以该一级选项值创建子名称管理器 name sonname = workbook.createname(); // 设置名称管理器的别名 sonname.setnamename(thisfirstoptionsvalue); // 以第二行该列数据拼接引用位置 string sonfunction = string.format("%s!$%s$2:$%s$%d", linkedoptionssheetname, firstoptionscolumnname, firstoptionscolumnname, secondoptions.size() + 1 ); // 设置名称管理器的引用位置 sonname.setreferstoformula(sonfunction); // 数据验证为序列模式,引用到每一个主表中的二级选项位置 // 创建子项的名称管理器,只是为了使得excel可以识别到数据 string mainsheetfirstoptionscolumnname = exceldownhandler.getexcelcolumnname(options.getindex()); for (int i = 0; i < 100; i++) { // 以一级选项对应的主体所在位置创建二级下拉 string secondoptionsfunction = string.format("=indirect(%s%d)", mainsheetfirstoptionscolumnname, i + 1); // 二级只能主表每一行的每一列添加二级校验 marklinkedoptionstosheet(helper, sheet, i, options.getnextindex(), helper.createformulalistconstraint(secondoptionsfunction)); } for (int rowindex = 0; rowindex < secondoptions.size(); rowindex++) { // 从第二行开始填充二级选项 int finalrowindex = rowindex + 1; int finalcolumindex = columindex; row row = optional.ofnullable(linkedoptionsdatasheet.getrow(finalrowindex)) // 没有则创建 .orelseget(() -> linkedoptionsdatasheet.createrow(finalrowindex)); optional // 在本级一级选项所在的列 .ofnullable(row.getcell(finalcolumindex)) // 不存在则创建 .orelseget(() -> row.createcell(finalcolumindex)) // 设置二级选项值 .setcellvalue(secondoptions.get(rowindex)); } } currentlinkedoptionssheetindex++; } /** * <h2>额外表格形式的普通下拉框</h2> * 由于下拉框可选值数量过多,为提升excel打开效率,使用额外表格形式做下拉 * * @param celindex 下拉选 * @param value 下拉选可选值 */ private void dropdownwithsheet(datavalidationhelper helper, workbook workbook, sheet sheet, integer celindex, list<string> value) { // 创建下拉数据表 sheet simpledatasheet = optional.ofnullable(workbook.getsheet(workbookutil.createsafesheetname(exceldownhandler.options_sheet_name))) .orelseget(() -> workbook.createsheet(workbookutil.createsafesheetname(exceldownhandler.options_sheet_name))); // 将下拉表隐藏 workbook.setsheethidden(workbook.getsheetindex(simpledatasheet), true); // 完善纵向的一级选项数据表 for (int i = 0; i < value.size(); i++) { int finali = i; // 获取每一选项行,如果没有则创建 row row = optional.ofnullable(simpledatasheet.getrow(i)) .orelseget(() -> simpledatasheet.createrow(finali)); // 获取本级选项对应的选项列,如果没有则创建 cell cell = optional.ofnullable(row.getcell(currentoptionscolumnindex)) .orelseget(() -> row.createcell(currentoptionscolumnindex)); // 设置值 cell.setcellvalue(value.get(i)); } // 创建名称管理器 name name = workbook.createname(); // 设置名称管理器的别名 string namename = string.format("%s_%d", exceldownhandler.options_sheet_name, celindex); name.setnamename(namename); // 以纵向第一列创建一级下拉拼接引用位置 string function = string.format("%s!$%s$1:$%s$%d", exceldownhandler.options_sheet_name, exceldownhandler.getexcelcolumnname(currentoptionscolumnindex), exceldownhandler.getexcelcolumnname(currentoptionscolumnindex), value.size()); // 设置名称管理器的引用位置 name.setreferstoformula(function); // 设置数据校验为序列模式,引用的是名称管理器中的别名 markoptionstosheet(helper, sheet, celindex, helper.createformulalistconstraint(namename)); currentoptionscolumnindex++; } /** * 挂载下拉的列,仅限一级选项 */ private void markoptionstosheet(datavalidationhelper helper, sheet sheet, integer celindex, datavalidationconstraint constraint) { // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 cellrangeaddresslist addresslist = new cellrangeaddresslist(1, 1000, celindex, celindex); markdatavalidationtosheet(helper, sheet, constraint, addresslist); } /** * 挂载下拉的列,仅限二级选项 */ private void marklinkedoptionstosheet(datavalidationhelper helper, sheet sheet, integer rowindex, integer celindex, datavalidationconstraint constraint) { // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 cellrangeaddresslist addresslist = new cellrangeaddresslist(rowindex, rowindex, celindex, celindex); markdatavalidationtosheet(helper, sheet, constraint, addresslist); } /** * 应用数据校验 */ private void markdatavalidationtosheet(datavalidationhelper helper, sheet sheet, datavalidationconstraint constraint, cellrangeaddresslist addresslist) { // 数据有效性对象 datavalidation datavalidation = helper.createvalidation(constraint, addresslist); // 处理excel兼容性问题 if (datavalidation instanceof xssfdatavalidation) { //数据校验 datavalidation.setsuppressdropdownarrow(true); //错误提示 datavalidation.seterrorstyle(datavalidation.errorstyle.stop); datavalidation.createerrorbox("提示", "此值与单元格定义数据不一致"); datavalidation.setshowerrorbox(true); //选定提示 datavalidation.createpromptbox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败"); datavalidation.setshowpromptbox(true); sheet.addvalidationdata(datavalidation); } else { datavalidation.setsuppressdropdownarrow(false); } sheet.addvalidationdata(datavalidation); } /** * <h2>依据列index获取列名英文</h2> * 依据列index转换为excel中的列名英文 * <p>例如第1列,index为0,解析出来为a列</p> * 第27列,index为26,解析为aa列 * <p>第28列,index为27,解析为ab列</p> * * @param columnindex 列index * @return 列index所在得英文名 */ private static string getexcelcolumnname(int columnindex) { // 26一循环的次数 int columncirclecount = columnindex / 26; // 26一循环内的位置 int thiscirclecolumnindex = columnindex % 26; // 26一循环的次数大于0,则视为栏名至少两位 string columnprefix = columncirclecount == 0 ? strutil.empty : strutil.subwithlength(exceldownhandler.excel_column_name, columncirclecount - 1, 1); // 从26一循环内取对应的栏位名 string columnnext = strutil.subwithlength(exceldownhandler.excel_column_name, thiscirclecolumnindex, 1); // 将二者拼接即为最终的栏位名 return columnprefix + columnnext; } }
6. exceldictformat 字典格式化注解
/** * 字典格式化 */ @target({elementtype.field}) @retention(retentionpolicy.runtime) @inherited public @interface exceldictformat { /** * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) */ string dicttype() default ""; /** * 读取内容转表达式 (如: 0=男,1=女,2=未知) */ string readconverterexp() default ""; /** * 分隔符,读取字符串组内容 */ string separator() default stringutils.separator; }
7. exceldictconvert 字典格式化转换处理 (普通导出方法调用)
/** * 字典格式化转换处理 */ @slf4j public class exceldictconvert implements converter<object> { @override public class<object> supportjavatypekey() { return object.class; } @override public celldatatypeenum supportexceltypekey() { return null; } @override public object converttojavadata(readcelldata<?> celldata, excelcontentproperty contentproperty, globalconfiguration globalconfiguration) { exceldictformat anno = getannotation(contentproperty.getfield()); string type = anno.dicttype(); string label = celldata.getstringvalue(); string value; if (stringutils.isblank(type)) { value = excelutil.reversebyexp(label, anno.readconverterexp(), anno.separator()); } else { value = springutils.getbean(dictservice.class).getdictvalue(type, label, anno.separator()); } return convert.convert(contentproperty.getfield().gettype(), value); } @override public writecelldata<string> converttoexceldata(object object, excelcontentproperty contentproperty, globalconfiguration globalconfiguration) { if (objectutil.isnull(object)) { return new writecelldata<>(""); } exceldictformat anno = getannotation(contentproperty.getfield()); string type = anno.dicttype(); string value = convert.tostr(object); string label; if (stringutils.isblank(type)) { label = excelutil.convertbyexp(value, anno.readconverterexp(), anno.separator()); } else { label = springutils.getbean(dictservice.class).getdictlabel(type, value, anno.separator()); } return new writecelldata<>(label); } private exceldictformat getannotation(field field) { return annotationutil.getannotation(field, exceldictformat.class); } }
8. 后续修改
1. 添加大标题,自动合并单元格
a. 添加大标题
b. 修改取数据方法
修改取下标为1的元素
/** * convertobjectstoexcelrows 将对象列表转换为 excel 行数据 * * @param list 对象集合 * @param heads 表头 * @return {@link list<list<object>>} excel 行数据 */ private static <t> list<list<object>> convertobjectstoexcelrows(list<t> list, list<list<string>> heads) { return list.stream() // 将对象转换为 map .map(excelutil::convertobjecttomap) .map(map -> { list<object> row = new arraylist<>(); for (list<string> header : heads) { string key = header.get(1); // 根据表头获取对应的值 row.add(map.get(key)); } return row; }) .tolist(); }
c. 导出方法
d. 导出内容
2. 解决 can not find ‘converter’ support class localtime.
a. 原因
easyexcel
默认支持一些常见的数据类型,但对于 localtime
这样的类型,可能需要自定义一个转换器。
b. 解决方案
- 自定义
localtime
转换器 - 直接添加个数据转换 (因为是自定义表头,所以走此方法)
c.直接数据转换(因不同的java版本写法会有不同,我用的java17)
/** * converttoexceldata 数据转换 * * @param value 值 * @return {@link java.lang.object} */ private static object converttoexceldata(object value) { if (objectutil.isempty(value)) { return value; } if (value instanceof localtime localtime) { return localtime.format(datetimeformatter.ofpattern("hh:mm:ss")); } else if (value instanceof localdatetime localdatetime) { return localdatetime.format(datetimeformatter.ofpattern("yyyy-mm-dd hh:mm:ss")); } else if (value instanceof localdate localdate) { return localdate.format(datetimeformatter.ofpattern("yyyy-mm-dd")); } else { return value; } }
d. 修改 convertobjecttomap 方法
发表评论