fix: prioritize subclass fields and ignore superclass annotations on field shadowing (#1007)

* fix: prioritize subclass fields and ignore superclass annotations on field shadowing

* refactor: simplify field skip condition

* refactor: renaming methods and modifying methods to return mutable types

---------

Co-authored-by: DeleiGuo <delei@apache.org>
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java
index 980fc85..3116e9b 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java
@@ -29,7 +29,6 @@
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
@@ -225,13 +224,7 @@
         if (clazz == null) {
             return null;
         }
-        List<Field> tempFieldList = new ArrayList<>();
-        Class<?> tempClass = clazz;
-        while (tempClass != null) {
-            Collections.addAll(tempFieldList, tempClass.getDeclaredFields());
-            // Get the parent class and give it to yourself
-            tempClass = tempClass.getSuperclass();
-        }
+        List<Field> tempFieldList = FieldUtils.resolveAllFields(clazz);
 
         ContentStyle parentContentStyle = clazz.getAnnotation(ContentStyle.class);
         ContentFontStyle parentContentFontStyle = clazz.getAnnotation(ContentFontStyle.class);
@@ -305,20 +298,8 @@
     }
 
     private static FieldCache doDeclaredFields(Class<?> clazz, ConfigurationHolder configurationHolder) {
-        List<Field> tempFieldList = new ArrayList<>();
-        Map<String, Field> fieldNameToField = new HashMap<>();
-        Class<?> tempClass = clazz;
-        // Prefer subclass fields, only process the bottom-most (subclass) definition for fields with the same name
-        while (tempClass != null) {
-            for (Field field : tempClass.getDeclaredFields()) {
-                String fieldName = FieldUtils.resolveCglibFieldName(field);
-                if (!fieldNameToField.containsKey(fieldName)) {
-                    fieldNameToField.put(fieldName, field);
-                    tempFieldList.add(field);
-                }
-            }
-            tempClass = tempClass.getSuperclass();
-        }
+        List<Field> tempFieldList = FieldUtils.resolveAllFields(clazz);
+
         ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class);
         Set<String> ignoreSet = new HashSet<>();
         // First collect all field names annotated with ExcelIgnore (including subclass overrides)
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java
index f80accc..5f78016 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/FieldUtils.java
@@ -27,12 +27,19 @@
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
 import org.apache.fesod.common.util.MemberUtils;
 import org.apache.fesod.common.util.StringUtils;
 import org.apache.fesod.shaded.cglib.beans.BeanMap;
 import org.apache.fesod.sheet.metadata.NullObject;
 
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class FieldUtils {
 
     public static Class<?> nullObjectClass = NullObject.class;
@@ -178,4 +185,33 @@
         }
         return match;
     }
+
+    /**
+     * Resolves and retrieves all declared fields from the specified class and its inheritance hierarchy.
+     * If a field with the same name (resolved by CGLIB) is declared in both a subclass and a superclass,
+     * the subclass field definition takes precedence.
+     *
+     * @param cls the target {@link Class}, must not be {@code null}
+     * @return a {@link List} containing all resolved fields, or an empty list if no fields are found
+     */
+    public static List<Field> resolveAllFields(Class<?> cls) {
+        Validate.isTrue(cls != null, "The class must not be null");
+
+        List<Field> result = new ArrayList<>();
+        Set<String> fieldNames = new HashSet<>();
+        Class<?> tempClass = cls;
+        while (tempClass != null && tempClass != Object.class) {
+            for (Field field : tempClass.getDeclaredFields()) {
+                boolean shouldSkip = Modifier.isStatic(field.getModifiers())
+                        || field.isSynthetic()
+                        || !fieldNames.add(resolveCglibFieldName(field));
+                if (shouldSkip) {
+                    continue;
+                }
+                result.add(field);
+            }
+            tempClass = tempClass.getSuperclass();
+        }
+        return result;
+    }
 }
diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java
index 7b2e901..0f344f7 100644
--- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java
+++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/core/ClassUtilsFieldOverrideTest.java
@@ -19,8 +19,10 @@
 
 package org.apache.fesod.sheet.core;
 
+import java.io.File;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.function.Function;
 import lombok.Getter;
@@ -28,12 +30,30 @@
 import org.apache.fesod.sheet.FesodSheet;
 import org.apache.fesod.sheet.annotation.ExcelIgnore;
 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.annotation.write.style.ContentFontStyle;
+import org.apache.fesod.sheet.annotation.write.style.ContentStyle;
+import org.apache.fesod.sheet.converters.Converter;
+import org.apache.fesod.sheet.enums.CellDataTypeEnum;
+import org.apache.fesod.sheet.enums.poi.HorizontalAlignmentEnum;
+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.apache.fesod.sheet.testkit.Tags;
+import org.apache.fesod.sheet.testkit.assertions.ExcelAssertions;
 import org.apache.fesod.sheet.testkit.base.AbstractExcelTest;
