专注于高性能网络应用开发,核心技术包括PHP、Java、GO、NodeJS等后端语言,VUE、UNI、APP等前端开发,服务器运维、数据库、实时通信、AI等领域拥有丰富经验

Spring Boot注解实现Excel导出:优雅处理文本显示、枚举与字典

在企业级应用开发中,Excel导出是一项非常常见的需求。无论是数据报表、业务统计还是信息备份,Excel都以其直观的表格形式和丰富的数据处理能力,成为了数据导出的首选格式。然而,在实际开发过程中,我们经常需要处理各种特殊字段,如格式化文本、枚举值转换、字典映射等,这些需求使得Excel导出功能的实现变得复杂。本文将介绍如何通过自定义注解的方式,优雅地处理这些字段转换需求,使Excel导出功能更加灵活、可维护。

问题分析

在实现Excel导出功能时,我们通常面临以下几个挑战:

  1. 文本显示格式化:日期、金额等字段需要按照特定格式显示,如日期格式化为"yyyy-MM-dd",金额格式化为"¥#,##0.00"等。
  2. 枚举值转换:数据库中存储的通常是枚举的code值(如0、1、2),但在导出时需要显示为对应的含义(如"男"、"女"、"未知")。
  3. 字典映射:某些字段的值需要根据系统字典表进行转换,如地区编码转换为地区名称,状态码转换为状态描述等。
  4. 多语言支持:国际化系统中,同一份数据可能需要根据用户语言环境导出不同语言的文本。
  5. 动态列处理:某些业务场景下,导出的列可能需要根据条件动态显示或隐藏。

传统的实现方式通常是在导出逻辑中编写大量的if-else判断或switch-case语句,不仅代码冗长,而且难以维护。当需求变更时,需要修改导出逻辑,增加了出错的风险。

解决方案

为了解决上述问题,我们可以采用基于注解的AOP(面向切面编程)思想,通过自定义注解来标记字段的导出行为,然后在导出过程中统一处理这些注解,实现字段值的自动转换。

整体方案设计如下:

  1. 自定义注解:设计一组注解,用于标记实体类字段的导出行为,包括列名、格式、转换规则等。
  2. 导出工具类:实现一个通用的Excel导出工具类,能够识别并处理这些注解,自动完成字段转换。
  3. 转换器接口:定义字段转换器接口,针对不同类型的字段(枚举、字典等)提供不同的实现。
  4. 注解处理器:在导出过程中,通过反射机制获取字段上的注解信息,根据注解配置调用相应的转换器处理字段值。

这种方案的优点是:

  • 声明式配置:通过注解声明导出行为,代码简洁直观。
  • 高内聚低耦合:导出逻辑与业务逻辑分离,互不影响。
  • 易于扩展:新增字段类型或转换规则时,只需添加新的转换器,无需修改现有代码。
  • 可维护性强:字段转换规则集中在注解中,便于统一管理和修改。

实现步骤

1. 自定义注解的设计

首先,我们需要设计一组注解来标记字段的导出行为。主要包括以下注解:

  • @ExcelExport:标记字段需要导出到Excel
  • @ExcelDateFormat:指定日期字段的格式
  • @ExcelEnumFormat:指定枚举字段的转换规则
  • @ExcelDictFormat:指定字典字段的转换规则

下面是这些注解的具体定义:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 标记字段需要导出到Excel
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelExport {
    /**
     * 导出列名
     */
    String value() default "";

    /**
     * 列顺序,越小越靠前
     */
    int order() default 0;

    /**
     * 是否导出,默认为true
     */
    boolean isExport() default true;

    /**
     * 替换文本,如{"0_男", "1_女"}
     */
    String[] replace() default {};
}

/**
 * 日期格式化注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelDateFormat {
    /**
     * 日期格式,如 "yyyy-MM-dd"
     */
    String value() default "yyyy-MM-dd";
}

/**
 * 枚举格式化注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelEnumFormat {
    /**
     * 枚举类
     */
    Class<? extends Enum<?>> enumClass();

    /**
     * 获取枚举描述的方法名
     */
    String method() default "getDescription";
}

/**
 * 字典格式化注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelDictFormat {
    /**
     * 字典类型
     */
    String dictType();

    /**
     * 字典值的获取方法,默认为getValue
     */
    String valueMethod() default "getValue";

    /**
     * 字典标签的获取方法,默认为getLabel
     */
    String labelMethod() default "getLabel";
}

