Excel 工具类使用文档
概述
本Excel工具类基于Apache POI封装,提供了简单易用的Excel导入导出功能。通过统一的@Excel
注解配置,支持多级数据结构、样式配置、自定义转换器等高级功能。
✨ 核心特性
- 统一注解:合并了原有的3个注解为一个
@Excel
注解 - 多级结构:支持对象嵌套、集合等复杂数据结构
- 样式配置:支持字体颜色、背景颜色等样式设置
- 自定义转换:支持数据转换器,实现双向转换
- 高性能:样式缓存机制,优化内存使用
- 安全可靠:解决Zip bomb安全问题
@Excel 注解属性详解
️ 基础属性
属性 | 类型 | 默认值 | 必填 | 说明 | 示例 |
---|
name | String | "" | ✅ | Excel列标题名称 | @Excel(name = "姓名") |
index | int | 0 | ✅ | Excel列索引(从0开始) | @Excel(index = 3) |
defaultValue | String | "" | ❌ | 单元格为空时的默认值 | @Excel(defaultValue = "未填写") |
字段类型配置
属性 | 类型 | 默认值 | 说明 | 使用场景 |
---|
fieldType | FieldType | COLUMN | 字段处理类型 | 控制字段的处理方式 |
FieldType 枚举值说明
枚举值 | 说明 | 适用场景 | 示例 |
---|
COLUMN | 普通列字段 | 基本数据类型字段 | String name , Integer age |
OBJECT | 对象字段 | 嵌套对象 | ClassInfo classInfo |
COLLECTION | 集合字段 | 对象集合 | List<Student> students |
️ 分组和排序
属性 | 类型 | 默认值 | 说明 | 示例 |
---|
isKey | boolean | false | 是否为分组关键字段 | 用于多级数据的分组依据 |
样式配置
属性 | 类型 | 默认值 | 说明 | 可选值示例 |
---|
fontColor | IndexedColors | AUTOMATIC | 字体颜色 | BLACK , WHITE , RED , BLUE |
backgroundColor | IndexedColors | AUTOMATIC | 背景颜色 | YELLOW , LIGHT_GREEN , PINK |
常用颜色枚举值
颜色类别 | 枚举值 |
---|
基础色 | BLACK , WHITE , RED , GREEN , BLUE , YELLOW , ORANGE , PINK |
浅色系 | LIGHT_BLUE , LIGHT_GREEN , LIGHT_YELLOW , LIGHT_ORANGE |
深色系 | DARK_BLUE , DARK_GREEN , DARK_RED , DARK_YELLOW |
灰色系 | GREY_25_PERCENT , GREY_40_PERCENT , GREY_50_PERCENT |
转换器配置
属性 | 类型 | 默认值 | 说明 | 示例 |
---|
converter | Class<? extends ExcelConverter> | DefaultConverter.class | 自定义转换器 | GenderConverter.class |
params | String[] | {} | 转换器参数 | params = {"param1", "param2"} |
集合类型配置
属性 | 类型 | 默认值 | 说明 | 使用条件 |
---|
type | Class<?> | Object.class | 集合元素类型 | fieldType 不为 COLUMN 时使用 |
使用示例
1️⃣ 基础实体类配置
@Data
public class Student
{
@Excel(name = "学号", index = 0)
private String studentId;
@Excel(name = "姓名", index = 1, fontColor = IndexedColors.BLUE)
private String name;
@Excel(name = "年龄", index = 2)
private Integer age;
@Excel(name = "性别", index = 3, converter = GenderConverter.class)
private String gender;
@Excel(name = "成绩", index = 4, converter = ScoreConverter.class)
private Double score;
@Excel(name = "备注", index = 5, defaultValue = "无")
private String remark;
}
2️⃣ 多级数据结构配置
@Data
public class School
{
@Excel(name = "学校名称", index = 0, isKey = true)
private String schoolName;
@Excel(name = "学校编码", index = 1)
private String schoolCode;
@Excel(name = "建校日期", index = 2)
private Date establishDate;
@Excel(name = "班级信息", fieldType = Excel.FieldType.OBJECT, type = ClassInfo.class)
private ClassInfo classInfo;
@Excel(name = "学生列表", fieldType = Excel.FieldType.COLLECTION, type = Student.class)
private List<
Student> students;
}
3️⃣ 样式配置示例
@Data
public class ClassInfo
{
@Excel(name = "班级编码", index = 5, isKey = true)
private String classCode;
@Excel(name = "班级名称", index = 6,
fontColor = IndexedColors.BLACK,
backgroundColor = IndexedColors.YELLOW)
private String className;
@Excel(name = "年级", index = 7,
backgroundColor = IndexedColors.LIGHT_GREEN)
private String grade;
}
自定义转换器
转换器接口定义
public interface ExcelConverter<
T, E> {
E convertToExcel(T value, String[] params);
T convertFromExcel(E value, String[] params);
default CellStyleInfo getCellStyle(T value, String[] params) {
return null;
}
}
转换器实现示例
1️⃣ 性别转换器(支持动态样式)
public class GenderConverter
implements ExcelConverter<
String, String> {
@Override
public String convertToExcel(String value, String[] params) {
return "1".equals(value) ? "男" : "0".equals(value) ? "女" : value;
}
@Override
public String convertFromExcel(String value, String[] params) {
return "男".equals(value) ? "1" : "女".equals(value) ? "0" : value;
}
@Override
public CellStyleInfo getCellStyle(String value, String[] params) {
if ("1".equals(value)) {
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.BLUE);
} else if ("0".equals(value)) {
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.PINK);
}
return null;
}
}
2️⃣ 分数转换器(支持动态样式)
public class ScoreConverter
implements ExcelConverter<
Double, String> {
@Override
public String convertToExcel(Double value, String[] params) {
return value == null ? "未评分" : String.format("%.1f", value);
}
@Override
public Double convertFromExcel(String value, String[] params) {
if (value == null || "未评分".equals(value)) {
return null;
}
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
return null;
}
}
@Override
public CellStyleInfo getCellStyle(Double value, String[] params) {
if (value == null) return null;
if (value >= 90) {
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.GREEN);
} else if (value >= 80) {
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.LIGHT_GREEN);
} else if (value >= 70) {
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.YELLOW);
} else if (value >= 60) {
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.ORANGE);
} else {
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.RED);
}
}
}
样式配置详解
静态样式(注解配置)
@Excel(name = "重要字段", index = 1,
fontColor = IndexedColors.WHITE,
backgroundColor = IndexedColors.RED)
private String importantField;
动态样式(转换器配置)
@Excel(name = "状态", index = 2, converter = StatusConverter.class)
private String status;
public class StatusConverter
implements ExcelConverter<
String, String> {
@Override
public CellStyleInfo getCellStyle(String value, String[] params) {
switch (value) {
case "success":
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.GREEN);
case "warning":
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.YELLOW);
case "error":
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.RED);
default:
return null;
}
}
}
样式优先级
- 动态样式(转换器返回)- 最高优先级
- 静态样式(注解配置)- 次优先级
- 默认样式(系统默认)- 最低优先级
工具类调用方法
导出Excel
@GetMapping("/export")
public void exportData(HttpServletResponse response) {
List<
School> data = schoolService.getAllData();
ExcelUtils.exportExcel(data, School.class,
"学校数据.xlsx", response);
}
导入Excel
@PostMapping("/import")
public String importData(@RequestParam("file") MultipartFile file) {
try (InputStream inputStream = file.getInputStream()) {
List<
School> dataList = ExcelUtils.importExcel(inputStream, School.class)
;
schoolService.batchSave(dataList);
return "导入成功,共导入 " + dataList.size() + " 条记录";
} catch (Exception e) {
return "导入失败:" + e.getMessage();
}
}
⚡ 性能优化特性
样式缓存机制
- 自动缓存:相同颜色组合的样式自动复用
- 内存优化:避免创建重复样式,减少内存占用
- 缓存清理:每次导出开始时自动清理缓存
Zip Bomb 安全处理
- 自动调整:导入时临时调整POI安全限制
- 自动恢复:导入完成后自动恢复原始设置
- 安全保障:不影响其他应用的安全性
最佳实践
1️⃣ 注解配置建议
场景 | 建议配置 | 示例 |
---|
基础字段 | 只配置name和index | @Excel(name = "姓名", index = 1) |
关键字段 | 添加颜色突出显示 | @Excel(name = "ID", index = 0, fontColor = IndexedColors.BLUE) |
可空字段 | 设置默认值 | @Excel(name = "备注", index = 5, defaultValue = "无") |
分组字段 | 标记为key | @Excel(name = "部门", index = 0, isKey = true) |
2️⃣ 转换器使用建议
场景 | 建议 | 原因 |
---|
简单映射 | 使用静态样式 | 性能更好,配置简单 |
复杂逻辑 | 使用转换器 | 灵活性更强,支持动态样式 |
性能敏感 | 缓存转换结果 | 避免重复计算 |
3️⃣ 样式配置建议
原则 | 说明 | 示例 |
---|
对比鲜明 | 确保文字清晰可读 | 深色背景配浅色文字 |
语义明确 | 用颜色表达含义 | 红色表示错误,绿色表示成功 |
数量适中 | 避免过多颜色混乱 | 建议不超过5种颜色组合 |
4️⃣ 错误处理建议
@Override
public Double convertFromExcel(String value, String[] params) {
try {
return value == null ? null : Double.parseDouble(value);
} catch (NumberFormatException e) {
log.warn("数值转换失败: {}", value);
return null;
}
}
注意事项
⚠️ 配置约束
- 索引唯一性:同一层级的字段
index
不能重复 - 类型匹配:
fieldType
与字段类型要匹配 - 转换器泛型:转换器泛型要与字段类型匹配
⚠️ 性能考虑
- 大数据量:建议分批处理,单次不超过10000条记录
- 复杂样式:过多动态样式可能影响性能
- 内存使用:注意大文件导入的内存占用
⚠️ 兼容性说明
- POI版本:基于Apache POI 5.x开发
- Java版本:需要Java 8+
- 文件格式:支持
.xlsx
格式,建议不使用.xls
源码
package org.whh.excel.annotation;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.whh.excel.converter.ExcelConverter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Excel {
String name() default "";
int index() default -1;
int width() default 20;
boolean required() default false;
String dateFormat() default "yyyy-MM-dd HH:mm:ss";
String numberFormat() default "";
String defaultValue() default "";
boolean isKey() default false;
IndexedColors fontColor() default IndexedColors.AUTOMATIC;
IndexedColors backgroundColor() default IndexedColors.AUTOMATIC;
Class<
? extends ExcelConverter<
?, ?>
> converter() default DefaultConverter.class;
String[] params() default {
};
Class<
?> type() default Object.class;
int order() default 0;
String description() default "";
FieldType fieldType() default FieldType.COLUMN;
enum FieldType {
COLUMN,
OBJECT,
COLLECTION
}
class DefaultConverter
implements ExcelConverter<
Object, Object> {
@Override
public Object convertToExcel(Object value, String[] params) {
return value;
}
@Override
public Object convertFromExcel(Object value, String[] params) {
return value;
}
@Override
public org.whh.excel.converter.CellStyleInfo getCellStyle(Object value, String[] params) {
return null;
}
}
}
package org.whh.excel.converter;
import lombok.Data;
import org.apache.poi.ss.usermodel.IndexedColors;
@Data
public class CellStyleInfo
{
private IndexedColors fontColor;
private IndexedColors backgroundColor;
public CellStyleInfo() {
}
public CellStyleInfo(IndexedColors fontColor, IndexedColors backgroundColor) {
this.fontColor = fontColor;
this.backgroundColor = backgroundColor;
}
public static CellStyleInfo withFontColor(IndexedColors fontColor) {
return new CellStyleInfo(fontColor, null);
}
public static CellStyleInfo withBackgroundColor(IndexedColors backgroundColor) {
return new CellStyleInfo(null, backgroundColor);
}
public static CellStyleInfo withColors(IndexedColors fontColor, IndexedColors backgroundColor) {
return new CellStyleInfo(fontColor, backgroundColor);
}
public boolean hasFontColor() {
return fontColor != null && fontColor != IndexedColors.AUTOMATIC;
}
public boolean hasBackgroundColor() {
return backgroundColor != null && backgroundColor != IndexedColors.AUTOMATIC;
}
public boolean hasAnyStyle() {
return hasFontColor() || hasBackgroundColor();
}
}
package org.whh.excel.converter;
public interface ExcelConverter<
T, E> {
E convertToExcel(T value, String[] params);
T convertFromExcel(E value, String[] params);
default CellStyleInfo getCellStyle(T value, String[] params) {
return null;
}
}
package org.whh.excel.converter;
import org.apache.poi.ss.usermodel.IndexedColors;
public class GenderConverter
implements ExcelConverter<
String, String> {
@Override
public String convertToExcel(String value, String[] params) {
if ("1".equals(value)) {
return "男";
} else if ("0".equals(value)) {
return "女";
}
return value;
}
@Override
public String convertFromExcel(String value, String[] params) {
if ("男".equals(value)) {
return "1";
} else if ("女".equals(value)) {
return "0";
}
return value;
}
@Override
public CellStyleInfo getCellStyle(String value, String[] params) {
if ("1".equals(value)) {
return CellStyleInfo.withColors(IndexedColors.WHITE, IndexedColors.BLUE);
} else if ("0".equals(value)) {
return CellStyleInfo.withColors(IndexedColors.BLACK, IndexedColors.PINK);
}
return null;
}
}
package org.whh.excel.util;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.whh.excel.annotation.Excel;
import org.whh.excel.converter.CellStyleInfo;
import org.whh.excel.converter.ExcelConverter;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ExcelUtils
{
private static final IndexedColors HEADER_BACKGROUND_COLOR = IndexedColors.AUTOMATIC;
private static final IndexedColors HEADER_FONT_COLOR = IndexedColors.BLACK;
private static final short HEADER_FONT_SIZE = 12;
private static final boolean HEADER_FONT_BOLD = true;
private static final BorderStyle HEADER_BORDER_STYLE = BorderStyle.THIN;
private static final IndexedColors HEADER_BORDER_COLOR = IndexedColors.BLACK;
public static <
T> void exportExcel(List<
T> data, Class<
T> clazz, String fileName, HttpServletResponse response) {
clearStyleCache();
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet();
List<
FieldInfo> allFields = parseEntityStructure(clazz);
createHeader(sheet, allFields);
int currentRow = 1;
for (T item : data) {
currentRow = fillMultiLevelData(sheet, item, allFields, currentRow);
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName + ".xlsx", "UTF-8"));
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
workbook.write(response.getOutputStream());
log.info("Excel导出成功,文件名: {}, 数据量: {} 条", fileName, data.size());
} catch (org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {
log.info("客户端连接中断 (可能是多线程下载工具如迅雷等导致): {}", e.getMessage());
} catch (java.io.IOException e) {
if (e.getMessage() != null &&
(e.getMessage().contains("你的主机中的软件中止了一个已建立的连接") || e.getMessage().contains("Connection reset") || e.getMessage().contains("Broken pipe"))) {
log.info("客户端网络连接中断 (通常是多线程下载工具如迅雷等的正常行为,文件仍会正确下载): {}", e.getMessage());
} else {
log.error("Excel导出时发生IO异常", e);
throw new RuntimeException("Excel导出失败", e);
}
} catch (org.apache.poi.openxml4j.exceptions.OpenXML4JRuntimeException e) {
Throwable cause = e.getCause();
if (cause instanceof org.apache.poi.openxml4j.exceptions.OpenXML4JException) {
String message = e.getMessage();
if (message != null &&
(message.contains("failed to be saved in the stream") || message.contains("Fail to save"))) {
log.info("Excel文件保存过程中客户端连接中断 (通常是多线程下载工具导致,文件会正确下载): {}", message);
return;
}
}
log.error("Excel导出失败", e);
throw new RuntimeException("Excel导出失败", e);
} catch (Exception e) {
log.error("Excel导出失败", e);
throw new RuntimeException("Excel导出失败", e);
}
}
public static <
T> List<
T> importExcel(InputStream inputStream, Class<
T> clazz) {
List<
T> result;
double originalMinInflateRatio = ZipSecureFile.getMinInflateRatio();
try {
ZipSecureFile.setMinInflateRatio(0.005);
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
Sheet sheet = workbook.getSheetAt(0);
List<
FieldInfo> allFields = parseEntityStructure(clazz);
result = parseMultiLevelData(sheet, clazz, allFields);
log.info("Excel导入成功,共导入 {} 条记录", result.size());
}
} catch (Exception e) {
log.error("Excel导入失败", e);
throw new RuntimeException("Excel导入失败", e);
} finally {
ZipSecureFile.setMinInflateRatio(originalMinInflateRatio);
log.debug("已恢复POI安全设置,压缩率限制: {}", originalMinInflateRatio);
}
return result;
}
private static <
T> List<
FieldInfo> parseEntityStructure(Class<
T> clazz) {
List<
FieldInfo> allFields = new ArrayList<
>();
collectAllFields(clazz, allFields, 0);
allFields.sort((a, b) ->
{
if (a.level != b.level) {
return Integer.compare(a.level, b.level);
}
return Integer.compare(a.columnIndex, b.columnIndex);
});
int currentIndex = 0;
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {
fieldInfo.columnIndex = currentIndex++;
}
}
return allFields;
}
private static void collectAllFields(Class<
?> clazz, List<
FieldInfo> allFields, int level) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Excel excel = field.getAnnotation(Excel.class)
;
if (excel != null) {
FieldInfo fieldInfo = new FieldInfo();
fieldInfo.field = field;
fieldInfo.excel = excel;
fieldInfo.level = level;
fieldInfo.fieldType = excel.fieldType();
if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {
fieldInfo.columnIndex = excel.index() != -1 ? excel.index() : Integer.MAX_VALUE;
}
if (excel.converter() != Excel.DefaultConverter.class)
{
try {
@SuppressWarnings("unchecked") ExcelConverter<
Object, Object> converter = (ExcelConverter<
Object, Object>) excel.converter().getDeclaredConstructor().newInstance();
fieldInfo.converter = converter;
} catch (Exception e) {
log.warn("初始化转换器失败: {}", excel.converter().getName());
}
}
allFields.add(fieldInfo);
if (fieldInfo.fieldType == Excel.FieldType.OBJECT || fieldInfo.fieldType == Excel.FieldType.COLLECTION) {
Class<
?> actualType = getActualType(field);
if (actualType != Object.class)
{
collectAllFields(actualType, allFields, level + 1);
}
}
}
}
}
private static Class<
?> getActualType(Field field) {
Excel excel = field.getAnnotation(Excel.class)
;
if (excel != null && excel.type() != Object.class)
{
return excel.type();
}
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericType;
Type[] actualTypeArguments = paramType.getActualTypeArguments();
if (actualTypeArguments.length >
0) {
return (Class<
?>) actualTypeArguments[0];
}
}
return field.getType();
}
private static void createHeader(Sheet sheet, List<
FieldInfo> allFields) {
Row headerRow = sheet.createRow(0);
CellStyle headerStyle = createHeaderStyle(sheet.getWorkbook());
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {
Cell cell = headerRow.createCell(fieldInfo.columnIndex);
String headerName = "";
if (fieldInfo.excel != null &&
StringUtils.isNotBlank(fieldInfo.excel.name())) {
headerName = fieldInfo.excel.name();
} else {
headerName = fieldInfo.field.getName();
}
cell.setCellValue(headerName);
cell.setCellStyle(headerStyle);
if (fieldInfo.excel != null && fieldInfo.excel.width() >
0) {
sheet.setColumnWidth(fieldInfo.columnIndex, fieldInfo.excel.width() * 256);
} else {
sheet.setColumnWidth(fieldInfo.columnIndex, 20 * 256);
}
}
}
}
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle headerStyle = workbook.createCellStyle();
Font headerFont = workbook.createFont();
headerFont.setBold(HEADER_FONT_BOLD);
headerFont.setFontHeightInPoints(HEADER_FONT_SIZE);
headerFont.setColor(HEADER_FONT_COLOR.getIndex());
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(HEADER_BACKGROUND_COLOR.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headerStyle.setBorderTop(HEADER_BORDER_STYLE);
headerStyle.setBorderBottom(HEADER_BORDER_STYLE);
headerStyle.setBorderLeft(HEADER_BORDER_STYLE);
headerStyle.setBorderRight(HEADER_BORDER_STYLE);
headerStyle.setTopBorderColor(HEADER_BORDER_COLOR.getIndex());
headerStyle.setBottomBorderColor(HEADER_BORDER_COLOR.getIndex());
headerStyle.setLeftBorderColor(HEADER_BORDER_COLOR.getIndex());
headerStyle.setRightBorderColor(HEADER_BORDER_COLOR.getIndex());
return headerStyle;
}
private static final Map<
String, CellStyle> styleCache = new ConcurrentHashMap<
>();
private static void clearStyleCache() {
styleCache.clear();
}
private static CellStyle createCellStyle(Workbook workbook, FieldInfo fieldInfo, Object value) {
IndexedColors fontColor = fieldInfo.excel.fontColor();
IndexedColors backgroundColor = fieldInfo.excel.backgroundColor();
if (fieldInfo.converter != null) {
try {
CellStyleInfo styleInfo = fieldInfo.converter.getCellStyle(value, fieldInfo.excel.params());
if (styleInfo != null) {
if (styleInfo.hasFontColor()) {
fontColor = styleInfo.getFontColor();
}
if (styleInfo.hasBackgroundColor()) {
backgroundColor = styleInfo.getBackgroundColor();
}
}
} catch (Exception e) {
log.warn("获取动态样式失败: {}", e.getMessage());
}
}
boolean needStyle = fontColor != IndexedColors.AUTOMATIC || backgroundColor != IndexedColors.AUTOMATIC;
if (!needStyle) {
return null;
}
String cacheKey = fontColor.name() + "_" + backgroundColor.name();
CellStyle cachedStyle = styleCache.get(cacheKey);
if (cachedStyle != null) {
return cachedStyle;
}
CellStyle cellStyle = workbook.createCellStyle();
if (fontColor != IndexedColors.AUTOMATIC) {
Font font = workbook.createFont();
font.setColor(fontColor.getIndex());
cellStyle.setFont(font);
}
if (backgroundColor != IndexedColors.AUTOMATIC) {
cellStyle.setFillForegroundColor(backgroundColor.getIndex());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
}
styleCache.put(cacheKey, cellStyle);
return cellStyle;
}
private static int fillMultiLevelData(Sheet sheet, Object rootItem, List<
FieldInfo> allFields, int startRow) {
try {
List<
Map<
String, Object>
> dataRows = expandToFlatRows(rootItem, allFields);
for (int i = 0; i < dataRows.size(); i++) {
Row row = sheet.createRow(startRow + i);
Map<
String, Object> rowData = dataRows.get(i);
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {
Cell cell = row.createCell(fieldInfo.columnIndex);
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
Object value = rowData.get(uniqueKey);
String cellValue = convertToExcelValue(value, fieldInfo);
cell.setCellValue(cellValue);
CellStyle cellStyle = createCellStyle(sheet.getWorkbook(), fieldInfo, value);
if (cellStyle != null) {
cell.setCellStyle(cellStyle);
}
}
}
}
if (dataRows.size() >
1) {
createMergeRegions(sheet, rootItem, allFields, startRow, dataRows.size());
}
return startRow + dataRows.size();
} catch (Exception e) {
log.error("填充多级数据失败", e);
return startRow + 1;
}
}
private static List<
Map<
String, Object>
> expandToFlatRows(Object item, List<
FieldInfo> allFields) {
List<
Map<
String, Object>
> result = new ArrayList<
>();
try {
expandObjectToRows(item, new HashMap<
>(), allFields, result);
} catch (Exception e) {
log.warn("展开对象失败", e);
}
return result.isEmpty() ? List.of(new HashMap<
>()) : result;
}
private static void expandObjectToRows(Object item, Map<
String, Object> parentData, List<
FieldInfo> allFields, List<
Map<
String, Object>
> result) throws Exception {
Map<
String, Object> currentData = new HashMap<
>(parentData);
List<
Object> childItems = new ArrayList<
>();
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.field.getDeclaringClass().isAssignableFrom(item.getClass())) {
Object value = fieldInfo.field.get(item);
if (fieldInfo.fieldType == Excel.FieldType.COLLECTION && value instanceof Collection) {
childItems.addAll((Collection<
?>) value);
} else if (fieldInfo.fieldType == Excel.FieldType.COLUMN) {
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
currentData.put(uniqueKey, value);
}
}
}
if (!childItems.isEmpty()) {
for (Object childItem : childItems) {
expandObjectToRows(childItem, currentData, allFields, result);
}
} else {
result.add(currentData);
}
}
private static void createMergeRegions(Sheet sheet, Object rootItem, List<
FieldInfo> allFields, int startRow, int totalRows) {
try {
HierarchyNode rootNode = buildHierarchyTree(rootItem, allFields, startRow);
createMergeRegionsRecursively(sheet, rootNode, allFields);
} catch (Exception e) {
log.warn("创建合并单元格失败", e);
}
}
private static HierarchyNode buildHierarchyTree(Object rootItem, List<
FieldInfo> allFields, int startRow) {
HierarchyNode rootNode = new HierarchyNode();
rootNode.item = rootItem;
rootNode.level = 0;
rootNode.startRow = startRow;
buildHierarchyTreeRecursively(rootNode, allFields);
return rootNode;
}
private static void buildHierarchyTreeRecursively(HierarchyNode node, List<
FieldInfo> allFields) {
try {
List<
Object> childItems = new ArrayList<
>();
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.field.getDeclaringClass().isAssignableFrom(node.item.getClass()) && fieldInfo.fieldType == Excel.FieldType.COLLECTION) {
Object value = fieldInfo.field.get(node.item);
if (value instanceof Collection) {
childItems.addAll((Collection<
?>) value);
}
break;
}
}
int currentRow = node.startRow;
for (Object childItem : childItems) {
HierarchyNode childNode = new HierarchyNode();
childNode.item = childItem;
childNode.level = node.level + 1;
childNode.startRow = currentRow;
childNode.parent = node;
node.children.add(childNode);
buildHierarchyTreeRecursively(childNode, allFields);
childNode.rowCount = calculateNodeRowCount(childNode);
currentRow += childNode.rowCount;
}
if (childItems.isEmpty()) {
node.rowCount = 1;
} else {
node.rowCount = node.children.stream().mapToInt(child -> child.rowCount).sum();
}
} catch (Exception e) {
log.warn("构建层级树失败", e);
node.rowCount = 1;
}
}
private static int calculateNodeRowCount(HierarchyNode node) {
if (node.children.isEmpty()) {
return 1;
}
return node.children.stream().mapToInt(child -> child.rowCount).sum();
}
private static void createMergeRegionsRecursively(Sheet sheet, HierarchyNode node, List<
FieldInfo> allFields) {
try {
if (node.rowCount >
1) {
List<
FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == node.level).toList();
for (FieldInfo fieldInfo : currentLevelFields) {
CellRangeAddress mergeRegion = new CellRangeAddress(node.startRow, node.startRow + node.rowCount - 1, fieldInfo.columnIndex, fieldInfo.columnIndex);
sheet.addMergedRegion(mergeRegion);
setCellStyle(sheet, node.startRow, fieldInfo.columnIndex);
}
}
for (HierarchyNode child : node.children) {
createMergeRegionsRecursively(sheet, child, allFields);
}
} catch (Exception e) {
log.warn("递归创建合并区域失败", e);
}
}
private static class HierarchyNode
{
Object item;
int level;
int startRow;
int rowCount;
HierarchyNode parent;
List<
HierarchyNode> children = new ArrayList<
>();
}
private static void setCellStyle(Sheet sheet, int row, int col) {
try {
Row sheetRow = sheet.getRow(row);
if (sheetRow != null) {
Cell cell = sheetRow.getCell(col);
if (cell != null) {
CellStyle existingStyle = cell.getCellStyle();
CellStyle newStyle = sheet.getWorkbook().createCellStyle();
if (existingStyle != null && existingStyle.getIndex() != 0) {
newStyle.cloneStyleFrom(existingStyle);
}
newStyle.setAlignment(HorizontalAlignment.CENTER);
newStyle.setVerticalAlignment(VerticalAlignment.CENTER);
cell.setCellStyle(newStyle);
}
}
} catch (Exception e) {
log.warn("设置单元格样式失败", e);
}
}
private static String convertToExcelValue(Object value, FieldInfo fieldInfo) {
if (value == null) {
return fieldInfo.excel != null ? fieldInfo.excel.defaultValue() : "";
}
if (fieldInfo.converter != null) {
try {
Object converted = fieldInfo.converter.convertToExcel(value, fieldInfo.excel.params());
return String.valueOf(converted);
} catch (Exception e) {
log.warn("字段转换失败: {}", fieldInfo.field.getName());
}
}
if (value instanceof Date && fieldInfo.excel != null &&
StringUtils.isNotBlank(fieldInfo.excel.dateFormat())) {
SimpleDateFormat sdf = new SimpleDateFormat(fieldInfo.excel.dateFormat());
return sdf.format((Date) value);
}
return String.valueOf(value);
}
private static <
T> List<
T> parseMultiLevelData(Sheet sheet, Class<
T> clazz, List<
FieldInfo> allFields) {
List<
T> result = new ArrayList<
>();
try {
if (sheet.getPhysicalNumberOfRows() <= 1) {
log.warn("Excel文件没有数据行");
return result;
}
Row headerRow = sheet.getRow(0);
Map<
String, Integer> headerMap = parseHeader(headerRow, allFields);
List<
Map<
String, Object>
> rowDataList = readAllRows(sheet, headerMap, allFields);
if (rowDataList.isEmpty()) {
log.warn("没有读取到任何数据");
return result;
}
result = buildMultiLevelObjects(rowDataList, clazz, allFields);
} catch (Exception e) {
log.error("解析Excel数据失败", e);
throw new RuntimeException("解析Excel数据失败", e);
}
return result;
}
private static Map<
String, Integer> parseHeader(Row headerRow, List<
FieldInfo> allFields) {
Map<
String, Integer> headerMap = new HashMap<
>();
if (headerRow != null) {
List<
FieldInfo> sortedFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.excel != null).sorted((a, b) ->
Integer.compare(a.level, b.level)).toList();
Map<
String, Set<
Integer>
> usedLevels = new HashMap<
>();
for (int i = 0; i < headerRow.getPhysicalNumberOfCells(); i++) {
Cell cell = headerRow.getCell(i);
if (cell != null) {
String headerName = cell.getStringCellValue();
for (FieldInfo fieldInfo : sortedFields) {
if (headerName.equals(fieldInfo.excel.name())) {
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
if (!headerMap.containsKey(uniqueKey)) {
usedLevels.computeIfAbsent(headerName, k ->
new HashSet<
>()).add(fieldInfo.level);
headerMap.put(uniqueKey, i);
break;
}
}
}
}
}
}
log.debug("解析表头完成,映射关系: {}", headerMap);
return headerMap;
}
private static List<
Map<
String, Object>
> readAllRows(Sheet sheet, Map<
String, Integer> headerMap, List<
FieldInfo> allFields) {
List<
Map<
String, Object>
> rowDataList = new ArrayList<
>();
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
Map<
String, Object> rowData = new HashMap<
>();
boolean hasData = false;
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.fieldType == Excel.FieldType.COLUMN && fieldInfo.excel != null) {
String fieldName = fieldInfo.field.getName();
String uniqueKey = "L" + fieldInfo.level + "_" + fieldName;
Integer colIndex = headerMap.get(uniqueKey);
if (colIndex != null) {
Cell cell = row.getCell(colIndex);
Object value = getCellValue(cell, fieldInfo);
rowData.put(uniqueKey, value);
if (value != null &&
!value.toString().trim().isEmpty()) {
hasData = true;
}
}
}
}
if (hasData) {
rowData.put("_rowIndex", i);
rowDataList.add(rowData);
}
}
fillMergedCellValues(rowDataList, allFields);
return rowDataList;
}
private static void fillMergedCellValues(List<
Map<
String, Object>
> rowDataList, List<
FieldInfo> allFields) {
if (rowDataList.isEmpty()) return;
Map<
Integer, List<
String>
> levelFields = new HashMap<
>();
for (FieldInfo fieldInfo : allFields) {
if (fieldInfo.fieldType == Excel.FieldType.COLUMN && fieldInfo.excel != null) {
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
levelFields.computeIfAbsent(fieldInfo.level, k ->
new ArrayList<
>()).add(uniqueKey);
}
}
int maxLevel = levelFields.keySet().stream().mapToInt(Integer::intValue).max().orElse(0);
for (int currentLevel = 0; currentLevel < maxLevel; currentLevel++) {
List<
String> currentLevelFieldKeys = levelFields.get(currentLevel);
if (currentLevelFieldKeys == null || currentLevelFieldKeys.isEmpty()) continue;
List<
String> currentLevelKeyFields = getKeyFieldNames(allFields, currentLevel);
Map<
String, List<
Map<
String, Object>
>
> currentLevelGroups = new LinkedHashMap<
>();
for (Map<
String, Object> rowData : rowDataList) {
String levelKey = buildObjectKey(rowData, currentLevelKeyFields, currentLevel);
currentLevelGroups.computeIfAbsent(levelKey, k ->
new ArrayList<
>()).add(rowData);
}
for (Map.Entry<
String, List<
Map<
String, Object>
>
> levelGroup : currentLevelGroups.entrySet()) {
String levelKey = levelGroup.getKey();
List<
Map<
String, Object>
> levelRowData = levelGroup.getValue();
for (String fieldKey : currentLevelFieldKeys) {
boolean hasEmptyValue = false;
Object masterValue = null;
for (Map<
String, Object> rowData : levelRowData) {
Object currentValue = rowData.get(fieldKey);
if (currentValue != null &&
!currentValue.toString().trim().isEmpty()) {
if (masterValue == null) {
masterValue = currentValue;
}
} else {
hasEmptyValue = true;
}
}
if (masterValue != null && hasEmptyValue) {
int filledCount = 0;
for (Map<
String, Object> rowData : levelRowData) {
Object currentValue = rowData.get(fieldKey);
if (currentValue == null || currentValue.toString().trim().isEmpty()) {
rowData.put(fieldKey, masterValue);
filledCount++;
}
}
}
}
}
}
}
private static Object getCellValue(Cell cell, FieldInfo fieldInfo) {
if (cell == null) return null;
try {
Sheet sheet = cell.getSheet();
int rowIndex = cell.getRowIndex();
int colIndex = cell.getColumnIndex();
for (CellRangeAddress range : sheet.getMergedRegions()) {
if (range.isInRange(rowIndex, colIndex)) {
Row firstRow = sheet.getRow(range.getFirstRow());
if (firstRow != null) {
Cell firstCell = firstRow.getCell(range.getFirstColumn());
if (firstCell != null && firstCell != cell) {
return getCellValue(firstCell, fieldInfo);
}
}
}
}
switch (cell.getCellType()) {
case STRING:
String stringValue = cell.getStringCellValue();
return convertCellValue(stringValue, fieldInfo);
case NUMERIC:
double numericValue = cell.getNumericCellValue();
if (fieldInfo.field.getType() == Date.class)
{
return cell.getDateCellValue();
} else if (fieldInfo.field.getType() == Integer.class || fieldInfo.field.
getType() == int.class)
{
return (int) numericValue;
}
return numericValue;
case BOOLEAN:
return cell.getBooleanCellValue();
default:
return null;
}
} catch (Exception e) {
log.warn("读取单元格值失败: {}", e.getMessage());
return null;
}
}
private static Object convertCellValue(String value, FieldInfo fieldInfo) {
if (value == null || value.trim().isEmpty()) return null;
Class<
?> fieldType = fieldInfo.field.getType();
try {
if (fieldInfo.converter != null) {
return fieldInfo.converter.convertFromExcel(value, new String[0]);
}
if (fieldType == String.class)
{
return value;
} else if (fieldType == Integer.class || fieldType ==
int.class)
{
return Integer.parseInt(value);
} else if (fieldType == Long.class || fieldType ==
long.class)
{
return Long.parseLong(value);
} else if (fieldType == Double.class || fieldType ==
double.class)
{
return Double.parseDouble(value);
} else if (fieldType == Boolean.class || fieldType ==
boolean.class)
{
return Boolean.parseBoolean(value);
} else if (fieldType == Date.class)
{
SimpleDateFormat[] formats = {
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), new SimpleDateFormat("yyyy-MM-dd"), new SimpleDateFormat("yyyy/MM/dd")
};
for (SimpleDateFormat format : formats) {
try {
return format.parse(value);
} catch (Exception ignored) {
}
}
}
return value;
} catch (Exception e) {
log.warn("转换单元格值失败: {} -> {}", value, fieldType.getSimpleName());
return value;
}
}
private static <
T> List<
T> buildMultiLevelObjects(List<
Map<
String, Object>
> rowDataList, Class<
T> clazz, List<
FieldInfo> allFields) {
List<
T> result = new ArrayList<
>();
try {
int maxLevel = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN).mapToInt(f -> f.level).max().orElse(0);
log.debug("最大层级深度: {}", maxLevel);
Map<
String, List<
Map<
String, Object>
>
> groupedData = groupDataByHierarchy(rowDataList, allFields);
for (Map.Entry<
String, List<
Map<
String, Object>
>
> entry : groupedData.entrySet()) {
T rootObject = buildSingleObject(entry.getValue(), clazz, allFields, 0);
if (rootObject != null) {
result.add(rootObject);
}
}
} catch (Exception e) {
log.error("构建多级对象失败", e);
}
return result;
}
private static Map<
String, List<
Map<
String, Object>
>
> groupDataByHierarchy(List<
Map<
String, Object>
> rowDataList, List<
FieldInfo> allFields) {
Map<
String, List<
Map<
String, Object>
>
> groupedData = new LinkedHashMap<
>();
List<
String> rootKeyFieldNames = getKeyFieldNames(allFields, 0);
for (Map<
String, Object> rowData : rowDataList) {
String key = buildObjectKey(rowData, rootKeyFieldNames, 0);
groupedData.computeIfAbsent(key, k ->
new ArrayList<
>()).add(rowData);
}
return groupedData;
}
private static List<
String> getKeyFieldNames(List<
FieldInfo> allFields, int level) {
List<
String> keyFieldNames = new ArrayList<
>();
List<
FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();
for (FieldInfo field : currentLevelFields) {
Excel excel = field.excel;
if (excel != null && excel.isKey()) {
keyFieldNames.add(field.field.getName());
}
}
if (keyFieldNames.isEmpty() &&
!currentLevelFields.isEmpty()) {
keyFieldNames.add(currentLevelFields.get(0).field.getName());
}
return keyFieldNames;
}
private static String buildObjectKey(Map<
String, Object> rowData, List<
String> keyFieldNames, int level) {
StringBuilder key = new StringBuilder();
for (String fieldName : keyFieldNames) {
String uniqueKey = "L" + level + "_" + fieldName;
Object value = rowData.get(uniqueKey);
key.append(value != null ? value.toString() : "null").append("|");
}
return key.toString();
}
private static <
T> T buildSingleObject(List<
Map<
String, Object>
> rowDataList, Class<
T> clazz, List<
FieldInfo> allFields, int level) {
try {
T object = clazz.getDeclaredConstructor().newInstance();
Map<
String, Object> representativeRow = findRepresentativeRow(rowDataList, allFields, level);
List<
FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();
for (FieldInfo fieldInfo : currentLevelFields) {
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
Object value = representativeRow.get(uniqueKey);
if (value != null) {
fieldInfo.field.set(object, value);
}
}
List<
FieldInfo> childCollectionFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLLECTION && f.level == level).toList();
for (FieldInfo collectionField : childCollectionFields) {
Class<
?> elementType = getActualType(collectionField.field);
Map<
String, List<
Map<
String, Object>
>
> childGroups = groupChildData(rowDataList, allFields, level + 1);
List<
Object> childObjects = new ArrayList<
>();
for (List<
Map<
String, Object>
> childRowData : childGroups.values()) {
Object childObject = buildSingleObject(childRowData, elementType, allFields, level + 1);
if (childObject != null) {
childObjects.add(childObject);
}
}
collectionField.field.set(object, childObjects);
}
return object;
} catch (Exception e) {
log.error("构建对象失败: {}", e.getMessage());
return null;
}
}
private static Map<
String, Object> findRepresentativeRow(List<
Map<
String, Object>
> rowDataList, List<
FieldInfo> allFields, int level) {
if (rowDataList.isEmpty()) {
return new HashMap<
>();
}
if (rowDataList.size() == 1) {
return rowDataList.get(0);
}
List<
FieldInfo> currentLevelFields = allFields.stream().filter(f -> f.fieldType == Excel.FieldType.COLUMN && f.level == level).toList();
Map<
String, Object> representativeRow = new HashMap<
>(rowDataList.get(0));
for (FieldInfo fieldInfo : currentLevelFields) {
String uniqueKey = "L" + fieldInfo.level + "_" + fieldInfo.field.getName();
Object currentValue = representativeRow.get(uniqueKey);
if (currentValue == null || currentValue.toString().trim().isEmpty()) {
for (Map<
String, Object> rowData : rowDataList) {
Object value = rowData.get(uniqueKey);
if (value != null &&
!value.toString().trim().isEmpty()) {
representativeRow.put(uniqueKey, value);
break;
}
}
}
}
return representativeRow;
}
private static Map<
String, List<
Map<
String, Object>
>
> groupChildData(List<
Map<
String, Object>
> rowDataList, List<
FieldInfo> allFields, int childLevel) {
Map<
String, List<
Map<
String, Object>
>
> childGroups = new LinkedHashMap<
>();
List<
String> childKeyFieldNames = getKeyFieldNames(allFields, childLevel);
for (Map<
String, Object> rowData : rowDataList) {
String key = buildObjectKey(rowData, childKeyFieldNames, childLevel);
childGroups.computeIfAbsent(key, k ->
new ArrayList<
>()).add(rowData);
}
return childGroups;
}
private static class FieldInfo
{
Field field;
Excel excel;
ExcelConverter<
Object, Object> converter;
int level;
int columnIndex;
Excel.FieldType fieldType;
}
}
相关链接
最后更新:2025-08-25