-
Notifications
You must be signed in to change notification settings - Fork 489
Description
Search before asking
- I searched in the issues and found nothing similar.
Fesod version
2.0.1-incubating
JDK version
21
Operating system
macos 26.4
Steps To Reproduce
Summary
Using the native POJO write path in org.apache.fesod:fesod-sheet:2.0.1-incubating
can trigger a StackOverflowError when:
- the DTO contains a
ZonedDateTimefield - a custom converter is registered for
ZonedDateTime - the DTO also uses normal field metadata such as:
@DateTimeFormat@NumberFormat@ExcelProperty(converter = ...)
In our project this blocks switching back to the pure native Fesod POJO export path
for ZonedDateTime-based exports.
Environment
- Java: project currently runs on Java 21
- Library:
org.apache.fesod:fesod-sheet:2.0.1-incubating - Output format: XLSX
Minimal Reproduction
Repository repro artifact:
- Test file:
package com.linzi.pitpat.excel;
import lombok.Data;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import org.apache.fesod.sheet.annotation.format.NumberFormat;
import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Documents an upstream Fesod bug we can reproduce reliably.
*
* <p>When using the native POJO write path with a DTO containing ZonedDateTime,
* Fesod 2.0.1-incubating triggers a StackOverflowError inside its write holder hashCode chain.
* We keep this test disabled because enabling it would break CI, but it provides a stable
* reproduction if we need to validate an upstream fix or a local dependency patch.</p>
*/
class FesodNativeMetadataRegressionTest {
@Test
@Disabled("Stable repro for upstream Fesod StackOverflowError on native POJO write path")
@DisplayName("native Fesod POJO export should reproduce StackOverflowError with ZonedDateTime metadata")
void nativePojoExport_shouldReproduceStackOverflowError() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
assertThrows(StackOverflowError.class, () -> FesodSheet.write(outputStream, NativeMixedMetadataRow.class)
.registerConverter(new NativeZonedDateStringConverter())
.sheet("NativeMixedMetadata")
.doWrite(List.of(createRow())));
}
private NativeMixedMetadataRow createRow() {
NativeMixedMetadataRow row = new NativeMixedMetadataRow();
row.setStatus("ACTIVE");
row.setAmount(new BigDecimal("1234.5"));
row.setCreatedAt(ZonedDateTime.of(2026, 3, 24, 15, 30, 45, 0, ZoneId.of("Asia/Shanghai")));
return row;
}
@Data
public static class NativeMixedMetadataRow {
@ExcelProperty(value = "Status", converter = NativeStatusLabelConverter.class)
private String status;
@ExcelProperty("Amount")
@NumberFormat("¥#,##0.00")
private BigDecimal amount;
@ExcelProperty("Created At")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private ZonedDateTime createdAt;
}
public static class NativeStatusLabelConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public WriteCellData<?> convertToExcelData(
String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return new WriteCellData<>("STATUS:" + value);
}
}
public static class NativeZonedDateStringConverter implements Converter<ZonedDateTime> {
@Override
public Class<?> supportJavaTypeKey() {
return ZonedDateTime.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public WriteCellData<?> convertToExcelData(
ZonedDateTime value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String format = contentProperty != null && contentProperty.getDateTimeFormatProperty() != null
? contentProperty.getDateTimeFormatProperty().getFormat()
: "yyyy-MM-dd HH:mm:ss";
return new WriteCellData<>(value.toLocalDateTime().format(java.time.format.DateTimeFormatter.ofPattern(format)));
}
}
}The test is intentionally marked @Disabled so CI stays green, but it is a stable reproducer.
Repro DTO
@Data
public static class NativeMixedMetadataRow {
@ExcelProperty(value = "Status", converter = NativeStatusLabelConverter.class)
private String status;
@ExcelProperty("Amount")
@NumberFormat("¥#,##0.00")
private BigDecimal amount;
@ExcelProperty("Created At")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private ZonedDateTime createdAt;
}Repro Export Code
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
FesodSheet.write(outputStream, NativeMixedMetadataRow.class)
.registerConverter(new NativeZonedDateStringConverter())
.sheet("NativeMixedMetadata")
.doWrite(List.of(row));Expected Result
Workbook is exported successfully, with native Fesod metadata handling preserved:
@ExcelProperty(converter = ...)is applied@NumberFormatis applied@DateTimeFormatis applied
Actual Result
StackOverflowError
Observed Stack Pattern
The stack repeatedly cycles through hashCode() calls on Fesod write holder objects:
java.lang.StackOverflowError
at java.util.ArrayList.hashCode(...)
at org.apache.fesod.sheet.metadata.Head.hashCode(...)
at org.apache.fesod.sheet.metadata.property.ExcelHeadProperty.hashCode(...)
at org.apache.fesod.sheet.write.property.ExcelWriteHeadProperty.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.AbstractWriteHolder.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder.hashCode(...)
at org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder.hashCode(...)
...
Suspected Root Cause
This is an inference based on source inspection plus stable reproduction.
WriteContextImpl.initSheet() stores WriteSheetHolder into maps owned by WriteWorkbookHolder:
writeWorkbookHolder.getHasBeenInitializedSheetIndexMap().put(writeSheetHolder.getSheetNo(), writeSheetHolder);
writeWorkbookHolder.getHasBeenInitializedSheetNameMap().put(writeSheetHolder.getSheetName(), writeSheetHolder);At the same time:
WriteSheetHolderholdsparentWriteWorkbookHolderWriteWorkbookHolderholds maps ofWriteSheetHolderAbstractWriteHolder,WriteWorkbookHolder, andWriteSheetHolderuse Lombok@EqualsAndHashCode
That appears to create a recursive object graph for structural hashCode() evaluation:
WriteWorkbookHolder
-> hasBeenInitializedSheetIndexMap / hasBeenInitializedSheetNameMap
-> WriteSheetHolder
-> parentWriteWorkbookHolder
-> WriteWorkbookHolder
-> ...
If Fesod evaluates structural hashCode() on these holder objects, recursion does not terminate.
Project Impact
In our project:
- the plain native POJO path would be the preferred implementation
- but this Fesod issue prevents using it safely for
ZonedDateTime - we currently use a compatibility path that still reuses Fesod field metadata instead of fully hardcoding formatting logic
Suggested Fix Direction
Likely fix directions in Fesod:
- exclude recursive holder references from
equals/hashCode - or avoid structural
equals/hashCodefor runtime write holders entirely - or replace the relevant holder equality semantics with identity-based handling
Local Verification
Project-level verification after documenting the repro:
mvn -q -pl pitpat-common/pitpat-excel test
mvn -q -pl demo-service -am -DskipTests compileCurrent Behavior
StackOverflowError
Expected Behavior
work normally
Anything else?
No response
Are you willing to submit a PR?
- I'm willing to submit a PR!