2. 导出工具类的实现

接下来,我们实现一个通用的Excel导出工具类,该工具类能够识别并处理上述注解,自动完成字段转换。

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Excel导出工具类
 */
public class ExcelExportUtil {
    
    /**
     * 导出Excel文件
     * @param response HTTP响应对象
     * @param dataList 数据列表
     * @param fileName 文件名
     * @param sheetName 工作表名
     * @param <T> 数据类型
     * @throws Exception 导出异常
     */
    public static <T> void exportExcel(HttpServletResponse response, List<T> dataList, 
                                      String fileName, String sheetName) throws Exception {
        if (dataList == null || dataList.isEmpty()) {
            throw new IllegalArgumentException("导出数据不能为空");
        }
        
        // 获取第一个对象的类类型
        Class<?> clazz = dataList.get(0).getClass();
        
        // 获取带有@ExcelExport注解的字段列表
        List<Field> fields = getExportFields(clazz);
        
        // 创建工作簿
        try (SXSSFWorkbook workbook = new SXSSFWorkbook()) {
            // 创建工作表
            Sheet sheet = workbook.createSheet(sheetName);
            
            // 创建标题行样式
            CellStyle headerStyle = createHeaderStyle(workbook);
            
            // 创建数据行样式
            CellStyle dataStyle = createDataStyle(workbook);
            
            // 创建标题行
            createHeaderRow(sheet, fields, headerStyle);
            
            // 填充数据
            fillDataRows(sheet, dataList, fields, dataStyle);
            
            // 自动调整列宽
            autoSizeColumns(sheet, fields.size());
            
            // 设置响应头
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
            response.setHeader("Content-disposition", "attachment;filename=" + encodedFileName + ".xlsx");
            
            // 写入输出流
            try (OutputStream out = response.getOutputStream()) {
                workbook.write(out);
            }
        }
    }
    
    /**
     * 获取带有@ExcelExport注解的字段列表,并按order排序
     * @param clazz 类类型
     * @return 字段列表
     */
    private static List<Field> getExportFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        
        // 获取所有字段(包括父类的)
        Class<?> currentClass = clazz;
        while (currentClass != null && currentClass != Object.class) {
            Collections.addAll(fields, currentClass.getDeclaredFields());
            currentClass = currentClass.getSuperclass();
        }
        
        // 过滤出带有@ExcelExport注解且isExport为true的字段
        List<Field> exportFields = new ArrayList<>();
        for (Field field : fields) {
            ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
            if (excelExport != null && excelExport.isExport()) {
                exportFields.add(field);
            }
        }
        
        // 按order排序
        exportFields.sort(Comparator.comparingInt(field -> {
            ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
            return excelExport != null ? excelExport.order() : 0;
        }));
        