+import org.apache.fesod.sheet.testkit.enums.ExcelFormat;
+import org.apache.fesod.sheet.testkit.params.ExcelFormatSource;
+import org.apache.fesod.sheet.testkit.params.FormatScope;
+import org.apache.fesod.sheet.util.DateUtils;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
 
 @Tag(Tags.ROUND_TRIP)
 public class ClassUtilsFieldOverrideTest extends AbstractExcelTest {
@@ -233,6 +253,275 @@
         Assertions.assertTrue(header12.contains("field"), "Child6 should contain field");
     }
 
+    public static class ParentStringConverter implements Converter<String> {
+
+        @Override
+        public CellDataTypeEnum supportExcelTypeKey() {
+            return CellDataTypeEnum.STRING;
+        }
+
+        @Override
+        public WriteCellData<?> convertToExcelData(
+                String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration)
+                throws Exception {
+            return new WriteCellData<>("Parent: " + value);
+        }
+    }
+
+    public static class ChildStringConverter implements Converter<String> {
+
+        @Override
+        public CellDataTypeEnum supportExcelTypeKey() {
+            return CellDataTypeEnum.STRING;
+        }
+
+        @Override
+        public WriteCellData<?> convertToExcelData(
+                String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration)
+                throws Exception {
+            return new WriteCellData<>("Child: " + value);
+        }
+    }
+
+    @Setter
+    @Getter
+    static class ParentWithAnnotation {
+        @DateTimeFormat("yyyy")
+        Date date;
+
+        @NumberFormat("#.##%")
+        Double doubleValue;
+
+        @DateTimeFormat("yyyy")
+        Date childDate;
+
+        @NumberFormat("#.##%")
+        Double childDoubleValue;
+
+        @ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
+        String string1;
+
+        @ContentFontStyle(fontHeightInPoints = 30)
+        String string2;
+
+        @ExcelProperty(converter = ParentStringConverter.class)
+        String string3;
+
+        @ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER)
+        String childString1;
+
+        @ContentFontStyle(fontHeightInPoints = 30)
+        String childString2;
+
+        @ExcelProperty(converter = ParentStringConverter.class)
+        String childString3;
+    }
+
+    @Setter
+    @Getter
+    static class Child extends ParentWithAnnotation {
+        Date childDate;
+        Double childDoubleValue;
+        String childString1;
+        String childString2;
+        String childString3;
+
+        static List<Child> data() {
+            List<Child> data = new ArrayList<>();
+            Child child = new Child();
+            child.setDate(new Date());
+            child.setDoubleValue(0.5D);
+            child.setChildDate(new Date());
+            child.setChildDoubleValue(0.5D);
+            child.setString1("string1");
+            child.setString2("string2");
+            child.setString3("string3");
+            child.setChildString1("childString1");
+            child.setChildString2("childString2");
+            child.setChildString3("childString3");
+            data.add(child);
+            return data;
+        }
+    }
+
+    @Setter
+    @Getter
+    static class ChildWithAnnotation extends ParentWithAnnotation {
+        @DateTimeFormat("yyyy-MM-dd")
+        Date childDate;
+
+        @NumberFormat("#.00")
+        Double childDoubleValue;
+
+        @ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT)
+        String childString1;
+
+        @ContentFontStyle(fontHeightInPoints = 25)
+        String childString2;
+
+        @ExcelProperty(converter = ChildStringConverter.class)
+        String childString3;
+
+        static List<ChildWithAnnotation> data() {
+            List<ChildWithAnnotation> data = new ArrayList<>();
+            ChildWithAnnotation child = new ChildWithAnnotation();
+            child.setDate(new Date());
+            child.setDoubleValue(0.5D);
+            child.setChildDate(new Date());
+            child.setChildDoubleValue(0.5D);
+            child.setString1("string1");
+            child.setString2("string2");
+            child.setString3("string3");
+            child.setChildString1("childString1");
+            child.setChildString2("childString2");
+            child.setChildString3("childString3");
+            data.add(child);
+            return data;
+        }
+    }
+
+    @ParameterizedTest
+    @ExcelFormatSource(value = FormatScope.BINARY)
+    void test_fieldShadowing_subclassWithoutAnnotation(ExcelFormat format) throws Exception {
+        File file = createTempFile(format);
+        DataFormatter formatter = new DataFormatter();
+
+        // Subclass overrides property without declaring annotations.
+        // Subclass attributes completely shadow parent class attributes; parent class annotations should not take
+        // effect/be inherited.
+        FesodSheet.write(file).head(Child.class).sheet().doWrite(Child.data());
+
+        try (ExcelAssertions ea = ExcelAssertions.assertThat(file)) {
+            ea.sheet(0)
+                    .row(1)
+                    // col 0 = childDate: subclass shadows parent, no @DateTimeFormat inherited
+                    .cell(0)
+                    .hasDataFormatString(DateUtils.defaultDateFormat)
+                    .and()
+                    // col 1 = childDoubleValue: subclass shadows parent, no @NumberFormat inherited
+                    .cell(1)
+                    .satisfies(cell -> {
+                        String formatCellValue = formatter.formatCellValue(cell);
+                        Assertions.assertEquals("0.5", formatCellValue);
+                    })
+                    .and()
+                    // col 2 = childString1: subclass shadows parent, no @ContentStyle inherited
+                    .cell(2)
+                    .satisfies(cell -> {
+                        Assertions.assertEquals("childString1", cell.getStringCellValue());
+                        Assertions.assertNotEquals(
+                                HorizontalAlignment.CENTER, cell.getCellStyle().getAlignment());
+                    })
+                    .and()
+                    // col 3 = childString2: subclass shadows parent, no @ContentFontStyle inherited
+                    .cell(3)
+                    .satisfies(cell -> Assertions.assertEquals("childString2", cell.getStringCellValue()))
+                    .and()
+                    // col 4 = childString3: subclass shadows parent, no converter inherited
+                    .cell(4)
+                    .satisfies(cell -> Assertions.assertEquals("childString3", cell.getStringCellValue()))
+                    .and()
+                    // col 5 = date: parent field retains @DateTimeFormat("yyyy")
+                    .cell(5)
+                    .hasDataFormatString("yyyy")
+                    .and()
+                    // col 6 = doubleValue: parent field retains @NumberFormat("#.##%")
+                    .cell(6)
+                    .satisfies(cell -> {
+                        String formatCellValue = formatter.formatCellValue(cell);
+                        Assertions.assertEquals("50%", formatCellValue);
+                    })
+                    .and()
+                    // col 7 = string1: parent field retains @ContentStyle(CENTER)
+                    .cell(7)
+                    .satisfies(cell -> {
+                        Assertions.assertEquals("string1", cell.getStringCellValue());
+                        Assertions.assertEquals(
+                                HorizontalAlignment.CENTER, cell.getCellStyle().getAlignment());
+                    })
+                    .and()
+                    // col 8 = string2: parent field retains @ContentFontStyle(fontHeightInPoints = 30)
+                    .cell(8)
+                    .satisfies(cell -> Assertions.assertEquals("string2", cell.getStringCellValue()))
+                    .hasFontSize((short) 30)
+                    .and()
+                    // col 9 = string3: parent field retains @ExcelProperty converter
+                    .cell(9)
+                    .satisfies(cell -> Assertions.assertEquals("Parent: string3", cell.getStringCellValue()));
+        }
+    }
+
+    @ParameterizedTest
+    @ExcelFormatSource(value = FormatScope.BINARY)
+    void test_fieldShadowing_subclassAnnotationPrecedence(ExcelFormat format) throws Exception {
+        File file = createTempFile(format);
+        DataFormatter formatter = new DataFormatter();
+
+        // Subclass overrides a property and explicitly declares a subclass-specific annotations.
+        // Directly use the annotation format on subclass attributes
+        FesodSheet.write(file).head(ChildWithAnnotation.class).sheet().doWrite(ChildWithAnnotation.data());
+
+        try (ExcelAssertions ea = ExcelAssertions.assertThat(file)) {
+            ea.sheet(0)
+                    .row(1)
+                    // col 0 = childDate: subclass @DateTimeFormat("yyyy-MM-dd") takes precedence
+                    .cell(0)
+                    .hasDataFormatString("yyyy-MM-dd")
+                    .and()
+                    // col 1 = childDoubleValue: subclass @NumberFormat("#.00") takes precedence
+                    .cell(1)
+                    .satisfies(cell -> {
+                        String formatCellValue = formatter.formatCellValue(cell);
+                        Assertions.assertEquals(".50", formatCellValue);
+                    })
+                    .and()
+                    // col 2 = childString1: subclass @ContentStyle(LEFT) takes precedence
+                    .cell(2)
+                    .satisfies(cell -> {
+                        Assertions.assertEquals("childString1", cell.getStringCellValue());
+                        Assertions.assertEquals(
+                                HorizontalAlignment.LEFT, cell.getCellStyle().getAlignment());
+                    })
+                    .and()
+                    // col 3 = childString2: subclass @ContentFontStyle(25) takes precedence
+                    .cell(3)
+                    .satisfies(cell -> Assertions.assertEquals("childString2", cell.getStringCellValue()))
+                    .hasFontSize((short) 25)
+                    .and()
+                    // col 4 = childString3: subclass @ExcelProperty converter takes precedence
+                    .cell(4)
+                    .satisfies(cell -> Assertions.assertEquals("Child: childString3", cell.getStringCellValue()))
+                    .and()
+                    // col 5 = date: parent @DateTimeFormat("yyyy") retained
+                    .cell(5)
+                    .hasDataFormatString("yyyy")
+                    .and()
+                    // col 6 = doubleValue: parent @NumberFormat("#.##%") retained
+                    .cell(6)
+                    .satisfies(cell -> {
+                        String formatCellValue = formatter.formatCellValue(cell);
+                        Assertions.assertEquals("50%", formatCellValue);
+                    })
+                    .and()
+                    // col 7 = string1: parent field retains @ContentStyle(CENTER)
+                    .cell(7)
+                    .satisfies(cell -> {
+                        Assertions.assertEquals("string1", cell.getStringCellValue());
+                        Assertions.assertEquals(
+                                HorizontalAlignment.CENTER, cell.getCellStyle().getAlignment());
+                    })
+                    .and()
+                    // col 8 = string2: parent field retains @ContentFontStyle(fontHeightInPoints = 30)
+                    .cell(8)
+                    .satisfies(cell -> Assertions.assertEquals("string2", cell.getStringCellValue()))
+                    .hasFontSize((short) 30)
+                    .and()
+                    // col 9 = string3: parent field retains @ExcelProperty converter
+                    .cell(9)
+                    .satisfies(cell -> Assertions.assertEquals("Parent: string3", cell.getStringCellValue()));
+        }
+    }
+
     private static Function<Path, List<String>> extractExcelHeader() {
         // Helper to read first row (header) from generated file
         Function<Path, List<String>> readHeader = path -> {
diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java
index e72a273..ca7a00c 100644
--- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java
+++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/FieldUtilsTest.java
@@ -21,7 +21,9 @@
 
 import java.lang.reflect.Field;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.fesod.shaded.cglib.beans.BeanMap;
 import org.apache.fesod.sheet.metadata.NullObject;
@@ -242,4 +244,58 @@
         Assertions.assertNotNull(field);
         Assertions.assertTrue(field.isAccessible());
     }
+
+    static class ParentClass {
+        public static final String STATIC_PARENT_FIELD = "STATIC";
+        private String commonField = "parentCommon";
+        private Integer parentOnlyField;
+    }
+
+    static class ChildClass extends ParentClass {
+        private String commonField = "childCommon";
+        private Double childOnlyField;
+    }
+
+    static class EmptyClass {}
+
+    @Test
+    void test_resolveFields_nullClass_throwsException() {
+        IllegalArgumentException exception =
+                Assertions.assertThrows(IllegalArgumentException.class, () -> FieldUtils.resolveAllFields(null));
+        Assertions.assertEquals("The class must not be null", exception.getMessage());
+    }
+
+    @Test
+    void test_resolveFields_emptyClass_returnsEmptyList() {
+        List<Field> fields = FieldUtils.resolveAllFields(EmptyClass.class);
+        Assertions.assertNotNull(fields);
+        Assertions.assertTrue(fields.isEmpty());
+    }
+
+    @Test
+    void test_resolveFields_InheritanceAndShadowing_SubclassTakesPrecedence() {
+        List<Field> fields = FieldUtils.resolveAllFields(ChildClass.class);
+
+        Assertions.assertEquals(3, fields.size());
+
+        List<String> fieldNames = fields.stream().map(Field::getName).collect(Collectors.toList());
+        Assertions.assertTrue(fieldNames.contains("childOnlyField"));
+        Assertions.assertTrue(fieldNames.contains("parentOnlyField"));
+        Assertions.assertTrue(fieldNames.contains("commonField"));
+
+        Field commonField = fields.stream()
+                .filter(f -> f.getName().equals("commonField"))
+                .findFirst()
+                .get();
+        Assertions.assertEquals(ChildClass.class, commonField.getDeclaringClass());
+    }
+
+    @Test
+    void test_resolveFields_IgnoresStaticFields() {
+        List<Field> fields = FieldUtils.resolveAllFields(ChildClass.class);
+
+        boolean hasStaticField = fields.stream().anyMatch(f -> f.getName().equals("STATIC_PARENT_FIELD"));
+
+        Assertions.assertFalse(hasStaticField);
+    }
 }