Excel 日期格式统一治理:从"显示不全"到"自动兼容"的完整方案
问题的三张面孔
在项目落地过程中,Excel 日期格式暴露了三个不同层面的问题:
| 问题 | 表现 | 根因 |
|---|---|---|
| 日期显示不全 | yyyy-MM-dd HH:mm:ss 只显示了 yyyy-MM-dd |
@ColumnWidth(18) + setWrapText(true) 导致时间部分被换行截断 |
| 动态列导出缺少时间 | 使用 List<List<Object>> 动态写法,时间部分丢失 | EasyExcel 无法读取字段上的 @DateTimeFormat 注解 |
| 导入混合格式异常 | Excel 中同一列有 2026/1/31(数值型)和 2026-02-02(文本型) |
EasyExcel 默认转换器只支持单一文本格式 |
下面逐一拆解每个问题的排查过程与修复方案。
问题一:日期显示不全------"丢失"的时间部分
现象
用户反馈:导出 Excel 中「录入时间」列只显示 2026-05-15,缺少时间部分 HH:mm:ss。
查看 POJO 定义:
java
@ColumnWidth(18)
@ExcelProperty("录入时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date lrsj;
注解定义完全正确,为什么 EasyExcel 不按注解输出?
排查过程
检查 EnhancedExportUtil 中动态列导出路径 exportWithConfigs 的代码:
java
// 动态列路径:表头和数据都通过 List<List<Object>> 传递
List<List<Object>> batchRows = buildRowsFromObjects(batch, plan.fields, fieldMap);
excelWriter.write(batchRows, writeSheet);
关键发现:excelWriter.write(batchRows, writeSheet) 是 List 形式写入,EasyExcel 此时直接使用 POI 底层 API 设置单元格值,不会扫描字段上的 @DateTimeFormat 注解。
而在 buildRowsFromObjects 方法中,通过反射拿到的 Date 对象被直接放入 List:
java
Object value = field.get(item); // Date 对象
cells.add(value); // 直接放入 List
EasyExcel 拿到 Date 类型后,使用默认的序列化方式(调用 toString())输出,只输出了日期部分。
修复方案:formatValue 统一格式化
在 EnhancedExportUtil 中添加 formatValue 方法,将所有日期类型统一格式化为字符串:
java
private static Object formatValue(Object value) {
if (value == null) return null;
if (value instanceof Date) {
return LocalDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault())
.format(LOCAL_DATE_TIME_FMT);
}
if (value instanceof LocalDateTime) {
return ((LocalDateTime) value).format(LOCAL_DATE_TIME_FMT);
}
if (value instanceof LocalDate) {
return ((LocalDate) value).format(LOCAL_DATE_FMT);
}
if (value instanceof LocalTime) {
return ((LocalTime) value).format(LOCAL_TIME_FMT);
}
return value;
}
这样所有日期类型都会输出格式化的字符串,而非原始 Date 对象。
问题二:动态列导出缺少时间------注解失效的真相
现象
同样是 lrsj 字段,在无列配置的默认导出路径 (exportDefaultByClass)下时间正常显示,切换到动态列路径 (exportWithConfigs)后时间丢失。
根因分析
EnhancedExportUtil 的两种导出路径对日期注解的处理不同:
| 导出路径 | 写入方式 | 注解支持 |
|---|---|---|
exportDefaultByClass |
EasyExcel.write(..., excelClass).doWrite(list) |
✅ 类写法,EasyExcel 扫描 @DateTimeFormat 自动格式化 |
exportWithConfigs |
excelWriter.write(batchRows, writeSheet) 传 List |
❌ 动态写法,无法读取 @DateTimeFormat 注解 |
修复方案
对 exportDefaultByClass 路径,确保使用类写法而非手动构建行数据:
java
// ❌ 错误:写 List 方式丢失注解
List<List<Object>> rows = buildRows(...);
excelWriter.write(rows, writeSheet);
// ✅ 正确:类写法,EasyExcel 自动处理注解
EasyExcel.write(response.getOutputStream(), excelClass)
.sheet(fileName)
.doWrite(dataList);
对于动态列路径,统一使用 formatValue 手动格式化日期。
问题三:导入时混合日期格式异常------FlexibleDateConverter
现象
导入 Excel 时报错:
com.alibaba.excel.exception.ExcelDataConvertException:
Convert data exception, field: tjrq, value: 2026/1/31
检查发现:Excel 中「提交日期」列存在混合格式------2026/1/31 是 Excel 内部存储为数值型 日期,2026-02-02 是文本型 日期,@DateTimeFormat 默认转换器只支持单一格式。
解决方案:自定义 FlexibleDateConverter
创建一个通用日期转换器,同时支持数值型和文本型日期:
java
public class FlexibleDateConverter implements Converter<Date> {
private static final String[] DATE_FORMATS = {
"yyyy-MM-dd HH:mm:ss",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd",
"yyyy/MM/dd",
"yyyy/M/d",
"yyyy-M-d",
};
@Override
public Date convertToJavaData(ReadCellData<?> cellData,
ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
// 数值类型(Excel 内部日期存为 numeric)
if (cellData.getType() == CellDataTypeEnum.NUMBER) {
double numericValue = cellData.getNumberValue().doubleValue();
return DateUtil.getJavaDate(numericValue);
}
// 文本类型,尝试多种格式解析
String text = cellData.getStringValue();
if (text == null || text.trim().isEmpty()) return null;
text = text.trim();
for (String format : DATE_FORMATS) {
try {
SimpleDateFormat sdf = new SimpleDateFormat(format);
sdf.setLenient(false);
return sdf.parse(text);
} catch (Exception ignored) { }
}
throw new IllegalArgumentException("无法解析日期: '" + text + "'");
}
@Override
public WriteCellData<String> convertToExcelData(Date value, ...) {
if (value == null) return new WriteCellData<>("");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return new WriteCellData<>(sdf.format(value));
}
}
使用方式:
java
@ExcelProperty(value = "提交日期", converter = FlexibleDateConverter.class)
private Date tjrq;
四、另一个隐藏的"显示不全":wrapText 截断
还有一个日期显示不全的案例,根因完全不同。
现象
用户反馈「录入时间」列只显示到 2026-05-15,时间部分完全看不到,但不是丢失,而是被截断了。
排查
查看 POI 生成的 Excel 单元格:
列宽 18 字符 × 不换行 → 日期时间 "2026-05-15 14:30:00" = 19 字符
当 @ColumnWidth(18) 设置列宽为 18 字符,而日期时间格式化后是 19 字符(含空格),并且样式开启了 setWrapText(true):
java
// 隔行变色样式中开启了自动换行
cellStyle.setWrapText(true);
自动换行后 :2026-05-15 在第一行显示,14:30:00 被换到第二行。但由于行高固定为 14pt(约 18px),第二行被截断不可见。
修复
移除所有数据行样式中 setWrapText(true) 调用:
java
// ❌ 删除以下三处
evenRowCellStyle.setWrapText(true);
oddRowCellStyle.setWrapText(true);
whiteBackgroundCellStyle.setWrapText(true);
统一治理方案总结
| 问题类型 | 根因 | 修复方案 | 适用范围 |
|---|---|---|---|
| 动态列导出日期丢失 | List 写入方式无法读取 @DateTimeFormat 注解 |
formatValue() 统一将 Date 格式化为字符串 |
所有动态列导出路径 |
| 默认导出日期显示不全 | @ColumnWidth + setWrapText(true) 导致换行截断 |
移除 setWrapText(true) |
隔行变色样式处理器 |
| 默认导出注解生效 | 类写法被替换为 List 写法 | 恢复类写法 doWrite(list) |
无列配置的默认导出 |
| 导入混合日期格式 | 数值型/文本型日期共存 | FlexibleDateConverter 自定义转换器 |
导入场景 |
两条路径的日期处理规则
导出工具内部有两条路径,规则如下:
exportDefaultByClass (无列配置)
└── 类写法 EasyExcel.write(xxx, excelClass).doWrite(list)
└── EasyExcel 自动扫描 @DateTimeFormat 注解
└── 必须使用类写法,避免退化为 List 模式
exportWithConfigs (有列配置)
└── 动态列,List<List<Object>> 写入
└── 使用 formatValue() 手动格式化所有日期类型
└── 默认输出格式 "yyyy-MM-dd HH:mm:ss"
同时建议在 Excel 实体类的日期字段上统一添加注解:
java
@ColumnWidth(22) // "yyyy-MM-dd HH:mm:ss" + 留白 = 22字符
@ExcelProperty("录入时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date lrsj;
这样无论是哪条路径,日期格式都能保持一致输出。
总结
Excel 日期格式的"混乱"本质上是工具类演进与注解机制不匹配 的产物。当导出功能从简单的 doWrite(list) 升级到动态列、分页写入时,EasyExcel 的注解机制不再自动生效。对此类问题的统一解决方案是:在数据转换层显式格式化日期,而不是依赖底层框架的隐式注解处理。