        return exportFields;
    }
    
    /**
     * 创建标题行样式
     * @param workbook 工作簿
     * @return 标题行样式
     */
    private static CellStyle createHeaderStyle(SXSSFWorkbook workbook) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        return style;
    }
    
    /**
     * 创建数据行样式
     * @param workbook 工作簿
     * @return 数据行样式
     */
    private static CellStyle createDataStyle(SXSSFWorkbook workbook) {
        CellStyle style = workbook.createCellStyle();
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        return style;
    }
    
    /**
     * 创建标题行
     * @param sheet 工作表
     * @param fields 字段列表
     * @param style 样式
     */
    private static void createHeaderRow(Sheet sheet, List<Field> fields, CellStyle style) {
        Row headerRow = sheet.createRow(0);
        
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
            
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(excelExport.value().isEmpty() ? field.getName() : excelExport.value());
            cell.setCellStyle(style);
        }
    }
    
    /**
     * 填充数据行
     * @param sheet 工作表
     * @param dataList 数据列表
     * @param fields 字段列表
     * @param style 样式
     * @throws Exception 异常
     */
    private static <T> void fillDataRows(Sheet sheet, List<T> dataList, List<Field> fields, CellStyle style) throws Exception {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        
        for (int i = 0; i < dataList.size(); i++) {
            T data = dataList.get(i);
            Row dataRow = sheet.createRow(i + 1);
            
            for (int j = 0; j < fields.size(); j++) {
                Field field = fields.get(j);
                field.setAccessible(true);
                
                Object value = field.get(data);
                Cell cell = dataRow.createCell(j);
                cell.setCellStyle(style);
                
                // 处理字段值
                Object processedValue = processFieldValue(field, value);
                
                // 设置单元格值
                if (processedValue == null) {
                    cell.setCellValue("");
                } else if (processedValue instanceof Date) {
                    cell.setCellValue(dateFormat.format((Date) processedValue));
                } else {
                    cell.setCellValue(processedValue.toString());
                }
            }
        }
    }
    
    /**
     * 处理字段值
     * @param field 字段
     * @param value 原始值
     * @return 处理后的值
     * @throws Exception 异常
     */
    private static Object processFieldValue(Field field, Object value) throws Exception {
        if (value == null) {
            return null;
        }
        
        // 处理@ExcelExport注解中的replace属性
        ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
        if (excelExport != null && excelExport.replace().length > 0) {
            return processReplaceValue(value, excelExport.replace());
        }
        
        // 处理日期格式化
        ExcelDateFormat dateFormat = field.getAnnotation(ExcelDateFormat.class);
        if (dateFormat != null && value instanceof Date) {
            SimpleDateFormat sdf = new SimpleDateFormat(dateFormat.value());
            return sdf.format((Date) value);
        }
        
        // 处理枚举格式化
        ExcelEnumFormat enumFormat = field.getAnnotation(ExcelEnumFormat.class);
        if (enumFormat != null) {
            return processEnumValue(value, enumFormat);
        }
        
        // 处理字典格式化
        ExcelDictFormat dictFormat = field.getAnnotation(ExcelDictFormat.class);
        if (dictFormat != null) {
            return processDictValue(value, dictFormat);
        }
        
        return value;
    }
    
    /**
     * 处理替换值
     * @param value 原始值
     * @param replaces 替换规则数组,如 {"0_男", "1_女"}
     * @return 替换后的值
     */
    private static Object processReplaceValue(Object value, String[] replaces) {
        String strValue = value.toString();
        
        for (String replace : replaces) {
            String[] keyValue = replace.split("_");
            if (keyValue.length == 2 && keyValue[0].equals(strValue)) {
                return keyValue[1];
            }
        }
        
        return value;
    }
    
    /**
     * 处理枚举值
     * @param value 原始值
     * @param enumFormat 枚举格式化注解
     * @return 处理后的值
     * @throws Exception 异常
     */
    private static Object processEnumValue(Object value, ExcelEnumFormat enumFormat) throws Exception {
        Class<?> enumClass = enumFormat.enumClass();
        String methodName = enumFormat.method();
        
        if (!enumClass.isEnum()) {
            return value;
        }
        
        // 获取枚举值数组
        Object[] enumConstants = enumClass.getEnumConstants();
        
        // 遍历枚举值
        for (Object enumConstant : enumConstants) {
            // 获取枚举值的方法
            try {
                // 尝试获取枚举值的getCode方法
                Method getCodeMethod = enumClass.getMethod("getCode");
                Object code = getCodeMethod.invoke(enumConstant);
                
                if (value.equals(code)) {
                    // 获取描述方法
                    Method getDescMethod = enumClass.getMethod(methodName);
                    return getDescMethod.invoke(enumConstant);
                }
            } catch (NoSuchMethodException e) {
                // 如果没有getCode方法,则使用ordinal()方法
                try {
                    Method ordinalMethod = Enum.class.getMethod("ordinal");
                    Object ordinal = ordinalMethod.invoke(enumConstant);
                    
                    if (value.equals(ordinal)) {
                        // 获取描述方法
                        Method getDescMethod = enumClass.getMethod(methodName);
                        return getDescMethod.invoke(enumConstant);
                    }
                } catch (NoSuchMethodException ex) {
                    return value;
                }
            }
        }
        
        return value;
    }
    
    /**
     * 处理字典值
     * @param value 原始值
     * @param dictFormat 字典格式化注解
     * @return 处理后的值
     * @throws Exception 异常
     */
    private static Object processDictValue(Object value, ExcelDictFormat dictFormat) throws Exception {
        // 这里应该是从字典服务获取字典数据,为了示例简化,直接返回一个模拟值
        // 实际项目中,这里应该调用字典服务获取字典数据
        
        // 模拟字典数据
        Map<String, Map<String, String>> dictData = new HashMap<>();
        Map<String, String> genderDict = new HashMap<>();
        genderDict.put("0", "男");
        genderDict.put("1", "女");
        genderDict.put("2", "未知");
        dictData.put("gender", genderDict);
        
        Map<String, String> statusDict = new HashMap<>();
        statusDict.put("0", "禁用");
        statusDict.put("1", "启用");
        dictData.put("status", statusDict);
        
        // 获取字典类型
        String dictType = dictFormat.dictType();
        
        // 获取字典数据
        Map<String, String> dict = dictData.get(dictType);
        if (dict == null) {
            return value;
        }
        
        // 获取字典值
        String dictValue = value.toString();
        
        // 返回字典标签
        return dict.getOrDefault(dictValue, dictValue);
    }
    
    /**
     * 自动调整列宽
     * @param sheet 工作表
     * @param columnCount 列数
     */
    private static void autoSizeColumns(Sheet sheet, int columnCount) {
        for (int i = 0; i < columnCount; i++) {
            sheet.autoSizeColumn(i);
            // 限制最大宽度,防止某些列过宽
            if (sheet.getColumnWidth(i) > 10000) {
                sheet.setColumnWidth(i, 10000);
            }
        }
    }
}

