Spring Boot注解实现Excel导出:优雅处理文本显示、枚举与字典
在企业级应用开发中,Excel导出是一项非常常见的需求。无论是数据报表、业务统计还是信息备份,Excel都以其直观的表格形式和丰富的数据处理能力,成为了数据导出的首选格式。然而,在实际开发过程中,我们经常需要处理各种特殊字段,如格式化文本、枚举值转换、字典映射等,这些需求使得Excel导出功能的实现变得复杂。本文将介绍如何通过自定义注解的方式,优雅地处理这些字段转换需求,使Excel导出功能更加灵活、可维护。
问题分析
在实现Excel导出功能时,我们通常面临以下几个挑战:
- 文本显示格式化:日期、金额等字段需要按照特定格式显示,如日期格式化为"yyyy-MM-dd",金额格式化为"¥#,##0.00"等。
- 枚举值转换:数据库中存储的通常是枚举的code值(如0、1、2),但在导出时需要显示为对应的含义(如"男"、"女"、"未知")。
- 字典映射:某些字段的值需要根据系统字典表进行转换,如地区编码转换为地区名称,状态码转换为状态描述等。
- 多语言支持:国际化系统中,同一份数据可能需要根据用户语言环境导出不同语言的文本。
- 动态列处理:某些业务场景下,导出的列可能需要根据条件动态显示或隐藏。
传统的实现方式通常是在导出逻辑中编写大量的if-else判断或switch-case语句,不仅代码冗长,而且难以维护。当需求变更时,需要修改导出逻辑,增加了出错的风险。
解决方案
为了解决上述问题,我们可以采用基于注解的AOP(面向切面编程)思想,通过自定义注解来标记字段的导出行为,然后在导出过程中统一处理这些注解,实现字段值的自动转换。
整体方案设计如下:
- 自定义注解:设计一组注解,用于标记实体类字段的导出行为,包括列名、格式、转换规则等。
- 导出工具类:实现一个通用的Excel导出工具类,能够识别并处理这些注解,自动完成字段转换。
- 转换器接口:定义字段转换器接口,针对不同类型的字段(枚举、字典等)提供不同的实现。
- 注解处理器:在导出过程中,通过反射机制获取字段上的注解信息,根据注解配置调用相应的转换器处理字段值。
这种方案的优点是:
- 声明式配置:通过注解声明导出行为,代码简洁直观。
- 高内聚低耦合:导出逻辑与业务逻辑分离,互不影响。
- 易于扩展:新增字段类型或转换规则时,只需添加新的转换器,无需修改现有代码。
- 可维护性强:字段转换规则集中在注解中,便于统一管理和修改。
实现步骤
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导出方案,能够优雅地处理文本显示、枚举值转换和字典映射等需求。该方案具有以下优点:
- 声明式配置:通过注解声明导出行为,代码简洁直观,易于理解和维护。
- 高内聚低耦合:导出逻辑与业务逻辑分离,互不影响,符合单一职责原则。
- 易于扩展:新增字段类型或转换规则时,只需添加新的转换器,无需修改现有代码,符合开闭原则。
- 性能优化:通过缓存、分页等方式,解决了大数据量导出的性能问题。
- 功能丰富:支持文本替换、日期格式化、枚举转换、字典映射等多种字段处理需求。
在实际项目中,可以根据具体需求进一步扩展该方案,例如添加多语言支持、动态列处理、数据校验等功能。同时,也可以考虑将此方案封装成一个独立的Starter,以便在多个项目中复用。
希望本文能够帮助你在Spring Boot项目中实现更加优雅的Excel导出功能。如果你有任何问题或建议,欢迎在评论区留言讨论。
版权声明:本文为原创文章,版权归 全栈开发技术博客 所有。
本文链接:https://www.lvtao.net/dev/springboot-annotation-excel-export-field-processing.html
转载时须注明出处及本声明