当前位置: 代码网 > it编程>编程语言>Java > SpringBoot种如何使用 EasyExcel 实现自定义表头导出并实现数据格式化转换

SpringBoot种如何使用 EasyExcel 实现自定义表头导出并实现数据格式化转换

2024年11月13日 Java 我要评论
springboot3 使用 easyexcel 封装工具类实现 自定义表头 导出并实现 数据格式化转换 与 添加下拉框 操作在现代企业应用中,数据导出功能是非常常见的需求。特别是在处理大量数据时,将

springboot3 使用 easyexcel 封装工具类实现 自定义表头 导出并实现 数据格式化转换 与 添加下拉框 操作

在现代企业应用中,数据导出功能是非常常见的需求。特别是在处理大量数据时,将数据导出为 excel 文件不仅方便用户查看和分析,还能提高数据处理的效率。apache poi 是一个常用的 java excel 处理库,但它在处理大数据量时性能较差。为此,阿里巴巴开源了 easyexcel,这是一个基于 java 的简单、优雅的 excel 操作库,它在处理大数据量时表现优异。

本文将详细介绍如何使用 easyexcel 实现自定义表头导出,并实现数据格式化转换。

官网 : easyexcel

封装了很多导出方法针对不同的业务场景,这里直接展示自定义表头的代码 .

1. 使用示例

vo 示例(部分字段)

使用方法 export

用户自定义表头的 字段显示名称 , 需跟导出注解 @excelpropertyvalue 相对应 , 不然无法取到值

页面显示数据

导出 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>> 是一个二维数组,每个元素都是一个 objecteasyexcel 无法知道这些 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 方法

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com