3. 文本显示处理

对于文本显示的处理,我们主要通过@ExcelExport注解的replace属性来实现。该属性允许我们定义一组替换规则,格式为"原始值_显示值",例如{"0_男", "1_女"}表示将0替换为"男",将1替换为"女"。

processReplaceValue方法中,我们遍历替换规则数组,将每个规则按"_"分割成键值对,然后与原始值进行比较,如果匹配则返回替换后的值。

4. 枚举值处理

对于枚举值的处理,我们使用@ExcelEnumFormat注解,该注解需要指定枚举类和获取枚举描述的方法名。

processEnumValue方法中,我们首先获取枚举类的所有枚举值,然后遍历这些枚举值,尝试通过getCode方法或ordinal方法获取枚举的代码值,与原始值进行比较,如果匹配则调用指定的方法获取枚举描述并返回。

5. 字典值处理

对于字典值的处理,我们使用@ExcelDictFormat注解,该注解需要指定字典类型以及获取字典值和标签的方法名。

processDictValue方法中,我们根据字典类型从字典服务获取字典数据(示例中为了简化,使用了模拟数据),然后将原始值作为字典值查找对应的字典标签并返回。

代码示例

下面是一个完整的使用示例,包括实体类定义和控制器代码。

1. 定义枚举类

/**
 * 性别枚举
 */
public enum GenderEnum {
    MALE(0, "男"),
    FEMALE(1, "女"),
    UNKNOWN(2, "未知");
    
    private final Integer code;
    private final String description;
    
    GenderEnum(Integer code, String description) {
        this.code = code;
        this.description = description;
    }
    
    public Integer getCode() {
        return code;
    }
    
    public String getDescription() {
        return description;
    }
    
    // 根据code获取枚举
    public static GenderEnum getByCode(Integer code) {
        if (code == null) {
            return null;
        }
        for (GenderEnum gender : values()) {
            if (gender.getCode().equals(code)) {
                return gender;
            }
        }
        return null;
    }
}

2. 定义实体类

import java.math.BigDecimal;
import java.util.Date;

/**
 * 用户实体类
 */
public class User {
    @ExcelExport(value = "用户ID", order = 1)
    private Long id;
    
    @ExcelExport(value = "用户名", order = 2)
    private String username;
    
    @ExcelExport(value = "性别", order = 3, replace = {"0_男", "1_女", "2_未知"})
    private Integer gender;
    
    @ExcelExport(value = "性别(枚举)", order = 4)
    @ExcelEnumFormat(enumClass = GenderEnum.class, method = "getDescription")
    private Integer genderEnum;
    
    @ExcelExport(value = "状态", order = 5)
    @ExcelDictFormat(dictType = "status")
    private String status;
    
    @ExcelExport(value = "创建时间", order = 6)
    @ExcelDateFormat("yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    
    @ExcelExport(value = "余额", order = 7, replace = {"0_免费", "1_付费"})
    private BigDecimal balance;
    
    public User(Long id, String username, Integer gender, Integer genderEnum, 
               String status, Date createTime, BigDecimal balance) {
        this.id = id;
        this.username = username;
        this.gender = gender;
        this.genderEnum = genderEnum;
        this.status = status;
        this.createTime = createTime;
        this.balance = balance;
    }
    
    // 省略getter和setter方法...
}

3. 定义控制器

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 用户控制器
 */
@RestController
@RequestMapping("/user")
public class UserController {
    
