用自定义注解解决excel动态表头导出的问题

发布于:2025-07-03 ⋅ 阅读:(24) ⋅ 点赞:(0)

导入的excel有固定表头+动态表头如何解决

自定义注解:

import java.lang.annotation.*;

/**
 * 自定义注解,用于动态生成excel表头
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldLabel {

    // 字段中文
    String label();

    // 字段顺序
    int order() default 0;

    // 分组标识
    String group() default "default";

}

导出的类:


import cn.com.fsg.ihro.openacct.support.annotation.FieldLabel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 服务费收入项明细excel导出
 *
 * @author makejava
 * @since 2025-07-01 16:41:21
 */
@Data
public class ExcelVO {

    @FieldLabel(label = "错误信息", order = 1, group = "error")
    private String errMsg;

    @FieldLabel(label = "业务日期", order = 11, group = "export")
    private String businessDay; 

    @FieldLabel(label = "雇员姓名", order = 15, group = "export")
    private String empName;

    @FieldLabel(label = "证件类型", order = 16, group = "export")
    private String idType;

    @FieldLabel(label = "证件号码", order = 17, group = "export")
    private String idNumber;
 
    @Schema(description = "动态字段:服务费收入项-产品方案名称")
    private Map<String, BigDecimal> dynamicMap = new HashMap<>();

}

工具里:利用反射+自定义注解+泛型 解析ExcelVO 并导出文件


import cn.com.fsg.ihro.openacct.support.annotation.FieldLabel;
import com.alibaba.excel.EasyExcel;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Excel导出工具类
 */
public class ExcelExportUtil {

    /**
     * 导出Excel(自动识别固定列 + 动态列)
     *
     * @param response 响应对象
     * @param dataList 数据列表
     * @param fileName 文件名
     * @param groups   分组
     */
    public static <T> void export(HttpServletResponse response, List<T> dataList, String fileName, String... groups) throws IOException {
        if (CollectionUtils.isEmpty(dataList)) {
            return;
        }

        // 提取动态表头 map
        Set<String> dynamicHeaders = extractDynamicHeaders(dataList.get(0));

        // 构建表头:固定表头+动态表头
        List<List<String>> head = buildHead(dataList.get(0).getClass(), dynamicHeaders, groups);

        // 构建数据
        List<Map<Integer, Object>> content = convertToMapList(dataList, groups);

        // 设置响应头
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));

        // 执行导出
        EasyExcel.write(response.getOutputStream())
                .head(head)
                .sheet()
                .doWrite(content);
    }

    /**
     * 提取动态列头(假设 VO 中有一个 Map 字段用于存储动态列)
     *
     * @param vo 数据对象
     */
    private static Set<String> extractDynamicHeaders(Object vo) {
        // getDeclaredFields获取当前类中声明的所有字段
        for (Field field : vo.getClass().getDeclaredFields()) {
            // 判断当前字段的类型是否是 Map 类型或其子类
            if (Map.class.isAssignableFrom(field.getType())) {
                try {
                    field.setAccessible(true);
                    @SuppressWarnings("unchecked")
                    Map<String, Object> dynamicMap = (Map<String, Object>) field.get(vo);
                    return dynamicMap != null ? dynamicMap.keySet() : Collections.emptySet();
                } catch (IllegalAccessException ignored) {
                }
            }
        }
        return Collections.emptySet();
    }

    /**
     * 构建表头:固定表头+动态表头(从 VO 的 @FieldLabel 注解提取)
     *
     * @param excelClass     Excel类
     * @param dynamicHeaders 动态列
     * @param groups         分组
     */
    public static List<List<String>> buildHead(Class<?> excelClass, Set<String> dynamicHeaders, String... groups) {
        List<List<String>> head = new ArrayList<>();

        // 1、获取当前类中声明的所有字段 2、字段有FieldLabel注解 3、通过group过滤 4、通过order排序
        List<Field> sortedFields = Arrays.stream(excelClass.getDeclaredFields())
                .filter(f -> f.isAnnotationPresent(FieldLabel.class))
                .filter(f -> groups.length == 0 || Arrays.asList(groups).contains(f.getAnnotation(FieldLabel.class).group()))
                .sorted(Comparator.comparingInt(f -> f.getAnnotation(FieldLabel.class).order()))
                .collect(Collectors.toList());

        // 构建固定列头
        for (Field field : sortedFields) {
            FieldLabel annotation = field.getAnnotation(FieldLabel.class);
            head.add(Collections.singletonList(annotation.label()));
        }

        // 添加动态列头
        for (String header : dynamicHeaders) {
            head.add(Collections.singletonList(header));
        }

        return head;
    }

    /**
     * 构建数据:将VO 转换为 Map<Integer, Object>
     *
     * @param dataList 数据
     * @param groups   分组
     */
    public static <T> List<Map<Integer, Object>> convertToMapList(List<T> dataList, String... groups) {
        return dataList.stream().map(vo -> {
            Map<Integer, Object> row = new LinkedHashMap<>();
            int index = 0;

            // 1、获取当前类中声明的所有字段 2、字段有FieldLabel注解 3、通过group过滤 4、通过order排序
            List<Field> sortedFields = Arrays.stream(vo.getClass().getDeclaredFields())
                    .filter(f -> f.isAnnotationPresent(FieldLabel.class))
                    .filter(f -> groups.length == 0 || Arrays.asList(groups).contains(f.getAnnotation(FieldLabel.class).group()))
                    .sorted(Comparator.comparingInt(f -> f.getAnnotation(FieldLabel.class).order()))
                    .collect(Collectors.toList());

            // 固定字段
            for (Field field : sortedFields) {
                field.setAccessible(true);
                try {
                    row.put(index++, field.get(vo));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("字段读取失败:" + field.getName(), e);
                }
            }

            // 动态字段
            for (Object value : extractDynamicMap(vo)) {
                row.put(index++, value);
            }

            return row;
        }).collect(Collectors.toList());
    }

    /**
     * 提取 VO 中的动态字段(支持泛型)
     *
     * @param vo 数据对象
     */
    private static List<Object> extractDynamicMap(Object vo) {
        // getDeclaredFields获取当前类中声明的所有字段
        for (Field field : vo.getClass().getDeclaredFields()) {
            // 判断当前字段的类型是否是 Map 类型或其子类
            if (Map.class.isAssignableFrom(field.getType())) {
                field.setAccessible(true);
                try {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> map = (Map<String, Object>) field.get(vo);
                    if (map != null) {
                        return new ArrayList<>(map.values());
                    }
                } catch (IllegalAccessException ignored) {
                }
            }
        }
        return Collections.emptyList();
    }
}

用法示例:

 	@GetMapping("/import-download")
    @Operation(summary = "下载excel")
    public void importDownload(@Valid SerIncomeDetailExcelQueryReqVO reqVO, HttpServletResponse response) throws IOException {
        // Step 1: 调用 service 查询数据,并转换为 ExcelVO 数据
        List<ExcelVO> dataList = service.importDownload(reqVO);

        // Step 2: 使用 EasyExcel 导出
        ExcelExportUtil.export(response, dataList, "服务费收入项明细-金额导入.xlsx", "export", "error");

    }