Repository: wangguanquan/eec
Pull request #241 (2022-05-03 11:19:50)

Fix#237
#237


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index eb947146..11036c2a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,6 +3,7 @@ Version  ()
 - 增加StyleDesign用于样式处理（单元格或者整行样式处理）
 - 增加FreezePanes用于冻结网格
 - 修改部分BUG(#227,#238)
+- 读取文件支持自定义注解转对象
 
 Version 0.4.14 (2021-12-19)
 -------------
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index 78337a74..6cfd0d32 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -53,7 +53,6 @@ import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
@@ -443,9 +442,9 @@ public class ExcelReader implements Closeable {
         return this;
     }
 
-    // --- PRIVATE FUNCTIONS
+    // --- PROTECTED FUNCTIONS
 
-    private ContentType checkContentType(Path root) {
+    protected ContentType checkContentType(Path root) {
         SAXReader reader = new SAXReader();
         Document document;
         // Read [Content_Types].xml
@@ -470,7 +469,37 @@ public class ExcelReader implements Closeable {
         return contentType;
     }
 
-    private ExcelReader(Path path, int bufferSize, int cacheSize, int option) throws IOException {
+    public ExcelReader(InputStream is) throws IOException {
+        this(is, 0, 0, VALUE_ONLY);
+    }
+
+    public ExcelReader(InputStream is, int option) throws IOException {
+        this(is, 0, 0, option);
+    }
+
+    public ExcelReader(InputStream stream, int bufferSize, int cacheSize, int option) throws IOException {
+        Path temp = FileUtil.mktmp(Const.EEC_PREFIX);
+        if (temp == null) {
+            throw new IOException("Create temp directory error. Please check your permission");
+        }
+        FileUtil.cp(stream, temp);
+
+        init(temp, bufferSize, cacheSize, option);
+    }
+
+    public ExcelReader(Path path) throws IOException {
+        init(path, 0, 0, VALUE_ONLY);
+    }
+
+    public ExcelReader(Path path, int option) throws IOException {
+        init(path, 0, 0, option);
+    }
+
+    public ExcelReader(Path path, int bufferSize, int cacheSize, int option) throws IOException {
+        init(path, bufferSize, cacheSize, option);
+    }
+
+    protected ExcelReader init(Path path, int bufferSize, int cacheSize, int option) throws IOException {
         // Store template stream as zip file
         Path tmp = FileUtil.mktmp(Const.EEC_PREFIX);
         LOGGER.debug("Unzip file to：{}", tmp);
@@ -582,6 +611,7 @@ public class ExcelReader implements Closeable {
         if ((option >> 1 & 1) == 1) {
             parseFormula();
         }
+        return this;
     }
 
     /**
@@ -641,7 +671,7 @@ public class ExcelReader implements Closeable {
         return er;
     }
 
-    private XMLSheet sheetFactory(int option) {
+    protected XMLSheet sheetFactory(int option) {
         XMLSheet sheet;
         switch (option) {
             case VALUE_AND_CALC: sheet = new XMLCalcSheet(); break;
@@ -659,7 +689,7 @@ public class ExcelReader implements Closeable {
      * @param path documents path
      * @return enum of ExcelType
      */
-    private static ExcelType getType(Path path) {
+    public static ExcelType getType(Path path) {
         ExcelType type;
         try (InputStream is = Files.newInputStream(path)) {
             byte[] bytes = new byte[8];
diff --git a/src/main/java/org/ttzero/excel/reader/HeaderRow.java b/src/main/java/org/ttzero/excel/reader/HeaderRow.java
index 2a2bc173..52beb2ff 100644
--- a/src/main/java/org/ttzero/excel/reader/HeaderRow.java
+++ b/src/main/java/org/ttzero/excel/reader/HeaderRow.java
@@ -19,6 +19,7 @@ package org.ttzero.excel.reader;
 import org.ttzero.excel.annotation.ExcelColumn;
 import org.ttzero.excel.annotation.IgnoreImport;
 import org.ttzero.excel.annotation.RowNum;
+import org.ttzero.excel.entity.ListSheet;
 import org.ttzero.excel.manager.Const;
 import org.ttzero.excel.util.ReflectUtil;
 import org.ttzero.excel.util.StringUtil;
@@ -26,6 +27,7 @@ import org.ttzero.excel.util.StringUtil;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
+import java.lang.reflect.AccessibleObject;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
@@ -34,6 +36,7 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -49,46 +52,43 @@ import static org.ttzero.excel.entity.IWorksheetWriter.isLocalTime;
 import static org.ttzero.excel.util.ReflectUtil.listDeclaredFields;
 import static org.ttzero.excel.util.ReflectUtil.listDeclaredMethods;
 import static org.ttzero.excel.util.ReflectUtil.mapping;
-import static org.ttzero.excel.util.StringUtil.isNotEmpty;
+import static org.ttzero.excel.util.StringUtil.EMPTY;
 
 /**
  * @author guanquan.wang at 2019-04-17 11:55
  */
-class HeaderRow extends Row {
-    private String[] names;
-    private Class<?> clazz;
-    private Field[] fields;
-    private Method[] methods;
-    private int[] columns;
-    private Class<?>[] fieldClazz;
-    private Object t;
+public class HeaderRow extends Row {
+    protected String[] names;
+    protected Class<?> clazz;
+    protected Object t;
     /* The column name and column position mapping */
-    private Map<String, Integer> mapping;
+    protected Map<String, Integer> mapping;
+    /* Storage header column */
+    protected ListSheet.EntryColumn[] columns;
 
-    private HeaderRow() { }
+    protected HeaderRow() { }
 
-    static HeaderRow with(Row row) {
-        HeaderRow hr = new HeaderRow();
-        hr.names = new String[row.lc];
-        hr.mapping = new HashMap<>();
+    public HeaderRow with(Row row) {
+        this.names = new String[row.lc];
+        this.mapping = new HashMap<>();
         for (int i = row.fc; i < row.lc; i++) {
-            hr.names[i] = row.getString(i);
-            hr.mapping.put(hr.names[i], i);
+            this.names[i] = row.getString(i);
+            this.mapping.put(this.names[i], i);
         }
         // Extends from row
-        hr.fc = row.fc;
-        hr.lc = row.lc;
-        hr.index = row.index;
-        hr.cells = new Cell[hr.names.length];
+        this.fc = row.fc;
+        this.lc = row.lc;
+        this.index = row.index;
+        this.cells = new Cell[this.names.length];
         for (int i = 0; i < row.fc; i++) {
-            hr.cells[i] = new Cell();
+            this.cells[i] = new Cell();
         }
         for (int i = row.fc; i < row.lc; i++) {
             Cell cell = new Cell();
-            cell.setSv(hr.names[i]);
-            hr.cells[i] = cell;
+            cell.setSv(this.names[i]);
+            this.cells[i] = cell;
         }
-        return hr;
+        return this;
     }
 
     final boolean is(Class<?> clazz) {
@@ -101,137 +101,85 @@ class HeaderRow extends Row {
      * @param clazz the type of binding
      * @return the header row
      */
-    final HeaderRow setClass(Class<?> clazz) {
+    protected HeaderRow setClass(Class<?> clazz) {
         this.clazz = clazz;
+        // Parse Field
         Field[] declaredFields = listDeclaredFields(clazz);
+        // Parse Method
+        Map<String, Method> tmp = attachOtherColumn(clazz);
 
-        Method[] writeMethods = null;
-        try {
-            writeMethods = listDeclaredMethods(clazz, method -> method.getAnnotation(ExcelColumn.class) != null
-                    || method.getAnnotation(RowNum.class) != null);
-        } catch (IntrospectionException e) {
-            LOGGER.warn("Get [" + clazz + "] read declared failed.", e);
-        }
-
-        Map<String, Method> tmp = new LinkedHashMap<>();
-
-        int writeLength = methodMapping(clazz, writeMethods, tmp);
-        methods = new Method[declaredFields.length + writeLength];
-        Class<?>[] fieldClazz = new Class[methods.length];
+        ListSheet.EntryColumn[] columns = new ListSheet.EntryColumn[declaredFields.length + tmp.size()];
 
-        int[] index = new int[declaredFields.length];
         int count = 0;
-        for (int i = 0, n; i < declaredFields.length; i++) {
+        for (int i = 0; i < declaredFields.length; i++) {
             Field f = declaredFields[i];
             f.setAccessible(true);
             String gs = f.getName();
 
-            // Ignore annotation on read method
+            // The setter methods take precedence over property reflection
             Method method = tmp.get(gs);
             if (method != null) {
-                if (method.getAnnotation(IgnoreImport.class) != null) {
-                    declaredFields[i] = null;
-                    continue;
-                }
-
-                method.setAccessible(true);
-                methods[i] = method;
-                ExcelColumn mec = method.getAnnotation(ExcelColumn.class);
-                if (mec != null && isNotEmpty(mec.value())) {
-                    n = check(mec.value(), gs);
-                    if (n == -1) {
-                        declaredFields[i] = null;
-                    } else {
-                        index[i] = n;
-                        count++;
-                    }
-                    continue;
-                }
-
-                // Row Num
-                RowNum rowNum = method.getAnnotation(RowNum.class);
-                if (rowNum != null) {
-                    fieldClazz[i] = RowNum.class;
-                    index[i] = i;
-                    count++;
+                ListSheet.EntryColumn column = createColumn(method);
+                if (column != null) {
+                    column.method = method;
+                    column.colIndex = check(column.name, gs);
+                    if (column.clazz == null) column.clazz = method.getParameterTypes()[0];
+                    columns[count++] = column;
+
+                    // Remove from mapper if attached
+                    tmp.remove(gs);
                     continue;
                 }
             }
 
-            // skip not import fields
-            IgnoreImport nit = f.getAnnotation(IgnoreImport.class);
-            if (nit != null) {
-                declaredFields[i] = null;
-                continue;
-            }
-
-            // Row Num
-            RowNum rowNum = f.getAnnotation(RowNum.class);
-            if (rowNum != null) {
-                index[i] = i;
-                count++;
-                fieldClazz[i] = RowNum.class;
-                continue;
-            }
-
-            // field has display name
-            ExcelColumn ec = f.getAnnotation(ExcelColumn.class);
-            if (ec != null && isNotEmpty(ec.value())) {
-                n = check(ec.value(), gs);
-                if (n == -1) {
-                    declaredFields[i] = null;
-                    continue;
+            ListSheet.EntryColumn column = createColumn(f);
+            if (column != null) {
+                if (StringUtil.isEmpty(column.name)) {
+                    column.name = gs;
                 }
-            }
-            // Annotation value is null
-            else if (ec != null || methods[i] != null) {
-                String name = f.getName();
-                n = getIndex(name);
-                if (n == -1 && (n = getIndex(StringUtil.toPascalCase(name))) == -1) {
-                    declaredFields[i] = null;
-                    continue;
+                if (method != null) {
+                    column.method = method;
+                    if (column.clazz == null) column.clazz = method.getParameterTypes()[0];
+                } else {
+                    column.field = f;
+                    if (column.clazz == null) column.clazz = declaredFields[i].getType();
                 }
-            } else {
-                declaredFields[i] = null;
-                continue;
+                column.colIndex = check(column.name, gs);
+                columns[count++] = column;
             }
-
-            index[i] = n;
-            count++;
         }
 
-        if (writeLength > 0) {
-            System.arraycopy(writeMethods, 0, methods, declaredFields.length, writeLength);
-            count += writeLength;
-//            for (int i = declaredFields.length, j = 0; j < writeLength; j++) {
-//                index[i++] =
-//            }
+        if (!tmp.isEmpty()) {
+            for (Map.Entry<String, Method> entry : tmp.entrySet()) {
+                ListSheet.EntryColumn column = createColumn(entry.getValue());
+                if (column == null) {
+                    column = new ListSheet.EntryColumn(entry.getKey());
+                }
+                column.method = entry.getValue();
+                column.colIndex = getIndex(column.name);
+                if (column.clazz == null) column.clazz = entry.getValue().getParameterTypes()[0];
+                columns[count++] = column;
+            }
         }
 
-        this.fields = new Field[count];
-        this.columns = new int[count];
-        this.fieldClazz = new Class<?>[count];
-
-        for (int i = 0, j = 0; i < declaredFields.length; i++) {
-            if (declaredFields[i] != null) {
-                fields[j] = declaredFields[i];
-                columns[j] = index[i];
-                methods[j] = methods[i];
-                if (fieldClazz[i] != null) {
-                    this.fieldClazz[j] = fieldClazz[i];
-                } else {
-                    this.fieldClazz[j] = methods[i] != null ? methods[i].getParameterTypes()[0] : declaredFields[i].getType();
+        // Remove not mapped column
+        int j = 0;
+        for (int i = 0; i < count; i++) {
+            ListSheet.EntryColumn column = columns[i];
+            if (column.colIndex >= 0 || column.clazz == RowNum.class) {
+                if (i != j) {
+                    columns[j] = column;
                 }
                 j++;
             }
         }
 
-
+        this.columns = j < columns.length ? Arrays.copyOf(columns, j) : columns;
 
         return this;
     }
 
-    private int methodMapping(Class<?> clazz, Method[] writeMethods, Map<String, Method> tmp) {
+    protected int methodMapping(Class<?> clazz, Method[] writeMethods, Map<String, Method> tmp) {
         try {
             PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(clazz)
                 .getPropertyDescriptors();
@@ -241,6 +189,7 @@ class HeaderRow extends Row {
                 Method method = propertyDescriptors[i].getWriteMethod();
                 if (method == null) continue;
                 int index = ReflectUtil.indexOf(allMethods, method);
+                // Subclass methods have higher priority
                 mergedMethods[i] = index >= 0 ? allMethods[index] : method;
             }
 
@@ -251,7 +200,7 @@ class HeaderRow extends Row {
         return 0;
     }
 
-    private int check(String first, String second) {
+    protected int check(String first, String second) {
         int n = getIndex(first);
         if (n == -1) n = getIndex(second);
         if (n == -1) {
@@ -268,26 +217,18 @@ class HeaderRow extends Row {
      * @throws IllegalAccessException -
      * @throws InstantiationException -
      */
-    final HeaderRow setClassOnce(Class<?> clazz) throws IllegalAccessException, InstantiationException {
+    protected HeaderRow setClassOnce(Class<?> clazz) throws IllegalAccessException, InstantiationException {
         setClass(clazz);
         this.t = clazz.newInstance();
         return this;
     }
 
-    final Field[] getFields() {
-        return fields;
-    }
-
-    final int[] getColumns() {
+    protected ListSheet.EntryColumn[] getColumns() {
         return columns;
     }
 
-    final Class<?>[] getFieldClazz() {
-        return fieldClazz;
-    }
-
     @SuppressWarnings("unchecked")
-    final <T> T getT() {
+    protected <T> T getT() {
         return (T) t;
     }
 
@@ -340,8 +281,8 @@ class HeaderRow extends Row {
                 Arrays.fill(chars, 0, n, '-');
             }
 
-            if (fieldClazz != null) {
-                Class<?> c = fieldClazz[i];
+            if (columns != null && i < columns.length && columns[i].clazz != RowNum.class) {
+                Class<?> c = columns[i].clazz;
 
                 // Align Center
                 if (isDate(c) || isLocalDate(c) || isLocalDateTime(c) || isLocalTime(c) || isChar(c) || isBool(c)) {
@@ -363,7 +304,7 @@ class HeaderRow extends Row {
         return buf.toString();
     }
 
-    int simpleTestLength(String name) {
+    protected int simpleTestLength(String name) {
         if (name == null) return 4;
         char[] chars = name.toCharArray();
         double d = 0.0;
@@ -376,184 +317,246 @@ class HeaderRow extends Row {
 
     void put(Row row, Object t) throws IllegalAccessException, InvocationTargetException {
         for (int i = 0; i < columns.length; i++) {
-            if (methods[i] != null)
+            if (columns[i].method != null)
                 methodPut(i, row, t);
             else
                 fieldPut(i, row, t);
         }
     }
 
-    private void fieldPut(int i, Row row, Object t) throws IllegalAccessException {
-        int c = columns[i];
-        if (fieldClazz[i] == String.class) {
-            fields[i].set(t, row.getString(c));
+    protected void fieldPut(int i, Row row, Object t) throws IllegalAccessException {
+        ListSheet.EntryColumn ec = columns[i];
+        int c = ec.colIndex;
+        Class<?> fieldClazz = ec.clazz;
+        if (fieldClazz == String.class) {
+            ec.field.set(t, row.getString(c));
         }
-        else if (fieldClazz[i] == Integer.class) {
-            fields[i].set(t, row.getInt(c));
+        else if (fieldClazz == Integer.class) {
+            ec.field.set(t, row.getInt(c));
         }
-        else if (fieldClazz[i] == Long.class) {
-            fields[i].set(t, row.getLong(c));
+        else if (fieldClazz == Long.class) {
+            ec.field.set(t, row.getLong(c));
         }
-        else if (fieldClazz[i] == java.util.Date.class || fieldClazz[i] == java.sql.Date.class) {
-            fields[i].set(t, row.getDate(c));
+        else if (fieldClazz == java.util.Date.class || fieldClazz == java.sql.Date.class) {
+            ec.field.set(t, row.getDate(c));
         }
-        else if (fieldClazz[i] == java.sql.Timestamp.class) {
-            fields[i].set(t, row.getTimestamp(c));
+        else if (fieldClazz == java.sql.Timestamp.class) {
+            ec.field.set(t, row.getTimestamp(c));
         }
-        else if (fieldClazz[i] == Double.class) {
-            fields[i].set(t, row.getDouble(c));
+        else if (fieldClazz == Double.class) {
+            ec.field.set(t, row.getDouble(c));
         }
-        else if (fieldClazz[i] == Float.class) {
-            fields[i].set(t, row.getFloat(c));
+        else if (fieldClazz == Float.class) {
+            ec.field.set(t, row.getFloat(c));
         }
-        else if (fieldClazz[i] == Boolean.class) {
-            fields[i].set(t, row.getBoolean(c));
+        else if (fieldClazz == Boolean.class) {
+            ec.field.set(t, row.getBoolean(c));
         }
-        else if (fieldClazz[i] == BigDecimal.class) {
-            fields[i].set(t, row.getDecimal(c));
+        else if (fieldClazz == BigDecimal.class) {
+            ec.field.set(t, row.getDecimal(c));
         }
-        else if (fieldClazz[i] == int.class) {
+        else if (fieldClazz == int.class) {
             Integer v;
-            fields[i].set(t, (v = row.getInt(c)) != null ? v : 0);
+            ec.field.set(t, (v = row.getInt(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == long.class) {
+        else if (fieldClazz == long.class) {
             Long v;
-            fields[i].set(t, (v = row.getLong(c)) != null ? v : 0L);
+            ec.field.set(t, (v = row.getLong(c)) != null ? v : 0L);
         }
-        else if (fieldClazz[i] == double.class) {
+        else if (fieldClazz == double.class) {
             Double v;
-            fields[i].set(t, (v = row.getDouble(c)) != null ? v : 0.0D);
+            ec.field.set(t, (v = row.getDouble(c)) != null ? v : 0.0D);
         }
-        else if (fieldClazz[i] == float.class) {
+        else if (fieldClazz == float.class) {
             Float v;
-            fields[i].set(t, (v = row.getFloat(c)) != null ? v : 0.0F);
+            ec.field.set(t, (v = row.getFloat(c)) != null ? v : 0.0F);
         }
-        else if (fieldClazz[i] == boolean.class) {
+        else if (fieldClazz == boolean.class) {
             Boolean v;
-            fields[i].set(t, (v = row.getBoolean(c)) != null ? v : false);
+            ec.field.set(t, (v = row.getBoolean(c)) != null ? v : false);
         }
-        else if (fieldClazz[i] == java.sql.Time.class) {
-            fields[i].set(t, row.getTime(c));
+        else if (fieldClazz == java.sql.Time.class) {
+            ec.field.set(t, row.getTime(c));
         }
-        else if (fieldClazz[i] == LocalDateTime.class) {
-            fields[i].set(t, row.getLocalDateTime(c));
+        else if (fieldClazz == LocalDateTime.class) {
+            ec.field.set(t, row.getLocalDateTime(c));
         }
-        else if (fieldClazz[i] == LocalDate.class) {
-            fields[i].set(t, row.getLocalDate(c));
+        else if (fieldClazz == LocalDate.class) {
+            ec.field.set(t, row.getLocalDate(c));
         }
-        else if (fieldClazz[i] == LocalTime.class) {
-            fields[i].set(t, row.getLocalTime(c));
+        else if (fieldClazz == LocalTime.class) {
+            ec.field.set(t, row.getLocalTime(c));
         }
-        else if (fieldClazz[i] == Character.class) {
-            fields[i].set(t, row.getChar(c));
+        else if (fieldClazz == Character.class) {
+            ec.field.set(t, row.getChar(c));
         }
-        else if (fieldClazz[i] == Byte.class) {
-            fields[i].set(t, row.getByte(c));
+        else if (fieldClazz == Byte.class) {
+            ec.field.set(t, row.getByte(c));
         }
-        else if (fieldClazz[i] == Short.class) {
-            fields[i].set(t, row.getShort(c));
+        else if (fieldClazz == Short.class) {
+            ec.field.set(t, row.getShort(c));
         }
-        else if (fieldClazz[i] == char.class) {
+        else if (fieldClazz == char.class) {
             Character v;
-            fields[i].set(t, (v = row.getChar(c)) != null ? v : '\0');
+            ec.field.set(t, (v = row.getChar(c)) != null ? v : '\0');
         }
-        else if (fieldClazz[i] == byte.class) {
+        else if (fieldClazz == byte.class) {
             Byte v;
-            fields[i].set(t, (v = row.getByte(c)) != null ? v : 0);
+            ec.field.set(t, (v = row.getByte(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == short.class) {
+        else if (fieldClazz == short.class) {
             Short v;
-            fields[i].set(t, (v = row.getShort(c)) != null ? v : 0);
+            ec.field.set(t, (v = row.getShort(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == RowNum.class) {
-            fields[i].set(t, row.getRowNum());
+        else if (fieldClazz == RowNum.class) {
+            ec.field.set(t, row.getRowNum());
         }
     }
 
-    private void methodPut(int i, Row row, Object t) throws IllegalAccessException, InvocationTargetException {
-        int c = columns[i];
-        if (fieldClazz[i] == String.class) {
-            methods[i].invoke(t, row.getString(c));
+    protected void methodPut(int i, Row row, Object t) throws IllegalAccessException, InvocationTargetException {
+        ListSheet.EntryColumn ec = columns[i];
+        int c = ec.colIndex;
+        Class<?> fieldClazz = ec.clazz;
+        if (fieldClazz == String.class) {
+            ec.method.invoke(t, row.getString(c));
         }
-        else if (fieldClazz[i] == Integer.class) {
-            methods[i].invoke(t, row.getInt(c));
+        else if (fieldClazz == Integer.class) {
+            ec.method.invoke(t, row.getInt(c));
         }
-        else if (fieldClazz[i] == Long.class) {
-            methods[i].invoke(t, row.getLong(c));
+        else if (fieldClazz == Long.class) {
+            ec.method.invoke(t, row.getLong(c));
         }
-        else if (fieldClazz[i] == java.util.Date.class || fieldClazz[i] == java.sql.Date.class) {
-            methods[i].invoke(t, row.getDate(c));
+        else if (fieldClazz == java.util.Date.class || fieldClazz == java.sql.Date.class) {
+            ec.method.invoke(t, row.getDate(c));
         }
-        else if (fieldClazz[i] == java.sql.Timestamp.class) {
-            methods[i].invoke(t, row.getTimestamp(c));
+        else if (fieldClazz == java.sql.Timestamp.class) {
+            ec.method.invoke(t, row.getTimestamp(c));
         }
-        else if (fieldClazz[i] == Double.class) {
-            methods[i].invoke(t, row.getDouble(c));
+        else if (fieldClazz == Double.class) {
+            ec.method.invoke(t, row.getDouble(c));
         }
-        else if (fieldClazz[i] == Float.class) {
-            methods[i].invoke(t, row.getFloat(c));
+        else if (fieldClazz == Float.class) {
+            ec.method.invoke(t, row.getFloat(c));
         }
-        else if (fieldClazz[i] == Boolean.class) {
-            methods[i].invoke(t, row.getBoolean(c));
+        else if (fieldClazz == Boolean.class) {
+            ec.method.invoke(t, row.getBoolean(c));
         }
-        else if (fieldClazz[i] == BigDecimal.class) {
-            methods[i].invoke(t, row.getDecimal(c));
+        else if (fieldClazz == BigDecimal.class) {
+            ec.method.invoke(t, row.getDecimal(c));
         }
-        else if (fieldClazz[i] == int.class) {
+        else if (fieldClazz == int.class) {
             Integer v;
-            methods[i].invoke(t, (v = row.getInt(c)) != null ? v : 0);
+            ec.method.invoke(t, (v = row.getInt(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == long.class) {
+        else if (fieldClazz == long.class) {
             Long v;
-            methods[i].invoke(t, (v = row.getLong(c)) != null ? v : 0);
+            ec.method.invoke(t, (v = row.getLong(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == double.class) {
+        else if (fieldClazz == double.class) {
             Double v;
-            methods[i].invoke(t, (v = row.getDouble(c)) != null ? v : 0.0D);
+            ec.method.invoke(t, (v = row.getDouble(c)) != null ? v : 0.0D);
         }
-        else if (fieldClazz[i] == float.class) {
+        else if (fieldClazz == float.class) {
             Float v;
-            methods[i].invoke(t, (v = row.getFloat(c)) != null ? v : 0.0F);
+            ec.method.invoke(t, (v = row.getFloat(c)) != null ? v : 0.0F);
         }
-        else if (fieldClazz[i] == boolean.class) {
+        else if (fieldClazz == boolean.class) {
             Boolean v;
-            methods[i].invoke(t, (v = row.getBoolean(c)) != null ? v : false);
+            ec.method.invoke(t, (v = row.getBoolean(c)) != null ? v : false);
         }
-        else if (fieldClazz[i] == java.sql.Time.class) {
-            methods[i].invoke(t, row.getTime(c));
+        else if (fieldClazz == java.sql.Time.class) {
+            ec.method.invoke(t, row.getTime(c));
         }
-        else if (fieldClazz[i] == LocalDateTime.class) {
-            methods[i].invoke(t, row.getLocalDateTime(c));
+        else if (fieldClazz == LocalDateTime.class) {
+            ec.method.invoke(t, row.getLocalDateTime(c));
         }
-        else if (fieldClazz[i] == LocalDate.class) {
-            methods[i].invoke(t, row.getLocalDate(c));
+        else if (fieldClazz == LocalDate.class) {
+            ec.method.invoke(t, row.getLocalDate(c));
         }
-        else if (fieldClazz[i] == LocalTime.class) {
-            methods[i].invoke(t, row.getLocalTime(c));
+        else if (fieldClazz == LocalTime.class) {
+            ec.method.invoke(t, row.getLocalTime(c));
         }
-        else if (fieldClazz[i] == Character.class) {
-            methods[i].invoke(t, row.getChar(c));
+        else if (fieldClazz == Character.class) {
+            ec.method.invoke(t, row.getChar(c));
         }
-        else if (fieldClazz[i] == Byte.class) {
-            methods[i].invoke(t, row.getByte(c));
+        else if (fieldClazz == Byte.class) {
+            ec.method.invoke(t, row.getByte(c));
         }
-        else if (fieldClazz[i] == Short.class) {
-            methods[i].invoke(t, row.getShort(c));
+        else if (fieldClazz == Short.class) {
+            ec.method.invoke(t, row.getShort(c));
         }
-        else if (fieldClazz[i] == char.class) {
+        else if (fieldClazz == char.class) {
             Character v;
-            methods[i].invoke(t, (v = row.getChar(c)) != null ? v : '\0');
+            ec.method.invoke(t, (v = row.getChar(c)) != null ? v : '\0');
         }
-        else if (fieldClazz[i] == byte.class) {
+        else if (fieldClazz == byte.class) {
             Byte v;
-            methods[i].invoke(t, (v = row.getByte(c)) != null ? v : 0);
+            ec.method.invoke(t, (v = row.getByte(c)) != null ? v : 0);
         }
-        else if (fieldClazz[i] == short.class) {
+        else if (fieldClazz == short.class) {
             Short v;
-            methods[i].invoke(t, (v = row.getShort(c)) != null ? v : 0);
+            ec.method.invoke(t, (v = row.getShort(c)) != null ? v : 0);
+        }
+        else if (fieldClazz == RowNum.class) {
+            ec.method.invoke(t, row.getRowNum());
+        }
+    }
+
+    /**
+     * Ignore some columns, override this method to add custom filtering
+     *
+     * @param ao {@code Method} or {@code Field}
+     * @return true if ignore current column
+     */
+    protected boolean ignoreColumn(AccessibleObject ao) {
+        return ao.getAnnotation(IgnoreImport.class) != null;
+    }
+
+    /**
+     * Attach some others column
+     *
+     * @param clazz Target class
+     * @return a column and Write method mapping
+     */
+    protected Map<String, Method> attachOtherColumn(Class<?> clazz) {
+        Method[] writeMethods;
+        try {
+            writeMethods = listDeclaredMethods(clazz, method -> method.getAnnotation(ExcelColumn.class) != null
+                    || method.getAnnotation(RowNum.class) != null);
+        } catch (IntrospectionException e) {
+            LOGGER.warn("Get [" + clazz + "] read declared failed.", e);
+            return Collections.emptyMap();
+        }
+
+        Map<String, Method> tmp = new LinkedHashMap<>();
+
+        methodMapping(clazz, Arrays.stream(writeMethods).filter(m -> m.getParameterCount() == 1).toArray(Method[]::new), tmp);
+
+        return tmp;
+    }
+
+    /**
+     * Create column from {@link ExcelColumn} annotation
+     * <p>
+     * Override the method to extend custom comments
+     *
+     * @param ao {@link AccessibleObject} witch defined the {@code ExcelColumn} annotation
+     * @return the Worksheet's {@link ListSheet.EntryColumn} information
+     */
+    protected ListSheet.EntryColumn createColumn(AccessibleObject ao) {
+        // Filter all ignore column
+        if (ignoreColumn(ao)) return null;
+
+        ao.setAccessible(true);
+        ExcelColumn ec = ao.getAnnotation(ExcelColumn.class);
+        if (ec != null) {
+            return new ListSheet.EntryColumn(ec.value());
         }
-        else if (fieldClazz[i] == RowNum.class) {
-            methods[i].invoke(t, row.getRowNum());
+        // Row Num
+        RowNum rowNum = ao.getAnnotation(RowNum.class);
+        if (rowNum != null) {
+            return new ListSheet.EntryColumn(EMPTY, RowNum.class);
         }
+        return null;
     }
 }
diff --git a/src/main/java/org/ttzero/excel/reader/Row.java b/src/main/java/org/ttzero/excel/reader/Row.java
index 2f224c8e..90e2a112 100644
--- a/src/main/java/org/ttzero/excel/reader/Row.java
+++ b/src/main/java/org/ttzero/excel/reader/Row.java
@@ -60,20 +60,20 @@ import static org.ttzero.excel.util.StringUtil.isNotEmpty;
 public abstract class Row {
     protected final Logger LOGGER = LoggerFactory.getLogger(getClass());
     // Index to row
-    int index = -1;
+    protected int index = -1;
     // Index to first column (zero base)
-    int fc = 0;
+    protected int fc = 0;
     // Index to last column (zero base)
-    int lc = -1;
+    protected int lc = -1;
     // Share cell
-    Cell[] cells;
+    protected Cell[] cells;
     /**
      * The Shared String Table
      */
-    SharedStrings sst;
+    protected SharedStrings sst;
     // The header row
     protected HeaderRow hr;
-    boolean unknownLength;
+    protected boolean unknownLength;
 
     // Cache formulas
     private PreCalc[] sharedCalc;
@@ -81,7 +81,7 @@ public abstract class Row {
     /**
      * The global styles
      */
-    Styles styles;
+    protected Styles styles;
 
     /**
      * The number of row. (one base)
@@ -171,13 +171,17 @@ public abstract class Row {
      *
      * @return header Row
      */
-    HeaderRow asHeader() {
-        HeaderRow hr = HeaderRow.with(this);
-        this.hr = hr;
-        return hr;
+    protected HeaderRow asHeader() {
+        return new HeaderRow().with(this);
     }
 
-    Row setHr(HeaderRow hr) {
+    /**
+     * Setting header row
+     *
+     * @param hr {@link HeaderRow}
+     * @return self
+     */
+    protected Row setHr(HeaderRow hr) {
         this.hr = hr;
         return this;
     }
diff --git a/src/main/java/org/ttzero/excel/reader/XMLRow.java b/src/main/java/org/ttzero/excel/reader/XMLRow.java
index 0cfb6738..c64697a1 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLRow.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLRow.java
@@ -35,15 +35,15 @@ import static org.ttzero.excel.util.StringUtil.swap;
  * value. You can use the {@link #isEmpty()} method to
  * test whether the row data is an empty node. The empty node
  * is defined as: No values and styles and formats. Line like this.
- * <code><row r="x"/></code> You can get the data eq by cell
+ * {@code <row r="x"/>} You can get the data eq by cell
  * subscript like ResultSet: {@link #getInt(int)} to get the current
  * line The data in the second column, the subscript, starts at 0.
  *
  * @author guanquan.wang on 2018-09-22
  */
-class XMLRow extends Row {
-    int startRow;
-    StringBuilder buf;
+public class XMLRow extends Row {
+    protected int startRow;
+    protected StringBuilder buf;
 
     /**
      * The number of row. (one base)
@@ -71,9 +71,9 @@ class XMLRow extends Row {
     }
 
     @SuppressWarnings("unused")
-    XMLRow() { }
+    public XMLRow() { }
 
-    XMLRow(SharedStrings sst, Styles styles, int startRow) {
+    public XMLRow(SharedStrings sst, Styles styles, int startRow) {
         this.sst = sst;
         this.styles = styles;
         this.startRow = startRow;
@@ -81,12 +81,12 @@ class XMLRow extends Row {
     }
 
     /////////////////////////unsafe////////////////////////
-    char[] cb;
-    int from, to;
-    int cursor, e;
+    protected char[] cb;
+    protected int from, to;
+    protected int cursor, e;
 
     ///////////////////////////////////////////////////////
-    XMLRow with(char[] cb, int from, int size) {
+    protected XMLRow with(char[] cb, int from, int size) {
 //        LOGGER.debug(new String(cb, from, size));
         this.cb = cb;
         this.from = from;
@@ -98,7 +98,7 @@ class XMLRow extends Row {
     }
 
     /* empty row*/
-    XMLRow empty(char[] cb, int from, int size) {
+    protected XMLRow empty(char[] cb, int from, int size) {
 //        LOGGER.debug(new String(cb, from, size));
         this.cb = cb;
         this.from = from;
@@ -123,7 +123,7 @@ class XMLRow extends Row {
         }
     }
 
-    int searchSpan() {
+    protected int searchSpan() {
         int i = from + 4, _lc = lc;
         for (; cb[i] != '>'; i++) {
             if (cb[i] <= ' ' && cb[i + 1] == 's' && cb[i + 2] == 'p'
@@ -158,7 +158,7 @@ class XMLRow extends Row {
     /**
      * Loop parse cell
      */
-    void parseCells() {
+    protected void parseCells() {
         cursor = searchSpan();
         for (; cb[cursor++] != '>'; ) ;
         unknownLength = lc < 0;
@@ -241,7 +241,7 @@ class XMLRow extends Row {
         return cell;
     }
 
-    private long toLong(int a, int b) {
+    protected long toLong(int a, int b) {
         boolean _n;
         if (_n = cb[a] == '-') a++;
         long n = cb[a++] - '0';
@@ -251,15 +251,15 @@ class XMLRow extends Row {
         return _n ? -n : n;
     }
 
-    private String toString(int a, int b) {
+    protected String toString(int a, int b) {
         return new String(cb, a, b - a);
     }
 
-    private double toDouble(int a, int b) {
+    protected double toDouble(int a, int b) {
         return Double.parseDouble(toString(a, b));
     }
 
-    private boolean isNumber(int a, int b) {
+    protected boolean isNumber(int a, int b) {
         if (a == b) return false;
         if (cb[a] == '-') a++;
         for (; a < b; ) {
@@ -269,7 +269,7 @@ class XMLRow extends Row {
         return a == b;
     }
 
-    private boolean isDouble(int a, int b) {
+    protected boolean isDouble(int a, int b) {
         if (a == b) return false;
         if (cb[a] == '-') a++;
         for (char i = 0, e = 0; a < b; ) {
@@ -296,7 +296,7 @@ class XMLRow extends Row {
     }
 
     /* Found specify target  */
-    private int get(char c) {
+    protected int get(char c) {
         // Ignore all attributes
         return get(null, c, null);
     }
@@ -309,7 +309,7 @@ class XMLRow extends Row {
      * @param attrConsumer an attribute consumer
      * @return the start index of value
      */
-    int get(Cell cell, char c, Attribute attrConsumer) {
+    protected int get(Cell cell, char c, Attribute attrConsumer) {
         for (; cursor < e && (cb[cursor] != '<' || cb[cursor + 1] != c
             || cb[cursor + 2] != '>' && cb[cursor + 2] > ' ' && cb[cursor + 2] != '/'); cursor++) ;
         if (cursor == e) return cursor;
@@ -355,18 +355,18 @@ class XMLRow extends Row {
      *
      * @return the end index of string value
      */
-    private int getT() {
+    protected int getT() {
         return get('t');
     }
 
     /**
      * Found value tag range
      *
-     * Code like this {@code <v>0</v>
+     * Code like this {@code <v>0</v>}
      *
      * @return the end index of int value
      */
-    private int getV() {
+    protected int getV() {
         return get('v');
     }
 
@@ -375,7 +375,7 @@ class XMLRow extends Row {
      *
      * @param cell current {@link Cell}
      */
-    void parseCellValue(Cell cell) {
+    protected void parseCellValue(Cell cell) {
         // @Mark: Ignore Formula string default
 
         // Get value
@@ -490,7 +490,7 @@ class XMLCalcRow extends XMLRow {
      * Loop parse cell
      */
     @Override
-    void parseCells() {
+    protected void parseCells() {
         int index = 0;
         cursor = searchSpan();
         for (; cb[cursor++] != '>'; ) ;
@@ -516,7 +516,7 @@ class XMLCalcRow extends XMLRow {
      * @param cell current {@link Cell}
      */
     @Override
-    void parseCellValue(Cell cell) {
+    protected void parseCellValue(Cell cell) {
         // If cell has formula
         if (cell.f || !hasCalcFunc) {
             // Parse calc
@@ -650,7 +650,7 @@ class XMLMergeRow extends XMLRow {
      * @param cell current {@link Cell}
      */
     @Override
-    void parseCellValue(Cell cell) {
+    protected void parseCellValue(Cell cell) {
 
         // Parse value
         super.parseCellValue(cell);
diff --git a/src/main/java/org/ttzero/excel/reader/XMLSheet.java b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
index 396eb263..cca665ff 100644
--- a/src/main/java/org/ttzero/excel/reader/XMLSheet.java
+++ b/src/main/java/org/ttzero/excel/reader/XMLSheet.java
@@ -41,32 +41,32 @@ import java.util.regex.Pattern;
  *
  * @author guanquan.wang on 2018-09-22
  */
-class XMLSheet implements Sheet {
+public class XMLSheet implements Sheet {
     final Logger LOGGER = LoggerFactory.getLogger(getClass());
 
-    XMLSheet() { }
+    public XMLSheet() { }
 
-    String name;
-    int index; // per sheet index of workbook
+    protected String name;
+    protected int index; // per sheet index of workbook
 //        , size = -1; // size of rows per sheet
-    Path path;
-    int id;
+    protected Path path;
+    protected int id;
     /**
      * The Shared String Table
      */
-    SharedStrings sst;
+    protected SharedStrings sst;
     /**
      * The {@link Styles}
      */
-    Styles styles;
-    int startRow = -1; // row index of data
-    HeaderRow header;
-    boolean hidden; // state hidden
+    protected Styles styles;
+    protected int startRow = -1; // row index of data
+    protected HeaderRow header;
+    protected boolean hidden; // state hidden
 
     // Range address of the used area in the current sheet
-    Dimension dimension;
+    protected Dimension dimension;
     // XMLDrawings
-    Drawings drawings;
+    protected Drawings drawings;
 
 
     /**
@@ -74,7 +74,7 @@ class XMLSheet implements Sheet {
      *
      * @param name the worksheet name
      */
-    void setName(String name) {
+    protected void setName(String name) {
         this.name = name;
     }
 
@@ -83,7 +83,7 @@ class XMLSheet implements Sheet {
      *
      * @param path the temp path
      */
-    void setPath(Path path) {
+    protected void setPath(Path path) {
         this.path = path;
     }
 
@@ -92,7 +92,7 @@ class XMLSheet implements Sheet {
      *
      * @param sst the {@link SharedStrings}
      */
-    void setSst(SharedStrings sst) {
+    protected void setSst(SharedStrings sst) {
         this.sst = sst;
     }
 
@@ -101,7 +101,7 @@ class XMLSheet implements Sheet {
      *
      * @param styles the {@link Styles}
      */
-    void setStyles(Styles styles) {
+    protected void setStyles(Styles styles) {
         this.styles = styles;
     }
 
@@ -126,7 +126,7 @@ class XMLSheet implements Sheet {
         return index;
     }
 
-    void setIndex(int index) {
+    protected void setIndex(int index) {
         this.index = index;
     }
 
@@ -145,7 +145,7 @@ class XMLSheet implements Sheet {
      *
      * @param id the id of worksheet
      */
-    void setId(int id) {
+    protected void setId(int id) {
         this.id = id;
     }
 
@@ -154,7 +154,7 @@ class XMLSheet implements Sheet {
      *
      * @param drawings resource info
      */
-    void setDrawings(Drawings drawings) {
+    protected void setDrawings(Drawings drawings) {
         this.drawings = drawings;
     }
 
@@ -195,6 +195,8 @@ class XMLSheet implements Sheet {
 
     /**
      * Test Worksheet is hidden
+     *
+     * @return true: if current sheet is hidden
      */
     @Override
     public boolean isHidden() {
@@ -254,10 +256,10 @@ class XMLSheet implements Sheet {
     private BufferedReader reader;
     private char[] cb; // buffer
     private int nChar, length;
-    boolean eof = false, heof = false; // OPTIONS = false
-    long mark;
+    protected boolean eof = false, heof = false; // OPTIONS = false
+    protected long mark;
 
-    XMLRow sRow;
+    protected XMLRow sRow;
 
     /**
      * Load sheet.xml as BufferedReader
@@ -328,7 +330,7 @@ class XMLSheet implements Sheet {
         } else {
             eof = false;
             mark += nChar;
-            sRow = new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1);
+            sRow = createRow(sst, styles, this.startRow > 0 ? this.startRow : 1);
         }
 
         // Deep read if dimension information not write in header
@@ -490,9 +492,8 @@ class XMLSheet implements Sheet {
         Iterator<Row> nIter = new RowSetIterator(this::nextRow, true);
         if (nIter.hasNext()) {
             Row row = nIter.next();
-            if (header == null)
-                header = row.asHeader();
-            else row.setHr(header);
+            if (header == null) header = row.asHeader();
+            row.setHr(header);
         }
         return nIter;
     }
@@ -549,6 +550,10 @@ class XMLSheet implements Sheet {
         return this;
     }
 
+    protected XMLRow createRow(SharedStrings sst, Styles styles, int startRow) {
+        return new XMLRow(sst, styles, startRow);
+    }
+
     /*
     If the Dimension information is missing from the header,
     you can skip to the end of the file and look at the line
@@ -705,7 +710,7 @@ class XMLSheet implements Sheet {
         throws IOException { }
 
     Row createHeader(char[] cb, int start, int n) {
-        return new XMLRow(sst, styles, this.startRow > 0 ? this.startRow : 1).with(cb, start, n);
+        return createRow(sst, styles, this.startRow > 0 ? this.startRow : 1).with(cb, start, n);
     }
 
     @Override
diff --git a/src/main/java/org/ttzero/excel/util/ReflectUtil.java b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
index 13bfbd4c..cc109bc7 100644
--- a/src/main/java/org/ttzero/excel/util/ReflectUtil.java
+++ b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
@@ -352,16 +352,17 @@ public class ReflectUtil {
         if (writeMethods == null) {
             for (int i = 1; i < propertyDescriptors.length; i++) {
                 PropertyDescriptor pd = propertyDescriptors[i];
-                tmp.put(pd.getName(), mergedMethods[i]);
+                if (i < mergedMethods.length && mergedMethods[i] != null) {
+                    tmp.put(pd.getName(), mergedMethods[i]);
+                }
             }
         } else {
             int i;
             for (int j = 0; j < mergedMethods.length; j++) {
                 i = mergedMethods[j] != null ? indexOf(writeMethods, mergedMethods[j]) : -1;
-                if (i >= 0) {
-                    writeMethods[i] = null;
-                }
-                tmp.put(propertyDescriptors[j].getName(), mergedMethods[j]);
+                if (i >= 0) writeMethods[i] = null;
+                if (mergedMethods[j] != null)
+                    tmp.put(propertyDescriptors[j].getName(), mergedMethods[j]);
             }
 
             i = 0;


Test patch:
diff --git a/src/test/java/org/ttzero/excel/annotation/CellData.java b/src/test/java/org/ttzero/excel/annotation/CellData.java
new file mode 100644
index 00000000..33064288
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/annotation/CellData.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2017-2022, guanquan.wang@yandex.com All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.ttzero.excel.annotation;
+
+import java.math.BigDecimal;
+
+/**
+ * @author guanquan.wang at 2022-05-03 18:38
+ */
+public class CellData<T> {
+
+    private BigDecimal numberValue;
+
+    private String stringValue;
+
+    private Boolean booleanValue;
+
+    /**
+     * The resulting converted data.
+     */
+    private T data;
+
+    public CellData(CellData<T> other) {
+        this.numberValue = other.numberValue;
+        this.stringValue = other.stringValue;
+        this.booleanValue = other.booleanValue;
+        this.data = other.data;
+    }
+
+    public CellData() {}
+
+    public CellData(T data) {
+        this.data = data;
+    }
+
+    public BigDecimal getNumberValue() {
+        return numberValue;
+    }
+
+    public void setNumberValue(BigDecimal numberValue) {
+        this.numberValue = numberValue;
+    }
+
+    public String getStringValue() {
+        return stringValue;
+    }
+
+    public void setStringValue(String stringValue) {
+        this.stringValue = stringValue;
+    }
+
+    public Boolean getBooleanValue() {
+        return booleanValue;
+    }
+
+    public void setBooleanValue(Boolean booleanValue) {
+        this.booleanValue = booleanValue;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+
+
+    @Override
+    public String toString() {
+       if (stringValue != null) {
+           return stringValue;
+       }
+       if (numberValue != null) {
+           return numberValue.toString();
+       }
+       if (booleanValue != null) {
+           return booleanValue.toString();
+       }
+       return data.toString();
+    }
+
+}
diff --git a/src/test/java/org/ttzero/excel/annotation/Converter.java b/src/test/java/org/ttzero/excel/annotation/Converter.java
new file mode 100644
index 00000000..ba6dd069
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/annotation/Converter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2017-2022, guanquan.wang@yandex.com All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.ttzero.excel.annotation;
+
+/**
+ * @author guanquan.wang at 2022-05-03 18:36
+ */
+public interface Converter<T> {
+
+    T convertToJavaData(CellData cellData) throws Exception;
+
+    CellData convertToExcelData(T value) throws Exception;
+
+    final class AutoConverter implements Converter<Object> {
+
+        @Override
+        public Object convertToJavaData(CellData cellData) {
+            return null;
+        }
+
+        @Override
+        public CellData convertToExcelData(Object value) {
+            return null;
+        }
+    }
+
+}
+
diff --git a/src/test/java/org/ttzero/excel/annotation/CustomAnnoReaderTest.java b/src/test/java/org/ttzero/excel/annotation/CustomAnnoReaderTest.java
new file mode 100644
index 00000000..715719f0
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/annotation/CustomAnnoReaderTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2017-2022, guanquan.wang@yandex.com All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.ttzero.excel.annotation;
+
+import org.junit.Test;
+import org.ttzero.excel.customAnno.ExcelProperty;
+import org.ttzero.excel.entity.ListSheet;
+import org.ttzero.excel.entity.style.Styles;
+import org.ttzero.excel.reader.ExcelReadException;
+import org.ttzero.excel.reader.ExcelReader;
+import org.ttzero.excel.reader.HeaderRow;
+import org.ttzero.excel.reader.SharedStrings;
+import org.ttzero.excel.reader.Sheet;
+import org.ttzero.excel.reader.XMLRow;
+import org.ttzero.excel.reader.XMLSheet;
+import org.ttzero.excel.util.DateUtil;
+import org.ttzero.excel.util.StringUtil;
+
+import java.io.IOException;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.ttzero.excel.reader.ExcelReaderTest.testResourceRoot;
+import static org.ttzero.excel.util.StringUtil.EMPTY;
+
+/**
+ * @author guanquan.wang at 2022-05-03 18:34
+ */
+public class CustomAnnoReaderTest {
+
+    @Test public void testCustomeAnno() throws IOException {
+        try (ExcelReader reader = new MyExcelReader(testResourceRoot().resolve("1.xlsx"))) {
+            List<Entry> list = reader.sheets().flatMap(Sheet::dataRows).map(row -> row.to(Entry.class)).collect(Collectors.toList());
+            assert "4 | 3 | XuSu2gFg32 | 2018-11-27 | true | F | false".equals(list.get(0).toString());
+            assert "5 | 5 | 7OZXtUuk | 2018-11-27 | true | P | false".equals(list.get(60).toString());
+            assert "3 | 2 | Ae9CNO6eTu | 2018-11-27 | true | B | false".equals(list.get(93).toString());
+        }
+    }
+
+    public static class MyExcelReader extends ExcelReader {
+        public MyExcelReader(Path path) throws IOException {
+            super(path);
+        }
+
+        @Override
+        protected XMLSheet sheetFactory(int option) {
+            return new XMLSheet() {
+                @Override
+                protected XMLRow createRow(SharedStrings sst, Styles styles, int startRow) {
+                    return new XMLRow(sst, styles, startRow) {
+                        @Override
+                        protected HeaderRow asHeader() {
+                            return new HeaderRow() {
+                                @Override
+                                protected ListSheet.EntryColumn createColumn(AccessibleObject ao) {
+                                    ExcelProperty ep = ao.getAnnotation(ExcelProperty.class);
+                                    if (ep != null) {
+                                        Class<? extends Converter> converterClazz = ep.converter();
+                                        ListSheet.EntryColumn column;
+                                        if (converterClazz != Converter.AutoConverter.class) {
+                                            ConvertColumn cc = new ConvertColumn(ep.value()[0], converterClazz);
+
+                                            try {
+                                                cc.converter = converterClazz.newInstance();
+                                            } catch (IllegalAccessException | InstantiationException e1) {
+                                                LOGGER.warn("无法解析Converter: {}", converterClazz);
+                                            }
+                                            column = cc;
+                                        } else {
+                                            column = new ListSheet.EntryColumn(ep.value()[0]);
+                                        }
+                                        return column;
+                                    }
+                                    // Row Num
+                                    RowNum rowNum = ao.getAnnotation(RowNum.class);
+                                    if (rowNum != null) {
+                                        return new ListSheet.EntryColumn(EMPTY, RowNum.class);
+                                    }
+                                    return null;
+                                }
+
+                                @Override
+                                protected void methodPut(int i, org.ttzero.excel.reader.Row row, Object t) throws IllegalAccessException, InvocationTargetException {
+                                    Class<?> fieldClazz = columns[i].clazz;
+
+                                    // 兼容处理
+                                    if (Converter.class.isAssignableFrom(fieldClazz)) {
+                                        convert((ConvertColumn) columns[i], row, t);
+                                    } else {
+                                        super.methodPut(i, row, t);
+                                    }
+                                }
+
+                                @Override
+                                protected void fieldPut(int i, org.ttzero.excel.reader.Row row, Object t) throws IllegalAccessException {
+                                    Class<?> fieldClazz = columns[i].clazz;
+
+                                    // 兼容处理
+                                    if (Converter.class.isAssignableFrom(fieldClazz)) {
+                                        convert((ConvertColumn) columns[i], row, t);
+                                    } else {
+                                        super.fieldPut(i, row, t);
+                                    }
+                                }
+
+                                private void convert(ConvertColumn column, org.ttzero.excel.reader.Row row, Object t) {
+                                    Converter<?> converter = column.converter;
+                                    if (converter != null) {
+                                        try {
+                                            Object o = converter.convertToJavaData(toCellData(row, column.colIndex));
+                                            if (column.method != null) {
+                                                column.method.invoke(t, o);
+                                            } else {
+                                                column.field.set(t, o);
+                                            }
+                                        } catch (Exception e) {
+                                            throw new ExcelReadException(e);
+                                        }
+                                    }
+                                }
+
+                                private CellData toCellData(org.ttzero.excel.reader.Row row, int i) {
+                                    CellData cellData = new CellData();
+                                    switch (row.getCellType(i)) {
+                                        case STRING: cellData.setStringValue(row.getString(i)); break;
+                                        case INTEGER:
+                                        case LONG: cellData.setNumberValue(row.getDecimal(i)); break;
+                                        case BOOLEAN: cellData.setBooleanValue(row.getBoolean(i)); break;
+                                        default:
+                                            cellData.setData(row.getString(i)); break;
+                                    }
+                                    return cellData;
+                                }
+
+                            }.with(this);
+                        }
+                    };
+                }
+            };
+
+        }
+    }
+
+    public static class GameConverter implements Converter<Integer> {
+        final String[] names = {"未知", "LOL", "WOW", "极品飞车", "守望先锋", "怪物世界"};
+
+        @Override
+        public Integer convertToJavaData(CellData cellData) throws Exception {
+            return StringUtil.indexOf(names, cellData.getStringValue());
+        }
+
+        @Override
+        public CellData convertToExcelData(Integer value) throws Exception {
+            return value >= 0 && value < names.length ? new CellData<>(names[value]) : new CellData<>(names[0]);
+        }
+    }
+
+    public static class Entry {
+        @RowNum
+        private int row;
+        @ExcelProperty("渠道ID")
+        private Integer channelId;
+        @ExcelProperty(value = "游戏", converter = GameConverter.class)
+        private Integer gameCode;
+        @ExcelProperty
+        private String account;
+        @ExcelProperty("注册时间")
+        private java.util.Date registered;
+        @ExcelProperty("是否满30级")
+        private boolean up30;
+        @ExcelProperty("敏感信息不导出")
+        private int id; // not export
+        private String address;
+        @ExcelProperty("VIP")
+        private char c;
+
+        private boolean vip;
+
+        public boolean isUp30() {
+            return up30;
+        }
+
+        public void setC(char c) {
+            this.c = c;
+            this.vip = c == 'A';
+        }
+
+        public boolean isVip() {
+            return vip;
+        }
+
+        @Override
+        public String toString() {
+            return channelId + " | "
+                    + gameCode + " | "
+                    + account + " | "
+                    + (registered != null ? DateUtil.toDateString(registered) : null) + " | "
+                    + up30 + " | "
+                    + c + " | "
+                    + isVip()
+                    ;
+        }
+    }
+
+    public static class ConvertColumn extends ListSheet.EntryColumn {
+        private Converter<?> converter;
+
+        public ConvertColumn() {
+        }
+
+        public ConvertColumn(String name) {
+            super(name);
+        }
+
+        public ConvertColumn(String name, Class<?> clazz) {
+            super(name, clazz);
+        }
+
+        public Converter<?> getConverter() {
+            return converter;
+        }
+
+        public void setConverter(Converter<?> converter) {
+            this.converter = converter;
+        }
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/customAnno/ExcelProperty.java b/src/test/java/org/ttzero/excel/customAnno/ExcelProperty.java
new file mode 100644
index 00000000..fc69e6bd
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/customAnno/ExcelProperty.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017-2022, guanquan.wang@yandex.com All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package org.ttzero.excel.customAnno;
+
+import org.ttzero.excel.annotation.Converter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author guanquan.wang at 2022-05-03 18:36
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface ExcelProperty {
+
+    String[] value() default {""};
+
+    int index() default -1;
+
+    Class<? extends Converter> converter() default Converter.AutoConverter.class;
+
+}