    /**
     * 导出用户数据
     * @param response HTTP响应对象
     * @throws Exception 导出异常
     */
    @GetMapping("/export")
    public void exportUsers(HttpServletResponse response) throws Exception {
        // 模拟数据
        List<User> userList = new ArrayList<>();
        userList.add(new User(1L, "张三", 0, 0, "1", new Date(), new BigDecimal("0")));
        userList.add(new User(2L, "李四", 1, 1, "0", new Date(), new BigDecimal("100.50")));
        userList.add(new User(3L, "王五", 2, 2, "1", new Date(), new BigDecimal("0")));
        userList.add(new User(4L, "赵六", 0, 0, "1", new Date(), new BigDecimal("500.00")));
        
        // 导出Excel
        ExcelExportUtil.exportExcel(response, userList, "用户数据", "用户列表");
    }
}

难点讲解

在实现过程中,有几个难点需要特别关注:

1. 反射性能优化

在导出过程中,我们大量使用了反射来获取字段值和调用方法,这可能会影响性能。为了优化性能,我们可以:

  • 使用缓存:将反射获取的Field对象和Method对象缓存起来,避免重复获取。
  • 使用setAccessible(true):在获取Field对象后调用此方法,可以绕过访问权限检查,提高后续访问速度。
  • 考虑使用字节码操作库:如CGLIB或Byte Buddy,在运行时生成访问器类,替代反射操作。

下面是一个优化后的字段值处理示例:

// 字段缓存
private static final Map<Class<?>, List<Field>> FIELD_CACHE = new ConcurrentHashMap<>();

// 方法缓存
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();

/**
 * 获取带有@ExcelExport注解的字段列表,并按order排序(带缓存)
 * @param clazz 类类型
 * @return 字段列表
 */
private static List<Field> getExportFieldsWithCache(Class<?> clazz) {
    return FIELD_CACHE.computeIfAbsent(clazz, key -> {
        List<Field> fields = new ArrayList<>();
        
        // 获取所有字段(包括父类的)
        Class<?> currentClass = key;
        while (currentClass != null && currentClass != Object.class) {
            Collections.addAll(fields, currentClass.getDeclaredFields());
            currentClass = currentClass.getSuperclass();
        }
        
        // 过滤出带有@ExcelExport注解且isExport为true的字段
        List<Field> exportFields = new ArrayList<>();
        for (Field field : fields) {
            ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
            if (excelExport != null && excelExport.isExport()) {
                exportFields.add(field);
            }
        }
        
        // 按order排序
        exportFields.sort(Comparator.comparingInt(field -> {
            ExcelExport excelExport = field.getAnnotation(ExcelExport.class);
            return excelExport != null ? excelExport.order() : 0;
        }));
        
        return exportFields;
    });
}

/**
 * 获取方法(带缓存)
 * @param clazz 类类型
 * @param methodName 方法名
 * @param parameterTypes 参数类型
 * @return 方法对象
 */
private static Method getMethodWithCache(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
    String key = clazz.getName() + "." + methodName + Arrays.toString(parameterTypes);
    return METHOD_CACHE.computeIfAbsent(key, k -> {
        try {
            return clazz.getMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            return null;
        }
    });
}

2. 大数据量导出优化

当导出大量数据时,直接将所有数据加载到内存中可能会导致内存溢出。为了解决这个问题,我们可以:

  • 使用SXSSFWorkbook:这是Apache POI提供的流式API,可以将部分数据写入临时文件,减少内存消耗。
  • 分页查询:从数据库中分页查询数据,每查询一页就写入Excel,然后清空内存中的数据,再查询下一页。
  • 使用多线程:可以将数据查询和Excel写入操作分离,使用生产者-消费者模式,一个线程负责查询数据,另一个线程负责写入Excel。

下面是一个分页导出的示例:

/**
 * 分页导出Excel
 * @param response HTTP响应对象
 * @param supplier 数据提供者,接受页码和页大小,返回数据列表
 * @param fileName 文件名
 * @param sheetName 工作表名
 * @param pageSize 每页大小
 * @param <T> 数据类型
 * @throws Exception 导出异常
 */
public static <T> void exportExcelByPage(HttpServletResponse response, 
                                        DataSupplier<T> supplier, 
                                        String fileName, 
                                        String sheetName, 
                                        int pageSize) throws Exception {
    // 获取第一页数据
    List<T> firstPage = supplier.get(1, pageSize);
    if (firstPage == null || firstPage.isEmpty()) {
        throw new IllegalArgumentException("导出数据不能为空");
    }
    
    // 获取第一个对象的类类型
    Class<?> clazz = firstPage.get(0).getClass();
    
    // 获取带有@ExcelExport注解的字段列表
    List<Field> fields = getExportFields(clazz);
    
    // 创建工作簿
    try (SXSSFWorkbook workbook = new SXSSFWorkbook()) {
        // 创建工作表
        Sheet sheet = workbook.createSheet(sheetName);
        
        // 创建标题行样式
        CellStyle headerStyle = createHeaderStyle(workbook);
        
        // 创建数据行样式
        CellStyle dataStyle = createDataStyle(workbook);
        
        // 创建标题行
        createHeaderRow(sheet, fields, headerStyle);
        
        // 填充数据
        int rowNum = 1;  // 从第二行开始,第一行是标题
        int pageNum = 1;
        List<T> pageData = firstPage;
        
        while (pageData != null && !pageData.isEmpty()) {
            // 处理当前页数据
            for (T data : pageData) {
                Row dataRow = sheet.createRow(rowNum++);
                
                for (int j = 0; j < fields.size(); j++) {
                    Field field = fields.get(j);
                    field.setAccessible(true);
                    
                    Object value = field.get(data);
                    Cell cell = dataRow.createCell(j);
                    cell.setCellStyle(dataStyle);
                    
                    // 处理字段值
                    Object processedValue = processFieldValue(field, value);
                    
                    // 设置单元格值
                    if (processedValue == null) {
                        cell.setCellValue("");
                    } else if (processedValue instanceof Date) {
                        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                        cell.setCellValue(sdf.format((Date) processedValue));
                    } else {
                        cell.setCellValue(processedValue.toString());
                    }
                }
            }
            
            // 手动刷新行到磁盘,释放内存
            ((SXSSFSheet) sheet).flushRows();
            
            // 获取下一页数据
            pageNum++;
            pageData = supplier.get(pageNum, pageSize);
        }
        
        // 自动调整列宽
        autoSizeColumns(sheet, fields.size());
        
        // 设置响应头
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
        response.setHeader("Content-disposition", "attachment;filename=" + encodedFileName + ".xlsx");
        
        // 写入输出流
        try (OutputStream out = response.getOutputStream()) {
            workbook.write(out);
        }
    }
}

/**
 * 数据提供者接口
 * @param <T> 数据类型
 */
@FunctionalInterface
public interface DataSupplier<T> {
    /**
     * 获取数据
     * @param pageNum 页码
     * @param pageSize 页大小
     * @return 数据列表
     */
    List<T> get(int pageNum, int pageSize);
}

使用分页导出的示例:

@GetMapping("/export-large")
public void exportLargeData(HttpServletResponse response) throws Exception {
    // 导出Excel(分页)
    ExcelExportUtil.exportExcelByPage(response, (pageNum, pageSize) -> {
        // 这里应该是从数据库分页查询数据
        // 为了示例简化,我们模拟数据
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < pageSize; i++) {
            int id = (pageNum - 1) * pageSize + i + 1;
            if (id > 1000) {  // 假设只有1000条数据
                break;
            }
            userList.add(new User(
                (long) id, 
                "用户" + id, 
                id % 3, 
                id % 3, 
                id % 2 == 0 ? "1" : "0", 
                new Date(), 
                new BigDecimal(id % 2 == 0 ? "0" : "100.50"))
            );
        }
        return userList;
    }, "大量用户数据", "用户列表", 100);  // 每页100条
}

3. 复杂对象处理

在实际业务中,我们可能需要导出复杂对象(即对象中包含其他对象)的字段。为了处理这种情况,我们可以扩展注解,使其支持路径表达式,例如"user.address.city"表示导出用户对象的地址对象的城市字段。

下面是支持复杂对象的实现:

/**
 * 标记字段需要导出到Excel(增强版,支持复杂对象)
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelExport {
    /**
     * 导出列名
     */
    String value() default "";

    /**
     * 列顺序,越小越靠前
     */
    int order() default 0;

    /**
     * 是否导出,默认为true
     */
    boolean isExport() default true;

    /**
     * 替换文本,如{"0_男", "1_女"}
     */
    String[] replace() default {};
    
    /**
     * 字段路径,支持复杂对象,如"user.address.city"
     */
    String path() default "";
}

/**
 * 获取字段值(支持复杂对象)
 * @param obj 对象
 * @param fieldPath 字段路径,如"user.address.city"
 * @return 字段值
 * @throws Exception 异常
 */
private static Object getFieldValue(Object obj, String fieldPath) throws Exception {
    if (obj == null || fieldPath == null || fieldPath.isEmpty()) {
        return null;
    }
    
    String[] fields = fieldPath.split("\\.");
    Object currentObj = obj;
    
    for (String fieldName : fields) {
        if (currentObj == null) {
            return null;
        }
        
        Class<?> clazz = currentObj.getClass();
        Field field = getField(clazz, fieldName);
        if (field == null) {
            return null;
        }
        
        field.setAccessible(true);
        currentObj = field.get(currentObj);
    }
    
    return currentObj;
}

/**
 * 获取类的字段(包括父类)
 * @param clazz 类类型
 * @param fieldName 字段名
 * @return 字段对象
 */
private static Field getField(Class<?> clazz, String fieldName) {
    Class<?> currentClass = clazz;
    while (currentClass != null && currentClass != Object.class) {
        try {
            return currentClass.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            currentClass = currentClass.getSuperclass();
        }
    }
    return null;
}

使用复杂对象路径的示例:

public class Order {
    @ExcelExport(value = "订单ID", order = 1)
    private Long id;
    
    @ExcelExport(value = "用户名", order = 2, path = "user.username")
    private User user;
    
    // 其他字段...
}

public class User {
    private String username;
    // 其他字段...
}

总结

通过自定义注解的方式,我们实现了一个灵活、可扩展的Excel导出方案,能够优雅地处理文本显示、枚举值转换和字典映射等需求。该方案具有以下优点:

  1. 声明式配置:通过注解声明导出行为,代码简洁直观,易于理解和维护。
  2. 高内聚低耦合:导出逻辑与业务逻辑分离,互不影响,符合单一职责原则。
  3. 易于扩展:新增字段类型或转换规则时,只需添加新的转换器,无需修改现有代码,符合开闭原则。
  4. 性能优化:通过缓存、分页等方式,解决了大数据量导出的性能问题。
  5. 功能丰富:支持文本替换、日期格式化、枚举转换、字典映射等多种字段处理需求。

在实际项目中,可以根据具体需求进一步扩展该方案,例如添加多语言支持、动态列处理、数据校验等功能。同时,也可以考虑将此方案封装成一个独立的Starter,以便在多个项目中复用。

希望本文能够帮助你在Spring Boot项目中实现更加优雅的Excel导出功能。如果你有任何问题或建议,欢迎在评论区留言讨论。

相关文章

macOS下使用Docker快速部署Zookeeper+Dubbo-Admin

通过Docker Compose,我们只需一个配置文件就能快速搭建Zookeeper+Dubbo-Admin环境,极大简化了部署流程。这种容器化部署方式也方便后续扩展为集群模式在微服务架构中,服...

深入解析 Spring Boot 事务管理:从基础到实践

在现代应用程序开发中,事务管理是确保数据一致性和完整性的核心机制。Spring Boot 作为 Java 生态中的主流框架,通过声明式事务管理极大简化了这一过程。本文将从事务的基础知识入手,深入...

一些编程语言学习心得

作为一名专注于PHP、Go、Java和前端开发(JavaScript、HTML、CSS)的开发者,还得会运维、会谈客户....不想了,都是泪,今天说说这些年学习编程语言的一些体会,不同编程语言在...

Java中线程池遇到父子任务示例及避坑

在Java中使用线程池可以有效地管理和调度线程,提高系统的并发处理能力。然而,当涉及到父子任务时,可能会遇到一些常见的Bug,特别是在子线程中查询数据并行处理时。本文将通过示例代码展示这些常见问...