Repository: wangguanquan/eec
Pull request #77 (2019-09-25 09:11:55)

Fix#66
Add csv format support


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index b718bd85..b89a78fa 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,7 @@
+Version 0.3.4 ()
+-------------
+1. 支持CSV格式输出
+
 Version 0.3.3 (2019-08-14)
 -------------
 1. 修复导出null值时丢失样式
diff --git a/README.md b/README.md
index 502e267a..6a9c1703 100755
--- a/README.md
+++ b/README.md
@@ -88,6 +88,10 @@ xls格式的读取与xlsx对外暴露完全一样，ExcelReader内部判断文
 
 示例请查找`testReadXLS()`方法。
 
+## CSV格式支持
+
+ExcelWriter支持csv格式，只需要在`writeTo`方法前添加`saveAsCSV()`即可。[测试代码参考](./src/test/java/org/ttzero/excel/entity/csv)
+
 ## 示例
 
 ### 导出示例，更多使用方法请参考test/各测试类
diff --git a/pom.xml b/pom.xml
index acf64afc..7086b946 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,40 +73,45 @@
         <url>git@github.com/wangguanquan/eec.git</url>
     </scm>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>jar-no-fork</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <excludePackageNames>com.google.common.*</excludePackageNames>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
     <profiles>
         <profile>
             <id>release</id>
             <build>
                 <plugins>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-source-plugin</artifactId>
-                        <version>3.0.1</version>
-                        <executions>
-                            <execution>
-                                <phase>package</phase>
-                                <goals>
-                                    <goal>jar-no-fork</goal>
-                                </goals>
-                            </execution>
-                        </executions>
-                    </plugin>
-                    <plugin>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-javadoc-plugin</artifactId>
-                        <version>3.0.1</version>
-                        <executions>
-                            <execution>
-                                <phase>package</phase>
-                                <goals>
-                                    <goal>jar</goal>
-                                </goals>
-                            </execution>
-                        </executions>
-                        <configuration>
-                            <excludePackageNames>com.google.common.*</excludePackageNames>
-                        </configuration>
-                    </plugin>
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-gpg-plugin</artifactId>
diff --git a/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java b/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java
new file mode 100644
index 00000000..a59a2421
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/ICellValueAndStyle.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2019, 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.entity;
+
+import org.ttzero.excel.reader.Cell;
+import org.ttzero.excel.util.DateUtil;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+
+import static org.ttzero.excel.entity.IWorksheetWriter.isBigDecimal;
+import static org.ttzero.excel.entity.IWorksheetWriter.isBool;
+import static org.ttzero.excel.entity.IWorksheetWriter.isChar;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDate;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDateTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDouble;
+import static org.ttzero.excel.entity.IWorksheetWriter.isFloat;
+import static org.ttzero.excel.entity.IWorksheetWriter.isInt;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalDate;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalDateTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLong;
+import static org.ttzero.excel.entity.IWorksheetWriter.isShort;
+import static org.ttzero.excel.entity.IWorksheetWriter.isString;
+import static org.ttzero.excel.entity.IWorksheetWriter.isTime;
+
+/**
+ * Create by guanquan.wang at 2019-09-25 11:24
+ */
+public interface ICellValueAndStyle {
+    /**
+     * Setting cell value and cell styles
+     *
+     * @param cell the cell
+     * @param e    the cell value
+     * @param hc   the header column
+     */
+    void reset(int row, Cell cell, Object e, Sheet.Column hc);
+
+    /**
+     * Returns the worksheet name
+     *
+     * @return name of worksheet
+     */
+    String getFileSuffix();
+
+    /**
+     * Setting cell value
+     *
+     * @param cell  the cell
+     * @param e     the cell value
+     * @param hc    the header column
+     * @param clazz the cell value type
+     */
+    default void setCellValue(int row, Cell cell, Object e, Sheet.Column hc, Class<?> clazz) {
+        if (e == null) {
+            setNullValue(row, cell, hc);
+            return;
+        }
+        boolean hasIntProcessor = hc.processor != null;
+        if (isString(clazz)) {
+            cell.setSv(e.toString());
+        } else if (isDate(clazz)) {
+            cell.setAv(DateUtil.toDateValue((java.util.Date) e));
+        } else if (isDateTime(clazz)) {
+            cell.setIv(DateUtil.toDateTimeValue((Timestamp) e));
+        } else if (isChar(clazz)) {
+            Character c = (Character) e;
+            if (hasIntProcessor) conversion(row, cell, c, hc);
+            else cell.setCv(c);
+        } else if (isShort(clazz)) {
+            Short t = (Short) e;
+            if (hasIntProcessor) conversion(row, cell, t, hc);
+            else cell.setNv(t);
+        } else if (isInt(clazz)) {
+            Integer n = (Integer) e;
+            if (hasIntProcessor) conversion(row, cell, n, hc);
+            else cell.setNv(n);
+        } else if (isLong(clazz)) {
+            cell.setLv((Long) e);
+        } else if (isFloat(clazz)) {
+            cell.setDv((Float) e);
+        } else if (isDouble(clazz)) {
+            cell.setDv((Double) e);
+        } else if (isBool(clazz)) {
+            cell.setBv((Boolean) e);
+        } else if (isBigDecimal(clazz)) {
+            cell.setMv((BigDecimal) e);
+        } else if (isLocalDate(clazz)) {
+            cell.setAv(DateUtil.toDateValue((java.time.LocalDate) e));
+        } else if (isLocalDateTime(clazz)) {
+            cell.setIv(DateUtil.toDateTimeValue((java.time.LocalDateTime) e));
+        } else if (isTime(clazz)) {
+            cell.setTv(DateUtil.toTimeValue((java.sql.Time) e));
+        } else if (isLocalTime(clazz)) {
+            cell.setTv(DateUtil.toTimeValue((java.time.LocalTime) e));
+        } else {
+            cell.setSv(e.toString());
+        }
+    }
+
+    /**
+     * Setting cell value as null
+     *
+     * @param cell  the cell
+     * @param hc    the header column
+     */
+    default void setNullValue(int row, Cell cell, Sheet.Column hc) {
+        boolean hasIntProcessor = hc.processor != null;
+        if (hasIntProcessor) {
+            conversion(row, cell, 0, hc);
+        } else
+            cell.setBlank();
+    }
+
+    /**
+     * Int value conversion to others
+     *
+     * @param cell the cell
+     * @param n    the cell value
+     * @param hc   the header column
+     */
+    default void conversion(int row, Cell cell, int n, Sheet.Column hc) {
+        Object e = hc.processor.conversion(n);
+        if (e != null) {
+            Class<?> clazz = e.getClass();
+            if (isInt(clazz)) {
+                if (isChar(clazz)) {
+                    cell.setCv((Character) e);
+                } else if (isShort(clazz)) {
+                    cell.setNv((Short) e);
+                } else {
+                    cell.setNv((Integer) e);
+                }
+            } else {
+                setCellValue(row, cell, e, hc, clazz);
+            }
+        } else {
+            cell.setBlank();
+        }
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/IWorkbookWriter.java b/src/main/java/org/ttzero/excel/entity/IWorkbookWriter.java
index c2b053df..e55cbbee 100644
--- a/src/main/java/org/ttzero/excel/entity/IWorkbookWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/IWorkbookWriter.java
@@ -19,7 +19,11 @@ package org.ttzero.excel.entity;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import static org.ttzero.excel.util.StringUtil.indexOf;
 
 /**
  * Create by guanquan.wang at 2019-04-22 16:00
@@ -63,4 +67,48 @@ public interface IWorkbookWriter extends Storageable {
      * @throws IOException if io error occur
      */
     Path template() throws IOException;
+
+    /**
+     * Move src file into output path
+     *
+     * @param src the src file
+     * @param rootPath the output root path
+     * @param fileName the output file name
+     * @return the output file path
+     * @throws IOException if I/O error occur
+     */
+    default Path reMarkPath(Path src, Path rootPath, String fileName) throws IOException {
+        // If the file exists, add the subscript after the file name.
+        String suffix = getSuffix();
+        Path o = rootPath.resolve(fileName + suffix);
+        if (Files.exists(o)) {
+            final String fname = fileName;
+            Path parent = o.getParent();
+            if (parent != null && Files.exists(parent)) {
+                String[] os = parent.toFile().list((dir, name) ->
+                    new File(dir, name).isFile()
+                        && name.startsWith(fname)
+                        && name.endsWith(suffix)
+                );
+                String new_name;
+                if (os != null) {
+                    int len = os.length, n;
+                    do {
+                        new_name = fname + " (" + len++ + ")" + suffix;
+                        n = indexOf(os, new_name);
+                    } while (n > -1);
+                } else {
+                    new_name = fname + suffix;
+                }
+                o = parent.resolve(new_name);
+            } else {
+                // Rename to
+                Files.move(src, o, StandardCopyOption.REPLACE_EXISTING);
+                return o;
+            }
+        }
+        // Rename to xlsx
+        Files.move(src, o);
+        return o;
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/entity/ListMapSheet.java b/src/main/java/org/ttzero/excel/entity/ListMapSheet.java
index 75ec00da..e518288e 100644
--- a/src/main/java/org/ttzero/excel/entity/ListMapSheet.java
+++ b/src/main/java/org/ttzero/excel/entity/ListMapSheet.java
@@ -158,7 +158,7 @@ public class ListMapSheet extends ListSheet<Map<String, ?>> {
                 Cell cell = cells[i];
                 cell.clear();
 
-                setCellValueAndStyle(cell, e, hc);
+                cellValueAndStyle.reset(rows, cell, e, hc);
             }
         }
     }
diff --git a/src/main/java/org/ttzero/excel/entity/ListSheet.java b/src/main/java/org/ttzero/excel/entity/ListSheet.java
index a697ba2e..a1bd1ad8 100644
--- a/src/main/java/org/ttzero/excel/entity/ListSheet.java
+++ b/src/main/java/org/ttzero/excel/entity/ListSheet.java
@@ -257,7 +257,7 @@ public class ListSheet<T> extends Sheet {
                     else
                         e = fields[i].get(o);
 
-                    setCellValueAndStyle(cell, e, columns[i]);
+                    cellValueAndStyle.reset(rows, cell, e, columns[i]);
                 }
             }
         } catch (IllegalAccessException | InvocationTargetException e) {
@@ -401,6 +401,7 @@ public class ListSheet<T> extends Sheet {
                         continue;
                     }
 
+                    method.setAccessible(true);
                     methods[i] = method;
                     ExcelColumn mec = method.getAnnotation(ExcelColumn.class);
                     if (mec != null && isNotEmpty(mec.value())) {
@@ -434,6 +435,7 @@ public class ListSheet<T> extends Sheet {
             if (readLength > 0) {
                 for (int j = 0; j < readLength; j++) {
                     Method readMethod = readMethods[j];
+                    readMethod.setAccessible(true);
                     methods[i++] = readMethod;
                     ExcelColumn mec = readMethod.getAnnotation(ExcelColumn.class);
                     list.add(new Column(mec.value(), readMethod.getName(), readMethod.getReturnType())
@@ -474,6 +476,7 @@ public class ListSheet<T> extends Sheet {
             for (int i = 0; i < columns.length; i++) {
                 Column hc = columns[i];
                 methods[i] = tmp.get(hc.key);
+                if (methods[i] != null) methods[i].setAccessible(true);
 
                 for (Field field : declaredFields) {
                     if (hc.key.equals(field.getName())) {
diff --git a/src/main/java/org/ttzero/excel/entity/ResultSetSheet.java b/src/main/java/org/ttzero/excel/entity/ResultSetSheet.java
index 6cba5759..0377016b 100644
--- a/src/main/java/org/ttzero/excel/entity/ResultSetSheet.java
+++ b/src/main/java/org/ttzero/excel/entity/ResultSetSheet.java
@@ -210,7 +210,7 @@ public class ResultSetSheet extends Sheet {
 
                     Object e = rs.getObject(i);
 
-                    setCellValueAndStyle(cell, e, hc);
+                    cellValueAndStyle.reset(rows, cell, e, hc);
                 }
             }
         } catch (SQLException e) {
diff --git a/src/main/java/org/ttzero/excel/entity/Sheet.java b/src/main/java/org/ttzero/excel/entity/Sheet.java
index 8d9885a1..c8fa5592 100644
--- a/src/main/java/org/ttzero/excel/entity/Sheet.java
+++ b/src/main/java/org/ttzero/excel/entity/Sheet.java
@@ -18,6 +18,7 @@ package org.ttzero.excel.entity;
 
 
 import org.ttzero.excel.annotation.TopNS;
+import org.ttzero.excel.entity.e7.XMLWorksheetWriter;
 import org.ttzero.excel.entity.style.Border;
 import org.ttzero.excel.entity.style.Fill;
 import org.ttzero.excel.entity.style.Font;
@@ -144,6 +145,8 @@ public abstract class Sheet implements Cloneable, Storageable {
      */
     protected boolean shouldClose = true;
 
+    protected ICellValueAndStyle cellValueAndStyle;
+
     public int getId() {
         return id;
     }
@@ -156,6 +159,10 @@ public abstract class Sheet implements Cloneable, Storageable {
         this.sheetWriter = sheetWriter;
     }
 
+    public void setCellValueAndStyle(ICellValueAndStyle cellValueAndStyle) {
+        this.cellValueAndStyle = cellValueAndStyle;
+    }
+
     public Sheet() {
         this(null);
     }
@@ -794,7 +801,6 @@ public abstract class Sheet implements Cloneable, Storageable {
         return this;
     }
 
-
     /**
      * Output the export detail info
      *
@@ -916,6 +922,15 @@ public abstract class Sheet implements Cloneable, Storageable {
         return this;
     }
 
+    /**
+     * Returns the odd columns fill style
+     *
+     * @return the fill style value
+     */
+    public int getOddFill() {
+        return oddFill;
+    }
+
     /**
      * Returns the worksheet name
      *
@@ -1062,8 +1077,13 @@ public abstract class Sheet implements Cloneable, Storageable {
         return relManager.likeByTarget(key);
     }
 
+    /**
+     * Returns the worksheet name
+     *
+     * @return name of worksheet
+     */
     public String getFileName() {
-        return "sheet" + id + Const.Suffix.XML;
+        return "sheet" + id + cellValueAndStyle.getFileSuffix();
     }
 
     /**
@@ -1189,165 +1209,11 @@ public abstract class Sheet implements Cloneable, Storageable {
      */
     public void afterSheetAccess(Path workSheetPath) throws IOException {
         // relationship
-        relManager.write(workSheetPath, getFileName());
-
-        // others ...
-    }
-
-    /**
-     * Int value conversion to others
-     *
-     * @param cell the cell
-     * @param n    the cell value
-     * @param hc   the header column
-     */
-    protected void conversion(Cell cell, int n, Column hc) {
-        Object e = hc.processor.conversion(n);
-        if (e != null) {
-            Class<?> clazz = e.getClass();
-            if (isInt(clazz)) {
-                if (isChar(clazz)) {
-                    cell.setCv((Character) e);
-                } else if (isShort(clazz)) {
-                    cell.setNv((Short) e);
-                } else {
-                    cell.setNv((Integer) e);
-                }
-                cell.xf = getStyleIndex(hc, e);
-            } else {
-                setCellValue(cell, e, hc, clazz);
-                int style = hc.getCellStyle(clazz);
-                cell.xf = getStyleIndex(hc, n, style);
-            }
-        } else {
-            cell.setBlank();
-            cell.xf = getStyleIndex(hc, null);
-        }
-    }
-
-    /**
-     * Setting cell value and cell styles
-     *
-     * @param cell the cell
-     * @param e    the cell value
-     * @param hc   the header column
-     */
-    protected void setCellValueAndStyle(Cell cell, Object e, Column hc) {
-        setCellValue(cell, e, hc, hc.getClazz());
-        if (hc.processor == null) {
-            cell.xf = getStyleIndex(hc, e);
+        if (sheetWriter instanceof XMLWorksheetWriter) {
+            relManager.write(workSheetPath, getFileName());
         }
-    }
-
-    /**
-     * Setting cell value
-     *
-     * @param cell  the cell
-     * @param e     the cell value
-     * @param hc    the header column
-     * @param clazz the cell value type
-     */
-    protected void setCellValue(Cell cell, Object e, Column hc, Class<?> clazz) {
-        if (e == null) {
-            setNullValue(cell, hc);
-            return;
-        }
-        boolean hasIntProcessor = hc.processor != null;
-        if (isString(clazz)) {
-            cell.setSv(e.toString());
-        } else if (isDate(clazz)) {
-            cell.setAv(DateUtil.toDateValue((java.util.Date) e));
-        } else if (isDateTime(clazz)) {
-            cell.setIv(DateUtil.toDateTimeValue((Timestamp) e));
-        } else if (isChar(clazz)) {
-            Character c = (Character) e;
-            if (hasIntProcessor) conversion(cell, c, hc);
-            else cell.setCv(c);
-        } else if (isShort(clazz)) {
-            Short t = (Short) e;
-            if (hasIntProcessor) conversion(cell, t, hc);
-            else cell.setNv(t);
-        } else if (isInt(clazz)) {
-            Integer n = (Integer) e;
-            if (hasIntProcessor) conversion(cell, n, hc);
-            else cell.setNv(n);
-        } else if (isLong(clazz)) {
-            cell.setLv((Long) e);
-        } else if (isFloat(clazz)) {
-            cell.setDv((Float) e);
-        } else if (isDouble(clazz)) {
-            cell.setDv((Double) e);
-        } else if (isBool(clazz)) {
-            cell.setBv((Boolean) e);
-        } else if (isBigDecimal(clazz)) {
-            cell.setMv((BigDecimal) e);
-        } else if (isLocalDate(clazz)) {
-            cell.setAv(DateUtil.toDateValue((java.time.LocalDate) e));
-        } else if (isLocalDateTime(clazz)) {
-            cell.setIv(DateUtil.toDateTimeValue((java.time.LocalDateTime) e));
-        } else if (isTime(clazz)) {
-            cell.setTv(DateUtil.toTimeValue((java.sql.Time) e));
-        } else if (isLocalTime(clazz)) {
-            cell.setTv(DateUtil.toTimeValue((java.time.LocalTime) e));
-        } else {
-            cell.setSv(e.toString());
-        }
-    }
-
-    /**
-     * Setting cell value as null
-     *
-     * @param cell  the cell
-     * @param hc    the header column
-     */
-    protected void setNullValue(Cell cell, Column hc) {
-        boolean hasIntProcessor = hc.processor != null;
-        if (hasIntProcessor) {
-             conversion(cell, 0, hc);
-        } else
-            cell.setBlank();
-    }
-
-    /**
-     * Returns the cell style index
-     *
-     * @param hc    the header column
-     * @param o     the cell value
-     * @param style the default style
-     * @return the style index in xf
-     */
-    private int getStyleIndex(Column hc, Object o, int style) {
-        // Interlaced discoloration
-        if (autoOdd == 0 && isOdd() && !Styles.hasFill(style)) {
-            style |= oddFill;
-        }
-        int styleIndex = hc.styles.of(style);
-        if (hc.styleProcessor != null) {
-            style = hc.styleProcessor.build(o, style, hc.styles);
-            styleIndex = hc.styles.of(style);
-        }
-        return styleIndex;
-    }
 
-    /**
-     * Returns the cell style index
-     *
-     * @param hc the header column
-     * @param o  the cell value
-     * @return the style index in xf
-     */
-    protected int getStyleIndex(Column hc, Object o) {
-        int style = hc.getCellStyle();
-        return getStyleIndex(hc, o, style);
-    }
-
-    /**
-     * Check the odd rows
-     *
-     * @return true if odd rows
-     */
-    protected boolean isOdd() {
-        return (rows & 1) == 1;
+        // others ...
     }
 
     /**
@@ -1482,7 +1348,7 @@ public abstract class Sheet implements Cloneable, Storageable {
      * Each row-block is multiplexed and will be called to reset
      * the data when a row-block is completely written.
      * Call the {@link #getRowBlockSize()} method to get
-     * the row-block size, call the {@link #setCellValueAndStyle(Cell, Object, Column)}
+     * the row-block size, call the {@link ICellValueAndStyle#reset(int, Cell, Object, Column)}
      * method to set value and styles.
      */
     protected abstract void resetBlockData();
diff --git a/src/main/java/org/ttzero/excel/entity/Workbook.java b/src/main/java/org/ttzero/excel/entity/Workbook.java
index 0abe7a18..1c2b62b5 100644
--- a/src/main/java/org/ttzero/excel/entity/Workbook.java
+++ b/src/main/java/org/ttzero/excel/entity/Workbook.java
@@ -18,6 +18,7 @@ package org.ttzero.excel.entity;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.ttzero.excel.entity.csv.CSVWorkbookWriter;
 import org.ttzero.excel.entity.e7.XMLWorkbookWriter;
 import org.ttzero.excel.entity.style.Fill;
 import org.ttzero.excel.entity.style.Styles;
@@ -145,15 +146,8 @@ public class Workbook implements Storageable {
     public Workbook(String name, String creator) {
         this.name = name;
         this.creator = creator;
-        sheets = new Sheet[3]; // 默认建3个sheet页
-
-        sst = new SharedStrings();
+        sheets = new Sheet[3]; // Create three worksheet
         i18N = new I18N();
-        // Create a global styles
-        styles = Styles.create(i18N);
-
-        // Default writer
-        workbookWriter = new XMLWorkbookWriter(this);
     }
 
     /**
@@ -236,10 +230,12 @@ public class Workbook implements Storageable {
      * @return the global {@link SharedStrings}
      */
     public SharedStrings getSst() {
+        // CSV do not need SharedStringTable
+        if (!(workbookWriter instanceof CSVWorkbookWriter) && sst == null)
+            sst = new SharedStrings();
         return sst;
     }
 
-
     /**
      * Returns all {@link Sheet} in this workbook
      *
@@ -313,6 +309,9 @@ public class Workbook implements Storageable {
      * @return the Styles
      */
     public Styles getStyles() {
+        // CSV do not need Styles
+        if (!(workbookWriter instanceof CSVWorkbookWriter) && styles == null)
+            styles = Styles.create(i18N);
         return styles;
     }
 
@@ -368,6 +367,17 @@ public class Workbook implements Storageable {
         return this;
     }
 
+    /**
+     * Returns the {@link IWorkbookWriter}
+     *
+     * @return the workbook writer
+     */
+    public IWorkbookWriter getWorkbookWriter() {
+        if (workbookWriter == null)
+            workbookWriter = new XMLWorkbookWriter(this);
+        return workbookWriter;
+    }
+
     /**
      * Add a {@link Sheet} to the tail
      *
@@ -490,17 +500,9 @@ public class Workbook implements Storageable {
      * @throws SQLException if a database access error occurs
      */
     public Workbook addSheet(String name, String sql, Sheet.Column... columns) throws SQLException {
-        StatementSheet sheet = new StatementSheet(name, columns);
-        PreparedStatement ps = con.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
-        try {
-            ps.setFetchSize(Integer.MIN_VALUE);
-            ps.setFetchDirection(ResultSet.FETCH_REVERSE);
-        } catch (SQLException e) {
-            watch.what("Not support fetch size value of " + Integer.MIN_VALUE);
-        }
-        sheet.setPs(ps);
-        addSheet(sheet);
-        return this;
+        PreparedStatement ps = con.prepareStatement(sql
+            , ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+        return addSheet(name, ps, null, columns);
     }
 
     /**
@@ -545,19 +547,9 @@ public class Workbook implements Storageable {
      */
     public Workbook addSheet(String name, String sql, ParamProcessor pp
         , Sheet.Column... columns) throws SQLException {
-        StatementSheet sheet = new StatementSheet(name, columns);
         PreparedStatement ps = con.prepareStatement(sql
             , ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
-        try {
-            ps.setFetchSize(Integer.MIN_VALUE);
-            ps.setFetchDirection(ResultSet.FETCH_REVERSE);
-        } catch (SQLException e) {
-            watch.what("Not support fetch size value of " + Integer.MIN_VALUE);
-        }
-        pp.build(ps);
-        sheet.setPs(ps);
-        addSheet(sheet);
-        return this;
+        return addSheet(name, ps, pp, columns);
     }
 
     /**
@@ -587,16 +579,7 @@ public class Workbook implements Storageable {
      * @throws SQLException if a database access error occurs
      */
     public Workbook addSheet(String name, PreparedStatement ps, Sheet.Column... columns) throws SQLException {
-        StatementSheet sheet = new StatementSheet(name, columns);
-        try {
-            ps.setFetchSize(Integer.MIN_VALUE);
-            ps.setFetchDirection(ResultSet.FETCH_REVERSE);
-        } catch (SQLException e) {
-            watch.what("Not support fetch size value of " + Integer.MIN_VALUE);
-        }
-        sheet.setPs(ps);
-        addSheet(sheet);
-        return this;
+        return addSheet(name, ps, null, columns);
     }
 
     /**
@@ -627,7 +610,6 @@ public class Workbook implements Storageable {
      * @throws SQLException if a database access error occurs
      */
     public Workbook addSheet(String name, PreparedStatement ps, ParamProcessor pp, Sheet.Column... columns) throws SQLException {
-        ensureCapacityInternal();
         StatementSheet sheet = new StatementSheet(name, columns);
         try {
             ps.setFetchSize(Integer.MIN_VALUE);
@@ -635,7 +617,7 @@ public class Workbook implements Storageable {
         } catch (SQLException e) {
             watch.what("Not support fetch size value of " + Integer.MIN_VALUE);
         }
-        pp.build(ps);
+        if (pp != null) pp.build(ps);
         sheet.setPs(ps);
         addSheet(sheet);
         return this;
@@ -736,6 +718,7 @@ public class Workbook implements Storageable {
      */
     public Workbook saveAsExcel2003() throws OperationNotSupportedException {
         try {
+            // Create Styles and SharedStringTable
             Class<?> clazz = Class.forName("org.ttzero.excel.entity.e3.BIFF8WorkbookWriter");
             Constructor<?> constructor = clazz.getDeclaredConstructor(this.getClass());
             workbookWriter = (IWorkbookWriter) constructor.newInstance(this);
@@ -745,6 +728,17 @@ public class Workbook implements Storageable {
         return this;
     }
 
+    /**
+     * Save file as Comma-Separated Values. Each worksheet corresponds to
+     * a csv file. Default charset is 'UTF8' and separator character is ','.
+     *
+     * @return the {@link Workbook}
+     */
+    public Workbook saveAsCSV() {
+        workbookWriter = new CSVWorkbookWriter(this);
+        return this;
+    }
+
     /**
      * Output the export detail info
      *
@@ -794,6 +788,7 @@ public class Workbook implements Storageable {
      */
     @Override
     public void writeTo(Path path) throws IOException {
+        checkAndInitWriter();
         if (!Files.exists(path)) {
             String name = path.getFileName().toString();
             // write to file
@@ -830,6 +825,7 @@ public class Workbook implements Storageable {
      * @throws ExcelWriteException other runtime error
      */
     public void writeTo(OutputStream os) throws IOException, ExcelWriteException {
+        checkAndInitWriter();
         workbookWriter.writeTo(os);
     }
 
@@ -841,6 +837,7 @@ public class Workbook implements Storageable {
      * @throws ExcelWriteException other runtime error
      */
     public void writeTo(File file) throws IOException, ExcelWriteException {
+        checkAndInitWriter();
         if (!file.getParentFile().exists()) {
             FileUtil.mkdir(file.toPath().getParent());
         }
@@ -898,4 +895,29 @@ public class Workbook implements Storageable {
         this.workbookWriter.setWorkbook(this);
         return this;
     }
+
+    /**
+     * Create some global entry.
+     */
+    protected void init() {
+        // Create SharedStringTable
+        if (sst == null) {
+            sst = new SharedStrings();
+        }
+        // Create a global styles
+        if (styles == null) {
+            styles = Styles.create(i18N);
+        }
+    }
+
+    /**
+     * Check and Create {@link IWorkbookWriter}
+     */
+    protected void checkAndInitWriter() {
+        if (workbookWriter == null) {
+            // Create Styles and SharedStringTable
+            init();
+            workbookWriter = new XMLWorkbookWriter(this);
+        }
+    }
 }
diff --git a/src/main/java/org/ttzero/excel/entity/csv/CSVCellValueAndStyle.java b/src/main/java/org/ttzero/excel/entity/csv/CSVCellValueAndStyle.java
new file mode 100644
index 00000000..2d45b7ed
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/csv/CSVCellValueAndStyle.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2019, 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.entity.csv;
+
+import org.ttzero.excel.entity.ICellValueAndStyle;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.manager.Const;
+import org.ttzero.excel.reader.Cell;
+import org.ttzero.excel.util.DateUtil;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.time.format.DateTimeFormatter;
+
+import static org.ttzero.excel.entity.IWorksheetWriter.isBigDecimal;
+import static org.ttzero.excel.entity.IWorksheetWriter.isBool;
+import static org.ttzero.excel.entity.IWorksheetWriter.isChar;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDate;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDateTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isDouble;
+import static org.ttzero.excel.entity.IWorksheetWriter.isFloat;
+import static org.ttzero.excel.entity.IWorksheetWriter.isInt;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalDate;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalDateTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLocalTime;
+import static org.ttzero.excel.entity.IWorksheetWriter.isLong;
+import static org.ttzero.excel.entity.IWorksheetWriter.isShort;
+import static org.ttzero.excel.entity.IWorksheetWriter.isString;
+import static org.ttzero.excel.entity.IWorksheetWriter.isTime;
+
+/**
+ * Create by guanquan.wang at 2019-09-25 11:46
+ */
+public class CSVCellValueAndStyle implements ICellValueAndStyle {
+    /**
+     * Setting cell value and cell styles
+     *
+     * @param cell the cell
+     * @param e    the cell value
+     * @param hc   the header column
+     */
+    @Override
+    public void reset(int row, Cell cell, Object e, Sheet.Column hc) {
+        setCellValue(row, cell, e, hc, hc.getClazz());
+    }
+
+    /**
+     * Returns the worksheet name
+     *
+     * @return name of worksheet
+     */
+    @Override
+    public String getFileSuffix() {
+        return Const.Suffix.CSV;
+    }
+
+    /**
+     * Setting cell value
+     *
+     * @param cell  the cell
+     * @param e     the cell value
+     * @param hc    the header column
+     * @param clazz the cell value type
+     */
+    @Override
+    public void setCellValue(int row, Cell cell, Object e, Sheet.Column hc, Class<?> clazz) {
+        if (e == null) {
+            setNullValue(row, cell, hc);
+            return;
+        }
+        boolean hasIntProcessor = hc.processor != null;
+        if (isString(clazz)) {
+            cell.setSv(e.toString());
+        } else if (isDate(clazz)) {
+            cell.setSv(DateUtil.toDateString((java.util.Date) e));
+        } else if (isDateTime(clazz)) {
+            cell.setSv(DateUtil.toString((Timestamp) e));
+        } else if (isChar(clazz)) {
+            Character c = (Character) e;
+            if (hasIntProcessor) conversion(row, cell, c, hc);
+            else cell.setCv(c);
+        } else if (isShort(clazz)) {
+            Short t = (Short) e;
+            if (hasIntProcessor) conversion(row, cell, t, hc);
+            else cell.setNv(t);
+        } else if (isInt(clazz)) {
+            Integer n = (Integer) e;
+            if (hasIntProcessor) conversion(row, cell, n, hc);
+            else cell.setNv(n);
+        } else if (isLong(clazz)) {
+            cell.setLv((Long) e);
+        } else if (isFloat(clazz)) {
+            cell.setDv((Float) e);
+        } else if (isDouble(clazz)) {
+            cell.setDv((Double) e);
+        } else if (isBool(clazz)) {
+            cell.setBv((Boolean) e);
+        } else if (isBigDecimal(clazz)) {
+            cell.setMv((BigDecimal) e);
+        } else if (isLocalDate(clazz)) {
+            cell.setSv(((java.time.LocalDate) e).toString());
+        } else if (isLocalDateTime(clazz)) {
+            cell.setSv(DateUtil.LOCAL_DATE_TIME.format((java.time.LocalDateTime) e));
+        } else if (isTime(clazz)) {
+            cell.setSv(DateTimeFormatter.ISO_TIME.format(((java.sql.Time) e).toLocalTime()));
+        } else if (isLocalTime(clazz)) {
+            cell.setSv(DateTimeFormatter.ISO_TIME.format((java.time.LocalTime) e));
+        } else {
+            cell.setSv(e.toString());
+        }
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/csv/CSVWorkbookWriter.java b/src/main/java/org/ttzero/excel/entity/csv/CSVWorkbookWriter.java
new file mode 100644
index 00000000..1c39e8a1
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/csv/CSVWorkbookWriter.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2019, 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.entity.csv;
+
+import org.ttzero.excel.entity.ExcelWriteException;
+import org.ttzero.excel.entity.IWorkbookWriter;
+import org.ttzero.excel.entity.IWorksheetWriter;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.Workbook;
+import org.ttzero.excel.manager.Const;
+import org.ttzero.excel.util.FileUtil;
+import org.ttzero.excel.util.StringUtil;
+import org.ttzero.excel.util.ZipUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Write data as Comma-Separated Values format
+ *
+ * Create by guanquan.wang at 2019-08-21 21:46
+ */
+public class CSVWorkbookWriter implements IWorkbookWriter {
+    private Workbook workbook;
+    // The csv suffix
+    private String suffix = Const.Suffix.CSV;
+
+    public CSVWorkbookWriter() { }
+
+    public CSVWorkbookWriter(Workbook workbook) {
+        this.workbook = workbook;
+    }
+
+    @Override
+    public void setWorkbook(Workbook workbook) {
+        this.workbook = workbook;
+    }
+
+    /**
+     * The Comma-Separated Values format suffix
+     *
+     * @return const value '.csv'
+     */
+    @Override
+    public String getSuffix() {
+        return suffix;
+    }
+
+    /**
+     * Write to OutputStream
+     *
+     * @param os the out put stream
+     * @throws IOException         if io error occur
+     */
+    @Override
+    public void writeTo(OutputStream os) throws IOException {
+        Path path = createTemp();
+        Files.copy(path, os);
+        cleanTmp(path);
+    }
+
+    /**
+     * Write to file
+     *
+     * @param file the storage file
+     * @throws IOException         if io error occur
+     */
+    @Override
+    public void writeTo(File file) throws IOException {
+        Path path = createTemp();
+        FileUtil.cp(path, file);
+        cleanTmp(path);
+    }
+
+    /**
+     * The Comma-Separated Values format do not support.
+     *
+     * @return the template path
+     * @throws UnsupportedOperationException not support
+     */
+    @Override
+    public Path template() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void writeTo(Path root) throws IOException {
+        Path path = createTemp();
+        reMarkPath(path, root);
+        cleanTmp(path);
+    }
+
+    /**
+     * Clean tmp folders
+     *
+     * @param path the tmp path
+     */
+    private void cleanTmp(Path path) {
+        int i = 0;
+        for (Path sub : path) {
+            i++;
+            if (sub.toString().startsWith(Const.EEC_PREFIX)) {
+                break;
+            }
+        }
+        if (i < path.getNameCount()) {
+            FileUtil.rm_rf(path.getRoot().resolve(path.subpath(0, i)).toFile(), true);
+            workbook.what("0005");
+        }
+    }
+
+    protected void reMarkPath(Path src, Path path) throws IOException {
+        String name;
+        if (StringUtil.isEmpty(name = workbook.getName())) {
+            name = workbook.getI18N().getOrElse("non-name-file", "Non name");
+        }
+
+        Path resultPath = reMarkPath(src, path, name);
+        workbook.what("0006", resultPath.toString());
+    }
+
+    // Create csv file
+    protected Path createTemp() throws IOException, ExcelWriteException {
+        Sheet[] sheets = workbook.getSheets();
+        for (int i = 0; i < sheets.length; i++) {
+            Sheet sheet = sheets[i];
+            IWorksheetWriter worksheetWriter = getWorksheetWriter(sheet);
+            sheet.setSheetWriter(worksheetWriter);
+            sheet.setId(i + 1);
+            // default worksheet name
+            if (StringUtil.isEmpty(sheet.getName())) {
+                sheet.setName("Sheet" + (i + 1));
+            }
+            // Set cell value and style processor
+            sheet.setCellValueAndStyle(new CSVCellValueAndStyle());
+        }
+        workbook.what("0001");
+
+        Path root = null;
+        try {
+            root = FileUtil.mktmp(Const.EEC_PREFIX);
+            workbook.what("0002", root.toString());
+
+            // Write worksheet data one by one
+            for (int i = 0; i < workbook.getSize(); i++) {
+                Sheet e = workbook.getSheetAt(i);
+                e.writeTo(root);
+                e.close();
+            }
+
+            // Zip compress if multi worksheet occur
+            if (workbook.getSize() > 1) {
+                suffix = Const.Suffix.ZIP;
+                Path zipFile = ZipUtil.zipExcludeRoot(root, root);
+                workbook.what("0004", zipFile.toString());
+                FileUtil.rm_rf(root.toFile(), true);
+                return zipFile;
+            } else {
+                return root.resolve(workbook.getSheetAt(0).getName() + Const.Suffix.CSV);
+            }
+        } catch (IOException | ExcelWriteException e) {
+            // remove temp path
+            if (root != null) FileUtil.rm_rf(root);
+            throw e;
+        }
+    }
+
+    // --- Customize worksheet writer
+
+    protected IWorksheetWriter getWorksheetWriter(Sheet sheet) {
+        return new CSVWorksheetWriter(sheet);
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java b/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java
new file mode 100644
index 00000000..c56d7acf
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/csv/CSVWorksheetWriter.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2019, 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.entity.csv;
+
+import org.ttzero.excel.entity.IWorksheetWriter;
+import org.ttzero.excel.entity.Row;
+import org.ttzero.excel.entity.RowBlock;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.manager.Const;
+import org.ttzero.excel.reader.Cell;
+import org.ttzero.excel.util.CSVUtil;
+import org.ttzero.excel.util.DateUtil;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+
+import static org.ttzero.excel.reader.Cell.BOOL;
+import static org.ttzero.excel.reader.Cell.CHARACTER;
+import static org.ttzero.excel.reader.Cell.DATE;
+import static org.ttzero.excel.reader.Cell.DATETIME;
+import static org.ttzero.excel.reader.Cell.DECIMAL;
+import static org.ttzero.excel.reader.Cell.DOUBLE;
+import static org.ttzero.excel.reader.Cell.INLINESTR;
+import static org.ttzero.excel.reader.Cell.LONG;
+import static org.ttzero.excel.reader.Cell.NUMERIC;
+import static org.ttzero.excel.reader.Cell.SST;
+import static org.ttzero.excel.reader.Cell.TIME;
+import static org.ttzero.excel.util.StringUtil.isNotEmpty;
+
+/**
+ * Create by guanquan.wang at 2019-08-21 22:19
+ */
+public class CSVWorksheetWriter implements IWorksheetWriter {
+    private Sheet sheet;
+    private CSVUtil.Writer writer;
+
+    public CSVWorksheetWriter(Sheet sheet) {
+        this.sheet = sheet;
+    }
+
+    /**
+     * The row limit
+     *
+     * @return the const value {@code (1 << 31) - 1}
+     */
+    @Override
+    public int getRowLimit() {
+        return Integer.MAX_VALUE;
+    }
+
+    /**
+     * The column limit
+     *
+     * @return the const value 16_384
+     */
+    @Override
+    public int getColumnLimit() {
+        return Const.Limit.MAX_COLUMNS_ON_SHEET;
+    }
+
+    @Override
+    public void writeTo(Path path, Supplier<RowBlock> supplier) throws IOException {
+        Path workSheetPath = initWriter(path);
+        // Get the first block
+        RowBlock rowBlock = supplier.get();
+
+        // write before
+        writeBefore();
+
+        if (rowBlock != null && rowBlock.hasNext()) {
+            do {
+                // write row-block data
+                writeRow(rowBlock.next());
+                // end of row
+                if (rowBlock.isEof()) break;
+            } while ((rowBlock = supplier.get()) != null);
+        }
+        // Write some final info
+        sheet.afterSheetAccess(workSheetPath);
+    }
+
+    @Override
+    public IWorksheetWriter setWorksheet(Sheet sheet) {
+        return null;
+    }
+
+    @Override
+    public IWorksheetWriter clone() {
+        return null;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (writer != null)
+            writer.close();
+    }
+
+    @Override
+    public void writeTo(Path root) throws IOException {
+        Path workSheetPath = initWriter(root);
+        // Get the first block
+        RowBlock rowBlock = sheet.nextBlock();
+
+        // write before
+        writeBefore();
+
+        if (rowBlock.hasNext()) {
+            for (; ; ) {
+                // write row-block data
+                for (; rowBlock.hasNext(); writeRow(rowBlock.next())) ;
+                // end of row
+                if (rowBlock.isEof()) break;
+                // Get the next block
+                rowBlock = sheet.nextBlock();
+            }
+        }
+        // Write some final info
+        sheet.afterSheetAccess(workSheetPath);
+    }
+
+    protected Path initWriter(Path root) throws IOException {
+        Path workSheetPath = root.resolve(sheet.getName() + Const.Suffix.CSV);
+        writer = CSVUtil.newWriter(workSheetPath);
+        return workSheetPath;
+    }
+
+    /**
+     * Write worksheet header data
+     *
+     * @throws IOException if I/O error occur
+     */
+    protected void writeBefore() throws IOException {
+        // The header columns
+        Sheet.Column[] columns = sheet.getHeaderColumns();
+        boolean noneHeader = columns == null || columns.length == 0;
+
+        if (!noneHeader) {
+            for (Sheet.Column hc : columns) {
+                writer.write(isNotEmpty(hc.getName()) ? hc.getName() : hc.key);
+            }
+            writer.newLine();
+        }
+    }
+
+    /**
+     * Write a row data
+     *
+     * @param row a row data
+     * @throws IOException if I/O error occur
+     */
+    protected void writeRow(Row row) throws IOException {
+        Cell[] cells = row.getCells();
+        for (Cell cell : cells) {
+            switch (cell.t) {
+                case INLINESTR:
+                case SST:
+                    writer.write(cell.sv);
+                    break;
+                case NUMERIC:
+                    writer.write(cell.nv);
+                    break;
+                case LONG:
+                    writer.write(cell.lv);
+                    break;
+                case DOUBLE:
+                    writer.write(cell.dv);
+                    break;
+                case BOOL:
+                    writer.write(cell.bv);
+                    break;
+                case DECIMAL:
+                    writer.write(cell.mv.toString());
+                    break;
+                case CHARACTER:
+                    writer.writeChar(cell.cv);
+                    break;
+                case DATE:
+                    writer.write(DateUtil.toDateString(DateUtil.toDate(cell.nv)));
+                    break;
+                case DATETIME:
+                    writer.write(DateUtil.toString(DateUtil.toDate(cell.dv)));
+                    break;
+                case TIME:
+                    writer.write(DateUtil.toDate(cell.dv).toString());
+                    break;
+                default:
+                    writer.writeEmpty();
+            }
+        }
+        writer.newLine();
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLCellValueAndStyle.java b/src/main/java/org/ttzero/excel/entity/e7/XMLCellValueAndStyle.java
new file mode 100644
index 00000000..69e2a185
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLCellValueAndStyle.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2019, 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.entity.e7;
+
+import org.ttzero.excel.entity.ICellValueAndStyle;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.style.Styles;
+import org.ttzero.excel.manager.Const;
+import org.ttzero.excel.reader.Cell;
+
+import static org.ttzero.excel.entity.IWorksheetWriter.isChar;
+import static org.ttzero.excel.entity.IWorksheetWriter.isInt;
+import static org.ttzero.excel.entity.IWorksheetWriter.isShort;
+
+/**
+ * Create by guanquan.wang at 2019-09-25 11:25
+ */
+public class XMLCellValueAndStyle implements ICellValueAndStyle {
+    /**
+     * Automatic interlacing color
+     */
+    private int autoOdd;
+    /**
+     * Odd row's background color
+     */
+    private int oddFill;
+
+    public XMLCellValueAndStyle(int autoOdd, int oddFill) {
+        this.autoOdd = autoOdd;
+        this.oddFill = oddFill;
+    }
+
+    /**
+     * Int value conversion to others
+     *
+     * @param cell the cell
+     * @param n    the cell value
+     * @param hc   the header column
+     */
+    @Override
+    public void conversion(int row, Cell cell, int n, Sheet.Column hc) {
+        Object e = hc.processor.conversion(n);
+        if (e != null) {
+            Class<?> clazz = e.getClass();
+            if (isInt(clazz)) {
+                if (isChar(clazz)) {
+                    cell.setCv((Character) e);
+                } else if (isShort(clazz)) {
+                    cell.setNv((Short) e);
+                } else {
+                    cell.setNv((Integer) e);
+                }
+                cell.xf = getStyleIndex(row, hc, e);
+            } else {
+                setCellValue(row, cell, e, hc, clazz);
+                int style = hc.getCellStyle(clazz);
+                cell.xf = getStyleIndex(row, hc, n, style);
+            }
+        } else {
+            cell.setBlank();
+            cell.xf = getStyleIndex(row, hc, null);
+        }
+    }
+
+    /**
+     * Setting cell value and cell styles
+     *
+     * @param cell the cell
+     * @param e    the cell value
+     * @param hc   the header column
+     */
+    @Override
+    public void reset(int row, Cell cell, Object e, Sheet.Column hc) {
+        setCellValue(row, cell, e, hc, hc.getClazz());
+        if (hc.processor == null) {
+            cell.xf = getStyleIndex(row, hc, e);
+        }
+    }
+
+    /**
+     * Returns the cell style index
+     *
+     * @param hc    the header column
+     * @param o     the cell value
+     * @param style the default style
+     * @return the style index in xf
+     */
+    private int getStyleIndex(int rows, Sheet.Column hc, Object o, int style) {
+        // Interlaced discoloration
+        if (autoOdd == 0 && isOdd(rows) && !Styles.hasFill(style)) {
+            style |= oddFill;
+        }
+        int styleIndex = hc.styles.of(style);
+        if (hc.styleProcessor != null) {
+            style = hc.styleProcessor.build(o, style, hc.styles);
+            styleIndex = hc.styles.of(style);
+        }
+        return styleIndex;
+    }
+
+    /**
+     * Returns the cell style index
+     *
+     * @param hc the header column
+     * @param o  the cell value
+     * @return the style index in xf
+     */
+    private int getStyleIndex(int rows, Sheet.Column hc, Object o) {
+        int style = hc.getCellStyle();
+        return getStyleIndex(rows, hc, o, style);
+    }
+
+    /**
+     * Check the odd rows
+     *
+     * @return true if odd rows
+     */
+    private boolean isOdd(int rows) {
+        return (rows & 1) == 1;
+    }
+
+    /**
+     * Returns the worksheet name
+     *
+     * @return name of worksheet
+     */
+    @Override
+    public String getFileSuffix() {
+        return Const.Suffix.XML;
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
index 48170986..4d6451eb 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorkbookWriter.java
@@ -67,7 +67,7 @@ import java.util.Properties;
 public class XMLWorkbookWriter implements IWorkbookWriter {
 
     private Workbook workbook;
-    private RelManager relManager; // 关联管理
+    private RelManager relManager;
 
     public XMLWorkbookWriter() {
         relManager = new RelManager();
@@ -421,7 +421,7 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             if (sheet.getAutoOdd() == -1) {
                 sheet.setAutoOdd(workbook.getAutoOdd());
             }
-            // 默认隔行变色
+            // Interlace changes color as default
             if (sheet.getAutoOdd() == 0) {
                 sheet.setOddFill(workbook.getOddFill() == null ? new Fill(PatternType.solid, new Color(226, 237, 218)) : workbook.getOddFill());
             }
@@ -430,19 +430,21 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             if (StringUtil.isEmpty(sheet.getName())) {
                 sheet.setName("Sheet" + (i + 1));
             }
+            // Set cell value and style processor
+            sheet.setCellValueAndStyle(new XMLCellValueAndStyle(sheet.getAutoOdd(), sheet.getOddFill()));
         }
-        workbook.what("0001"); // 初始化完成
+        workbook.what("0001");
 
         Path root = null;
         try {
-            root = FileUtil.mktmp(Const.EEC_PREFIX); // 创建临时文件
+            root = FileUtil.mktmp(Const.EEC_PREFIX);
             workbook.what("0002", root.toString());
 
             Path xl = Files.createDirectory(root.resolve("xl"));
-            // 最先做水印, 写各sheet时需要使用
+            // Create  watermark first, it need to use when writing each sheet
             madeMark(xl);
 
-            // 写各worksheet内容
+            // Write worksheet data one by one
             for (int i = 0; i < workbook.getSize(); i++) {
                 Sheet e = workbook.getSheetAt(i);
                 e.writeTo(xl);
@@ -479,44 +481,10 @@ public class XMLWorkbookWriter implements IWorkbookWriter {
             name = workbook.getI18N().getOrElse("non-name-file", "Non name");
         }
 
-        reMarkPath(zip, path, name);
+        Path resultPath = reMarkPath(zip, path, name);
+        workbook.what("0006", resultPath.toString());
     }
 
-    protected void reMarkPath(Path zip, Path rootPath, String fileName) throws IOException {
-        // 如果文件存在则在文件名后加下标
-        Path o = rootPath.resolve(fileName + Const.Suffix.EXCEL_07);
-        if (Files.exists(o)) {
-            final String fname = fileName;
-            Path parent = o.getParent();
-            if (parent != null && Files.exists(parent)) {
-                String[] os = parent.toFile().list((dir, name) ->
-                    new File(dir, name).isFile()
-                        && name.startsWith(fname)
-                        && name.endsWith(Const.Suffix.EXCEL_07)
-                );
-                String new_name;
-                if (os != null) {
-                    int len = os.length, n;
-                    do {
-                        new_name = fname + " (" + len++ + ")" + Const.Suffix.EXCEL_07;
-                        n = StringUtil.indexOf(os, new_name);
-                    } while (n > -1);
-                } else {
-                    new_name = fname + Const.Suffix.EXCEL_07;
-                }
-                o = parent.resolve(new_name);
-            } else {
-                // Rename to xlsx
-                Files.move(zip, o, StandardCopyOption.REPLACE_EXISTING);
-                return;
-            }
-        }
-        // Rename to xlsx
-        Files.move(zip, o);
-        workbook.what("0006", o.toString());
-    }
-
-
     // --- TEMPLATE
 
     @Override
diff --git a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
index b351b847..335e2441 100644
--- a/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
+++ b/src/main/java/org/ttzero/excel/entity/e7/XMLWorksheetWriter.java
@@ -101,16 +101,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     @Override
     public void writeTo(Path path, Supplier<RowBlock> supplier) throws IOException {
-        this.workSheetPath = path.resolve("worksheets");
-        if (!Files.exists(this.workSheetPath)) {
-            FileUtil.mkdir(workSheetPath);
-        }
-        sheet.what("0010", sheet.getName());
-
-        Path sheetPath = workSheetPath.resolve(sheet.getFileName());
-
-        this.bw = new ExtBufferedWriter(Files.newBufferedWriter(
-            sheetPath, StandardCharsets.UTF_8));
+        Path sheetPath = initWriter(path);
 
         // Get the first block
         RowBlock rowBlock = supplier.get();
@@ -160,16 +151,7 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
      */
     @Override
     public void writeTo(Path path) throws IOException {
-        this.workSheetPath = path.resolve("worksheets");
-        if (!Files.exists(this.workSheetPath)) {
-            FileUtil.mkdir(workSheetPath);
-        }
-        sheet.what("0010", sheet.getName());
-
-        Path sheetPath = workSheetPath.resolve(sheet.getFileName());
-
-        this.bw = new ExtBufferedWriter(Files.newBufferedWriter(
-            sheetPath, StandardCharsets.UTF_8));
+        Path sheetPath = initWriter(path);
 
         // Get the first block
         RowBlock rowBlock = sheet.nextBlock();
@@ -213,6 +195,21 @@ public class XMLWorksheetWriter implements IWorksheetWriter {
         }
     }
 
+    protected Path initWriter(Path root) throws IOException {
+        this.workSheetPath = root.resolve("worksheets");
+        if (!Files.exists(this.workSheetPath)) {
+            FileUtil.mkdir(workSheetPath);
+        }
+        sheet.what("0010", sheet.getName());
+
+        Path sheetPath = workSheetPath.resolve(sheet.getFileName());
+
+        this.bw = new ExtBufferedWriter(Files.newBufferedWriter(
+            sheetPath, StandardCharsets.UTF_8));
+
+        return sheetPath;
+    }
+
     /**
      * Rest worksheet
      *
diff --git a/src/main/java/org/ttzero/excel/manager/Const.java b/src/main/java/org/ttzero/excel/manager/Const.java
index bba0fa2d..3a61493a 100644
--- a/src/main/java/org/ttzero/excel/manager/Const.java
+++ b/src/main/java/org/ttzero/excel/manager/Const.java
@@ -137,6 +137,14 @@ public interface Const {
          * Png
          */
         String PNG = ".png";
+        /**
+         * CSV
+         */
+        String CSV = ".csv";
+        /**
+         * Zip
+         */
+        String ZIP = ".zip";
     }
 
     /**
diff --git a/src/main/java/org/ttzero/excel/reader/ExcelReader.java b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
index 4ae0b806..0733cb67 100644
--- a/src/main/java/org/ttzero/excel/reader/ExcelReader.java
+++ b/src/main/java/org/ttzero/excel/reader/ExcelReader.java
@@ -44,7 +44,6 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermissions;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
@@ -166,13 +165,7 @@ public class ExcelReader implements AutoCloseable {
      * @throws IOException if I/O error occur
      */
     public static ExcelReader read(InputStream stream, int bufferSize, int cacheSize) throws IOException {
-        Path temp;
-        if (FileUtil.isWindows()) {
-            temp = Files.createTempFile(Const.EEC_PREFIX, null);
-        } else {
-            temp = Files.createTempFile(Const.EEC_PREFIX, null
-                , PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-x---")));
-        }
+        Path temp = FileUtil.mktmp(Const.EEC_PREFIX);
         if (temp == null) {
             throw new IOException("Create temp directory error. Please check your permission");
         }
diff --git a/src/main/java/org/ttzero/excel/util/CSVUtil.java b/src/main/java/org/ttzero/excel/util/CSVUtil.java
new file mode 100644
index 00000000..6cc974c5
--- /dev/null
+++ b/src/main/java/org/ttzero/excel/util/CSVUtil.java
@@ -0,0 +1,1185 @@
+/*
+ * Copyright (c) 2019, 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.util;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.ttzero.excel.util.ExtBufferedWriter.MIN_INTEGER_CHARS;
+import static org.ttzero.excel.util.ExtBufferedWriter.MIN_LONG_CHARS;
+import static org.ttzero.excel.util.ExtBufferedWriter.getChars;
+import static org.ttzero.excel.util.ExtBufferedWriter.stringSize;
+import static org.ttzero.excel.util.FileUtil.mkdir;
+
+/**
+ * Comma-Separated Values
+ * <p>
+ * RFC 4180 standard
+ * Reliance on the standard documented by RFC 4180 can simplify CSV exchange. However,
+ * this standard only specifies handling of text-based fields.
+ * Interpretation of the text of each field is still application-specific.
+ * <p>
+ * RFC 4180 formalized CSV. It defines the MIME type "text/csv", and CSV files that follow its
+ * rules should be very widely portable. Among its requirements:
+ * <p>
+ * 1. MS-DOS-style lines that end with (CR/LF) characters (optional for the last line).
+ * 2. An optional header record (there is no sure way to detect whether it is present,
+ * so care is required when importing).
+ * 3. Each record "should" contain the same number of comma-separated fields.
+ * 4. Any field may be quoted (with double quotes).
+ * 5. Fields containing a line-break, double-quote or commas should be quoted. (If they are not,
+ * the file will likely be impossible to process correctly).
+ * 6. A (double) quote character in a field must be represented by two (double) quote characters.
+ * <p>
+ * Create by guanquan.wang at 2019-02-12 17:27
+ */
+public class CSVUtil {
+    private static Logger logger = LogManager.getLogger(FileUtil.class);
+
+    private CSVUtil() { }
+    private static final char QUOTE = '"';
+    private static final char HT = 9;
+    private static final char LF = 10;
+    private static final char CR = 13;
+    private static final char COMMA = ',';
+    private static final String EMPTY = "";
+
+    // --- Read
+
+    /**
+     * Read csv format file by UTF-8 charset.
+     *
+     * @param path the csv file path
+     * @param clazz the class convert to
+     * @param <T> the result type
+     * @return ArrayList of clazz
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static <T> List<T> read(Path path, Class<T> clazz) throws IOException {
+        return read(path, clazz, null);
+    }
+
+    /**
+     * Read csv format file by UTF-8 charset.
+     *
+     * @param path the csv file path
+     * @return ArrayList of string
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static List<String[]> read(Path path) throws IOException {
+        return read(path, (Charset) null);
+    }
+
+    /**
+     * Read csv format file by UTF-8 charset.
+     *
+     * @param path the csv file path
+     * @param separator the separator character
+     * @return ArrayList of string
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static List<String[]> read(Path path, char separator) throws IOException {
+        return read(path, separator, null);
+    }
+
+    /**
+     * Read csv format file.
+     *
+     * @param path the csv file path
+     * @param clazz the class convert to
+     * @param charset the charset to use for encoding
+     * @param <T> the result type
+     * @return ArrayList of clazz
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static <T> List<T> read(Path path, Class<T> clazz, Charset charset) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Read csv format file.
+     *
+     * @param path the csv file path
+     * @param charset the charset to use for encoding
+     * @return ArrayList of string
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static List<String[]> read(Path path, Charset charset) throws IOException {
+        return read(path, (char) 0x0, charset);
+    }
+
+    /**
+     * Read csv format file.
+     *
+     * @param path the csv file path
+     * @param separator the separator character
+     * @param charset the charset to use for encoding
+     * @return ArrayList of string
+     * @throws IOException if I/O error occur
+     * @since 1.7
+     */
+    public static List<String[]> read(Path path, char separator, Charset charset) throws IOException {
+        // Check comma character and column
+        // FileNotFoundException will be occur
+        O o = init(path, separator, charset);
+        // Empty file
+        if (o == null) {
+            return Collections.emptyList();
+        }
+
+        // Use iterator
+        try (RowsIterator iter = new RowsIterator(o, path, charset)) {
+            List<String[]> result = new ArrayList<>();
+            while (iter.hasNext()) {
+                result.add(iter.next());
+            }
+            return result;
+        }
+    }
+
+    /**
+     * Create a CSV reader.
+     *
+     * @param path the csv file path
+     * @return a stream CSV format reader
+     */
+    public static Reader newReader(Path path) {
+        return newReader(path, null);
+    }
+
+    /**
+     * Create a CSV reader.
+     *
+     * @param path the csv file path
+     * @param charset the charset to use for encoding
+     * @return a stream CSV format reader
+     */
+    public static Reader newReader(Path path, Charset charset) {
+        return new Reader(path, charset);
+    }
+
+    /**
+     * Create a CSV reader.
+     *
+     * @param path the csv file path
+     * @param separator the separator character
+     * @return a stream CSV format reader
+     */
+    public static Reader newReader(Path path, char separator) {
+        Reader reader = newReader(path);
+        reader.separator = separator;
+        return reader;
+    }
+
+    /**
+     * Create a CSV reader.
+     *
+     * @param path the csv file path
+     * @param separator the separator character
+     * @param charset the charset to use for encoding
+     * @return a stream CSV format reader
+     */
+    public static Reader newReader(Path path, char separator, Charset charset) {
+        Reader reader = newReader(path, charset);
+        reader.separator = separator;
+        return reader;
+    }
+
+    // --- Writer
+
+    /**
+     * Save vector object as csv format file
+     *
+     * @param data the vector object to be save
+     * @param path the save path
+     * @throws IOException if I/O error occur
+     */
+    public static void writeTo(List<?> data, Path path) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Create a CSV writer
+     *
+     * @param path the storage path
+     * @return a CSV format writer
+     * @throws IOException no permission or other I/O error occur
+     */
+    public static Writer newWriter(Path path) throws IOException {
+        testOrCreate(path);
+        return new Writer(path);
+    }
+
+    /**
+     * Create a CSV writer
+     *
+     * @param path the storage path
+     * @param charset the charset to use for encoding
+     * @return a CSV format writer
+     * @throws IOException no permission or other I/O error occur
+     */
+    public static Writer newWriter(Path path, Charset charset) throws IOException {
+        testOrCreate(path);
+        return new Writer(path, charset);
+    }
+
+    /**
+     * Create a CSV writer
+     *
+     * @param path the storage path
+     * @param separator the separator character
+     * @return a CSV format writer
+     * @throws IOException no permission or other I/O error occur
+     */
+    public static Writer newWriter(Path path, char separator) throws IOException {
+        testOrCreate(path);
+        Writer writer = new Writer(path);
+        writer.separator = separator;
+        return writer;
+    }
+
+    /**
+     * Create a CSV writer
+     *
+     * @param path the storage path
+     * @param separator the separator character
+     * @param charset the charset to use for encoding
+     * @return a CSV format writer
+     * @throws IOException no permission or other I/O error occur
+     */
+    public static Writer newWriter(Path path, char separator, Charset charset) throws IOException {
+        testOrCreate(path);
+        Writer writer = new Writer(path, charset);
+        writer.separator = separator;
+        return writer;
+    }
+
+    private static void testOrCreate(Path path) throws IOException {
+        if (!Files.exists(path)) {
+            mkdir(path.getParent());
+        }
+    }
+
+    // --PUBLIC inner reader
+
+    /**
+     * A CSV format file reader.
+     *
+     * @since 1.8
+     */
+    public static class Reader implements AutoCloseable {
+
+        private RowsIterator iterator;
+        private Path path;
+        private Charset charset;
+        private char separator;
+
+        private Reader(Path path, Charset charset) {
+            this.path = path;
+            this.charset = charset;
+            this.separator = (char) 0x0;
+        }
+
+        /**
+         * Read csv format file.
+         *
+         * @param clazz the class convert to
+         * @param <T> the result type
+         * @return a stream of clazz array
+         * @since 1.8
+         */
+        public <T> Stream<T> stream(Class<T> clazz) {
+            throw new UnsupportedOperationException();
+        }
+
+        /**
+         * Read csv format file.
+         *
+         * @return a stream of string array
+         * @throws IOException file not exists or read file error.
+         * @since 1.8
+         */
+        public Stream<String[]> stream() throws IOException {
+            // Check comma character and column
+            // FileNotFoundException will be occur
+            O o = init(path, separator, charset);
+            // Empty file
+            if (o == null) {
+                return StreamSupport.stream(emptySql, false);
+            }
+
+            // Use iterator
+            iterator = new RowsIterator(o, path, charset);
+
+            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
+                iterator, Spliterator.ORDERED | Spliterator.NONNULL), false);
+        }
+
+        /**
+         * Read csv format file.
+         * there has only one string array in memory, so do not call 'collect' or 'toArray' function direct.
+         *
+         * @return a stream of sheared string array
+         * @throws IOException file not exists or read file error.
+         * @since 1.8
+         */
+        public Stream<String[]> sharedStream() throws IOException {
+            return sharedStream((char) 0x0);
+        }
+
+        /**
+         * Read csv format file.
+         * there has only one string array in memory, so do not call 'collect' or 'toArray' function direct.
+         *
+         * @param separator the separator character
+         * @return a stream of sheared string array
+         * @throws IOException file not exists or read file error.
+         * @since 1.8
+         */
+        public Stream<String[]> sharedStream(char separator) throws IOException {
+            // Check comma character and column
+            // FileNotFoundException will be occur
+            O o = init(path, separator, charset);
+            // Empty file
+            if (o == null) {
+                return StreamSupport.stream(emptySql, false);
+            }
+
+            // Use iterator
+            iterator = new SharedRowsIterator(o, path, charset);
+
+            return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
+                iterator, Spliterator.ORDERED | Spliterator.NONNULL), false);
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (iterator != null) {
+                try {
+                    iterator.close();
+                } catch (Exception e) {
+                    throw new IOException(e);
+                }
+            }
+        }
+
+        // None item iterator
+        private Spliterator<String[]> emptySql = new Spliterator<String[]>() {
+            @Override
+            public boolean tryAdvance(Consumer<? super String[]> action) {
+                return false;
+            }
+
+            @Override
+            public Spliterator<String[]> trySplit() {
+                return null;
+            }
+
+            @Override
+            public long estimateSize() {
+                return 0;
+            }
+
+            @Override
+            public int characteristics() {
+                return 0;
+            }
+        };
+    }
+
+    // -- PRIVATE inner function
+
+    private static class O {
+        int offset, line;
+        String value;
+        boolean newLine;
+        O(int offset) { this.offset = offset; }
+    }
+
+    /**
+     * Rows iterator
+     */
+    private static class RowsIterator implements AutoCloseable, Iterator<String[]> {
+        private int column;
+        private final char comma;
+        private BufferedReader reader;
+        private char[] chars;
+        private int offset;
+        private int i, _i;
+        private int n;
+        String[] nextRow;
+        private static final int length = 8192;
+        private O o;
+        boolean EOF, load;
+
+        RowsIterator(O o, Path path, Charset charset) throws IOException {
+            this.column = o.offset;
+            this.comma = o.value.charAt(0);
+            this.o = o;
+            // Default charset UTF-8
+            reader = charset != null ? Files.newBufferedReader(path, charset) : Files.newBufferedReader(path);
+            // Ignore the Byte-order mark (BOM)
+            if (o.line > 0) {
+                reader.skip(o.line);
+                o.line = 0;
+            }
+            chars = new char[length];
+            nextRow = new String[column];
+            this.offset = o.offset = 0;
+            load = true;
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (EOF) return false;
+            try {
+                for ( ; ; ) {
+                    if (load) {
+                        n = reader.read(chars, offset, length - offset);
+                        // EOF
+                        if (n <= 0) {
+                            EOF = true;
+                            return nextRow[0] != null;
+                        }
+                        n += offset;
+                        o.offset = 0;
+                        load = false;
+                    }
+                    // Parse a block characters
+                    while (parse(chars, n, o, comma)) {
+                        offset = o.offset;
+                        // Contain more than standard comma-separated fields
+                        if (i == column) {
+                            String[] _array = new String[++column];
+                            System.arraycopy(nextRow, 0, _array, 0, column - 1);
+                            nextRow = _array;
+                        }
+                        nextRow[i++] = o.value;
+                        _i = i;
+                        // End of block
+                        if (offset >= n) {
+                            // An integral row
+                            if (o.newLine) {
+                                o.line++;
+                                // Ignore empty row
+                                if ((i > 1 || nextRow[0] != null)) {
+                                    // Line end of '{comma}'
+                                    if (o.value == null) nextRow[i - 1] = EMPTY;
+                                    i = 0;
+                                    load = true;
+                                    offset = 0;
+                                    return load;
+                                }
+                            }
+                            break;
+                        }
+                        if (!o.newLine) continue;
+                        // Unquoted (CR/LF) characters
+                        o.line++;
+                        // Ignore empty row
+                        if ((i > 1 || nextRow[0] != null)) {
+                            // Line end of '{comma}'
+                            if (o.value == null) nextRow[i - 1] = EMPTY;
+                            i = 0;
+                            return true;
+                        }
+                        i = 0;
+                    }
+                    load = true;
+                    // Move the last character to header
+                    if (offset < n) {
+                        System.arraycopy(chars, offset, chars, 0, offset = n - offset);
+                    } else offset = 0;
+                }
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        }
+
+        @Override
+        public String[] next() {
+            if (nextRow[0] != null || hasNext()) {
+                String[] next = Arrays.copyOf(nextRow, _i);
+                nextRow[0] = null;
+                return next;
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (reader != null) {
+                reader.close();
+            }
+            chars = null;
+            nextRow = null;
+        }
+    }
+
+    /**
+     * Shared Row iterator
+     */
+    private static class SharedRowsIterator extends RowsIterator {
+        /*
+         A flag to mark the next row is ready.
+         */
+        private boolean produced;
+
+        SharedRowsIterator(O o, Path path, Charset charset) throws IOException {
+            super(o, path, charset);
+        }
+
+        @Override
+        public boolean hasNext() {
+            if (produced) return true;
+            return produced = super.hasNext();
+        }
+
+        @Override
+        public String[] next() {
+            if (produced || hasNext()) {
+                produced = false;
+                return nextRow;
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
+    }
+
+    /**
+     * Check comma character and column
+     *
+     * @param path the csv file path
+     * @param separator the separator character
+     * @param charset the charset to use for encoding
+     * @return comma character and column size
+     */
+    private static O init(Path path, char separator, Charset charset) throws IOException {
+        // Test charset
+        Charset bom = charsetTest(path);
+        // Default use UTF-8 charset
+        if (bom == null && charset == null) {
+            charset = StandardCharsets.UTF_8;
+        }
+        if (bom != null) {
+            if (charset == null) {
+                charset = bom;
+                // Print a warring log or reset charset
+            } else if (!charset.equals(bom)) {
+                logger.warn("Maybe the charset is " + bom);
+            }
+        }
+
+        try (BufferedReader reader = Files.newBufferedReader(path, charset)) {
+            int n = 0; // read 10 lines
+            String[] lines = new String[10];
+            String s;
+            while ((s = reader.readLine()) != null && n < lines.length) {
+                if (!s.isEmpty()) {
+                    lines[n++] = s;
+                }
+            }
+
+            // Empty file
+            if (lines[0] == null || lines[0].isEmpty()) {
+                return null;
+            }
+            // No enough information to judge the separator
+            if (n < 10) {
+                logger.warn("No enough information to judge the separator.");
+            }
+
+            // USA/UK CSV file almost use ',' or '\t'
+            // European CSV file almost use ';'
+            String[] commas = separator == 0
+                ? new String[] { String.valueOf(COMMA), String.valueOf(HT), ";" } : new String[] { String.valueOf(separator)};
+            int[][] columns = new int[commas.length][n];
+            for (int i = 0; i < commas.length; i++) {
+                for (int j = 0; j < n; j++) {
+                    columns[i][j] = lines[j].length() - lines[j].replace(commas[i], EMPTY).length();
+                }
+            }
+
+            // Find the most comma character
+            int[] nc = new int[commas.length];
+            for (int i = 0; i < columns.length; i++) {
+                Map<Integer, Integer> c = new HashMap<>();
+                for (int j : columns[i]) {
+                    if (j == 0) continue;
+                    Integer co = c.get(j);
+                    c.put(j, co != null ? co + 1 : 1);
+                }
+                if (c.isEmpty()) continue;
+
+                if (c.size() == 1) {
+                    Map.Entry<Integer, Integer> entry = c.entrySet().iterator().next();
+                    if (entry.getKey() > 65535) {
+                        throw new IOException("Too many columns occur. Max columns 65535 but has " + entry.getKey());
+                    }
+                    // there only read 10 lines, 4-bits be used
+                    nc[i] = (entry.getKey() << 4) + entry.getValue();
+                } else {
+                    int mv = 0, mk = 0;
+                    for (Map.Entry<Integer, Integer> entry : c.entrySet()) {
+                        if (entry.getValue() > mv || entry.getValue() == mv && entry.getKey() > mk) {
+                            mv = entry.getValue();
+                            mk = entry.getKey();
+                            nc[i] = (entry.getKey() << 4) + entry.getValue();
+                        }
+                    }
+                }
+            }
+
+            O o = new O(0);
+            o.line = bom != null ? 1 : 0;
+            n = 0;
+            // Find the final comma and column
+            for (int i = 0; i < nc.length; i++) {
+                int size = nc[i] >>> 4;
+                if (size++ == 0) continue;
+                int count = nc[i] & 0x0F;
+                if (count > n) {
+                    n = count;
+                    o.offset = size;
+                    o.value = commas[i];
+                } else if (count == n && size > o.offset) {
+                    o.offset = size;
+                    o.value = commas[i];
+                }
+            }
+
+            // Comma character not ',', '\t' or ';'
+            if (o.offset == 0) {
+                int count = 0;
+                for (int c : nc) {
+                    count += c;
+                }
+                // All top 10 row has only one word
+                if (count == 0) {
+                    o.offset = 1;
+                    o.value = commas[0];
+                } else {
+                    throw new IOException("Unknown comma character, Please specify a separator.");
+                }
+            }
+            return o;
+        }
+    }
+
+    private static Charset charsetTest(Path path) throws IOException {
+        Charset bom = null;
+        try (InputStream is = Files.newInputStream(path)) {
+            byte[] header = new byte[8];
+            int n = is.read(header);
+            if (n < 1) return null;
+            // 16-bit Unicode
+            if (n >= 2) {
+                // little-endian byte order
+                if ((header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xFE) {
+                    bom = StandardCharsets.UTF_16LE; // UTF-16/UCS-2
+                    // 32-bit Unicode
+                    if (n >= 4 && header[2] == 0x0 && header[3] == 0x0) {
+                        bom = Charset.forName("UTF-32LE"); // UTF-32/UCS-4
+                    }
+                }
+                // big-endian byte order
+                else if ((header[0] & 0xFF) == 0xFE && (header[1] & 0xFF) == 0xFF) {
+                    bom = StandardCharsets.UTF_16BE;
+                }
+            }
+            // 8-bit Unicode
+            if (n >= 3 && (header[0] & 0xFF) == 0xEF && (header[1] & 0xFF) == 0xBB && (header[2] & 0xFF) == 0xBF) {
+                bom = StandardCharsets.UTF_8; // UTF-8
+            }
+            // big-endian byte order UTF-32/UCS-4
+            if (n >= 4 && (header[0] & 0xFF) == 0x0 && (header[1] & 0xFF) == 0x0
+                && (header[2] & 0xFF) == 0xFE && (header[3] & 0xFF) == 0xFF) {
+                bom = Charset.forName("UTF-32BE");
+            }
+        }
+        return bom;
+    }
+
+    /**
+     * Parse char array
+     *
+     * @param chars the data array
+     * @param len the size of data
+     * @param o a cache object
+     * @param comma the separate character
+     * @return true if the char array has more integral string
+     */
+    private static boolean parse(char[] chars, int len, O o, char comma) {
+        int offset = o.offset, i = offset, iq = -1;
+        // the first character is '"'
+        boolean quoted = chars[i] == QUOTE
+            // a integral string
+            , integral = false
+            // if data size less than block length
+            , last_block = len < chars.length;
+
+        if (quoted) i++;
+        for (; i < len; i++) {
+            char c = chars[i];
+            if (c == QUOTE) {
+                if (!quoted) {
+                    throw new RuntimeException("LINE-NUMBER (zero-base): " + o.line + ". Comma-separated values" +
+                        " format error.\nFields containing a line-break, double-quote or commas should be quoted.");
+                }
+                if (i >= len - 1) {
+                    integral = true;
+                    i++;
+                    break;
+                }
+                if (chars[i + 1] == QUOTE) {
+                    iq = ++i;
+                    continue;
+                }
+                if (!last_block && chars[i + 1] != comma && chars[i + 1] != LF
+                    && !(chars[i + 1] == CR && i < len - 2 && chars[i + 2] == LF)) {
+                    throw new RuntimeException("LINE-NUMBER (zero-base): " + o.line + ". Comma-separated values " +
+                        "format error.\nA (double) quote character in a field must be represented by " +
+                        "two (double) quote characters.");
+                }
+                i++;
+                integral = true;
+                break;
+            } else if (c == comma || c == LF) {
+                if (!quoted) {
+                    integral = true;
+                    break;
+                }
+            } else if (c == HT && !quoted) {
+                throw new RuntimeException("LINE-NUMBER (zero-base): " + o.line + ". Comma-separated values " +
+                    "format error.\nA (double) quote character in a field must be represented by " +
+                    "two (double) quote characters.");
+            }
+        }
+
+        if (!integral && last_block) {
+            if (!quoted) integral = true;
+            else throw new RuntimeException("LINE-NUMBER (zero-base): " + o.line + ". Comma-separated values " +
+                "format error.\nA (double) quote character in a field must be represented by " +
+                "two (double) quote characters.");
+        }
+
+        if (integral) {
+            if (quoted) offset++;
+            // a integral string
+            if (offset == i && chars[offset] == LF || offset - i == 1 && chars[offset] == CR && chars[i] == LF)
+                o.value = null;
+            else {
+                o.value = i - offset > 0
+                    ? trim(chars, offset, quoted || chars[i - 1] == CR ? i - offset - 1 : i - offset, iq) : EMPTY;
+            }
+            if (i < len - 1 && chars[i] == CR && chars[i + 1] == LF) {
+                i += 1;
+                o.newLine = true;
+            } else if (i < len) {
+                o.newLine = chars[i] == LF;
+            } else o.newLine = false;
+            offset = i + 1;
+
+            // reset the offset
+            o.offset = offset;
+        }
+        return integral;
+    }
+
+    /**
+     * Returns a string whose value is this string, with any leading and trailing
+     * whitespace removed and double quoted character convert to single quoted character.
+     *
+     * @param chars a block data
+     * @param offset initial offset of the block data.
+     * @param size length of the integral string
+     * @return string
+     */
+    private static String trim(char[] chars, int offset, int size, int iq) {
+        if (size > 0) {
+            int len = offset + size;
+            if (iq >= 0) {
+                System.arraycopy(chars, iq, chars, iq - 1, len - iq);
+                len--;
+                for (int i = iq - 1; i > offset; i--) {
+                    if (chars[i] == QUOTE && chars[i - 1] == QUOTE) {
+                        System.arraycopy(chars, i, chars, i - 1, len - i);
+                        i--;
+                        len--;
+                    }
+                }
+            }
+            return new String(chars, offset, len - offset);
+        }
+        return EMPTY;
+    }
+
+    // --- PUBLIC inner Writer
+
+    /**
+     * A CSV format file writer.
+     *
+     * @since 1.8
+     */
+    public static class Writer implements AutoCloseable {
+
+        private BufferedWriter writer;
+        // Comma separator character, default ','
+        private char separator = COMMA;
+        private int column;
+        // The column index
+        private int i;
+        private char[] cb;
+        private int offset;
+        private final int length = 8192;
+
+        /**
+         * Line separator string.  This is the value of the line.separator
+         * property at the moment that the stream was created.
+         */
+        private char[] lineSeparator = System.lineSeparator().toCharArray();
+
+        /**
+         * Create a CSV format writer
+         *
+         * @param path the storage path
+         * @throws IOException If an I/O error occurs
+         */
+        private Writer(Path path) throws IOException {
+            this.writer = Files.newBufferedWriter(path);
+            init();
+        }
+
+        /**
+         * Create a CSV format writer
+         *
+         * @param path the storage path
+         * @param charset the charset to use for encoding
+         * @throws IOException If an I/O error occurs
+         */
+        private Writer(Path path, Charset charset) throws IOException {
+            this.writer = Files.newBufferedWriter(path, charset);
+            init();
+        }
+
+        private void init() {
+            cb = new char[length];
+        }
+
+        /**
+         * Writes a single character
+         *
+         * @param c int specifying a character to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void writeChar(char c) throws IOException {
+            test();
+            if (c == QUOTE) {
+                checkBound(4);
+                cb[offset++] = QUOTE;
+                cb[offset++] = QUOTE;
+                cb[offset++] = QUOTE;
+                cb[offset++] = QUOTE;
+            } else if (c == LF || c == HT || c == separator || c == COMMA) {
+                checkBound(3);
+                cb[offset++] = QUOTE;
+                cb[offset++] = c;
+                cb[offset++] = QUOTE;
+            } else {
+                checkBound(1);
+                cb[offset++] = c;
+            }
+        }
+
+        /**
+         * Writes a boolean value, will be convert to boolean string upper case
+         *
+         * @param b the int value to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(boolean b) throws IOException {
+            test();
+            if (b) {
+                checkBound(4);
+                cb[offset++] = 'T';
+                cb[offset++] = 'R';
+                cb[offset++] = 'U';
+                cb[offset++] = 'E';
+            } else {
+                checkBound(5);
+                cb[offset++] = 'F';
+                cb[offset++] = 'A';
+                cb[offset++] = 'L';
+                cb[offset++] = 'S';
+                cb[offset++] = 'E';
+            }
+        }
+
+        /**
+         * Writes a int value, will be convert to int string
+         *
+         * @param n the int value to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(int n) throws IOException {
+            test();
+            toChars(n);
+        }
+
+        /**
+         * Writes a long value, will be convert to long string
+         *
+         * @param l the long value to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(long l) throws IOException {
+            test();
+            toChars(l);
+        }
+
+        /**
+         * Writes a single-precision floating-point value, will be convert to single-precision string
+         *
+         * @param f the single-precision floating-point to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(float f) throws IOException {
+            test();
+            String fs = Float.toString(f);
+            int len = fs.length();
+            checkBound(len);
+            fs.getChars(0, len, cb, offset);
+            offset += len;
+        }
+
+        /**
+         * Writes a double-precision floating-point value, will be convert to double-precision string
+         *
+         * @param d the double-precision floating-point to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(double d) throws IOException {
+            test();
+            String ds = Double.toString(d);
+            int len = ds.length();
+            checkBound(len);
+            ds.getChars(0, len, cb, offset);
+            offset += len;
+        }
+
+        /**
+         * Compression and escape char sequence
+         * - line-break, double-quote or commas should be quoted.
+         * - A (double) quote character in a field must be represented by two (double) quote characters.
+         *
+         * @param text the string to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(String text) throws IOException {
+            write(text.toCharArray());
+        }
+
+        /**
+         * Compression and escape char sequence
+         * - line-break, double-quote or commas should be quoted.
+         * - A (double) quote character in a field must be represented by two (double) quote characters.
+         *
+         * @param chars the char sequence to be written
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(char[] chars) throws IOException {
+            write(chars, 0, chars.length);
+        }
+
+        /**
+         * Compression and escape char sequence
+         * - line-break, double-quote or commas should be quoted.
+         * - A (double) quote character in a field must be represented by two (double) quote characters.
+         *
+         * @param chars the char sequence to be written
+         * @param offset the offset index
+         * @param size size of characters
+         * @throws IOException If an I/O error occurs
+         */
+        public void write(char[] chars, int offset, int size) throws IOException {
+            test();
+            int i = 0;
+            int last = offset;
+            boolean quoted = false, shouldBeQuoted = false;
+
+            for ( ; i < size; ) {
+                char c = chars[i++];
+
+                // A (double) quote character in a field must be represented
+                // by two (double) quote characters.
+                if (c == QUOTE) {
+                    quoted = true;
+                    if (last == offset) {
+                        checkBound(1);
+                        cb[this.offset++] = QUOTE;
+                    }
+                    checkBound(i - last + 1);
+                    System.arraycopy(chars, last, cb, this.offset, i - last);
+                    this.offset += (i - last);
+                    cb[this.offset++] = QUOTE;
+                    last = i;
+                }
+
+                else if (c == LF || c == HT || c == separator || c == COMMA) {
+                    shouldBeQuoted = true;
+                }
+
+            }
+
+            if (quoted) {
+                checkBound(i - last + 1);
+                System.arraycopy(chars, last, cb, this.offset, i - last);
+                this.offset += (i - last);
+                cb[this.offset++] = QUOTE;
+            }
+            else if (shouldBeQuoted) {
+                checkBound(size + 2);
+                cb[this.offset++] = QUOTE;
+                System.arraycopy(chars, offset, cb, this.offset, size);
+                this.offset += size;
+                cb[this.offset++] = QUOTE;
+            }
+            else {
+                checkBound(size);
+                System.arraycopy(chars, offset, cb, this.offset, size);
+                this.offset += size;
+            }
+
+        }
+
+        /**
+         * Writes a empty column.
+         * @throws IOException If an I/O error occurs
+         */
+        public void writeEmpty() throws IOException {
+            test();
+        }
+
+        /**
+         * Writes a line separator.  The line separator string is defined by the
+         * system property <tt>line.separator</tt>, and is not necessarily a single
+         * newline ('\n') character.
+         *
+         * @exception  IOException  If an I/O error occurs
+         */
+        public void newLine() throws IOException {
+            checkBound(lineSeparator.length);
+            System.arraycopy(lineSeparator, 0, cb, offset, lineSeparator.length);
+            offset += lineSeparator.length;
+            if (column == 0) column = i;
+            i = 0;
+        }
+
+        /**
+         * Test the column index
+         *
+         * @return true if first column
+         */
+        private boolean test() throws IOException {
+            boolean first = i == 0;
+            i++;
+            if (column > 0 && i > column) {
+                // FIXME maybe throw an exception
+                logger.warn("Each record should contain the same number of comma-separated fields.");
+            }
+            if (!first) {
+                checkBound(1);
+                cb[offset++] = separator;
+            }
+            return first;
+        }
+
+        private void flush() throws IOException {
+            writer.write(cb, 0, offset);
+            offset = 0;
+        }
+
+        private void checkBound(int size) throws IOException {
+            if (offset + size > length) {
+                flush();
+            }
+        }
+
+        private void toChars(int i) throws IOException {
+            if (i == Integer.MIN_VALUE) {
+                checkBound(MIN_INTEGER_CHARS.length);
+                System.arraycopy(MIN_INTEGER_CHARS, 0, cb, offset, MIN_INTEGER_CHARS.length);
+                offset += MIN_INTEGER_CHARS.length;
+            } else {
+                int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
+                checkBound(size);
+                getChars(i, offset += size, cb);
+            }
+        }
+
+        private void toChars(long i) throws IOException {
+            if (i == Long.MIN_VALUE) {
+                checkBound(MIN_LONG_CHARS.length);
+                System.arraycopy(MIN_LONG_CHARS, 0, cb, offset, MIN_LONG_CHARS.length);
+                offset += MIN_LONG_CHARS.length;
+            } else {
+                int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
+                checkBound(size);
+                getChars(i, offset += size, cb);
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (writer != null) {
+                if (offset > 0) {
+                    flush();
+                }
+                writer.close();
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/ttzero/excel/util/DateUtil.java b/src/main/java/org/ttzero/excel/util/DateUtil.java
index a038e322..9157a2b4 100644
--- a/src/main/java/org/ttzero/excel/util/DateUtil.java
+++ b/src/main/java/org/ttzero/excel/util/DateUtil.java
@@ -26,9 +26,14 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
 import java.util.Date;
 import java.util.TimeZone;
 
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
+
 /**
  * A date util ror excel
  * <p>
@@ -47,6 +52,16 @@ public class DateUtil {
         = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
     static final ThreadLocal<SimpleDateFormat> dateFormat
         = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
+    public static final DateTimeFormatter LOCAL_DATE_TIME;
+
+    static {
+        LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
+            .parseCaseInsensitive()
+            .append(ISO_LOCAL_DATE)
+            .appendLiteral(' ')
+            .append(ISO_LOCAL_TIME)
+            .toFormatter();
+    }
 
     public static String toString(Date date) {
         return dateTimeFormat.get().format(date);
diff --git a/src/main/java/org/ttzero/excel/util/ExtBufferedWriter.java b/src/main/java/org/ttzero/excel/util/ExtBufferedWriter.java
index db6b375c..031e1cce 100644
--- a/src/main/java/org/ttzero/excel/util/ExtBufferedWriter.java
+++ b/src/main/java/org/ttzero/excel/util/ExtBufferedWriter.java
@@ -41,8 +41,8 @@ public class ExtBufferedWriter extends BufferedWriter {
     }
 
     private char[][] cache_char_array = new char[25][];
-    private static final char[] MIN_INTEGER_CHARS = {'-', '2', '1', '4', '7', '4', '8', '3', '6', '4', '8'};
-    private static final char[] MIN_LONG_CHARS = "-9223372036854775808".toCharArray();
+    static final char[] MIN_INTEGER_CHARS = {'-', '2', '1', '4', '7', '4', '8', '3', '6', '4', '8'};
+    static final char[] MIN_LONG_CHARS = "-9223372036854775808".toCharArray();
     private static final char[][] ESCAPE_CHARS = new char[63][];
 
     static {
@@ -172,7 +172,7 @@ public class ExtBufferedWriter extends BufferedWriter {
                 return i + 1;
     }
 
-    private static void getChars(int i, int index, char[] buf) {
+    static void getChars(int i, int index, char[] buf) {
         int q, r;
         int charPos = index;
         char sign = 0;
@@ -234,7 +234,7 @@ public class ExtBufferedWriter extends BufferedWriter {
      * <p>
      * Will fail if i == Long.MIN_VALUE
      */
-    private static void getChars(long i, int index, char[] buf) {
+    static void getChars(long i, int index, char[] buf) {
         long q;
         int r;
         int charPos = index;
diff --git a/src/main/java/org/ttzero/excel/util/ReflectUtil.java b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
index bdbd26bf..a01e4121 100644
--- a/src/main/java/org/ttzero/excel/util/ReflectUtil.java
+++ b/src/main/java/org/ttzero/excel/util/ReflectUtil.java
@@ -107,6 +107,7 @@ public class ReflectUtil {
      *
      * @param beanClass The bean class to be analyzed.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listDeclaredMethods(Class<?> beanClass)
         throws IntrospectionException {
@@ -119,6 +120,7 @@ public class ReflectUtil {
      * @param beanClass The bean class to be analyzed.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listDeclaredMethods(Class<?> beanClass, Predicate<Method> predicate)
         throws IntrospectionException {
@@ -135,6 +137,7 @@ public class ReflectUtil {
      *                  methods/properties/events in the stopClass or in its base classes
      *                  will be ignored in the analysis.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listDeclaredMethods(Class<?> beanClass, Class<?> stopClass)
         throws IntrospectionException {
@@ -161,6 +164,7 @@ public class ReflectUtil {
      *                  will be ignored in the analysis.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listDeclaredMethods(Class<?> beanClass, Class<?> stopClass, Predicate<Method> predicate)
         throws IntrospectionException {
@@ -174,6 +178,7 @@ public class ReflectUtil {
      *
      * @param beanClass The bean class to be analyzed.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listReadMethods(Class<?> beanClass) throws IntrospectionException {
         return listReadMethods(beanClass, Object.class);
@@ -187,6 +192,7 @@ public class ReflectUtil {
      *                  methods/properties/events in the stopClass or in its base classes
      *                  will be ignored in the analysis.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listReadMethods(Class<?> beanClass, Class<?> stopClass)
         throws IntrospectionException {
@@ -212,6 +218,7 @@ public class ReflectUtil {
      * @param beanClass The bean class to be analyzed.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listReadMethods(Class<?> beanClass, Predicate<Method> predicate)
         throws IntrospectionException {
@@ -229,6 +236,7 @@ public class ReflectUtil {
      *                  will be ignored in the analysis.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listReadMethods(Class<?> beanClass, Class<?> stopClass, Predicate<Method> predicate)
         throws IntrospectionException {
@@ -242,6 +250,7 @@ public class ReflectUtil {
      *
      * @param beanClass The bean class to be analyzed.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listWriteMethods(Class<?> beanClass) throws IntrospectionException {
         return listWriteMethods(beanClass, Object.class);
@@ -255,6 +264,7 @@ public class ReflectUtil {
      *                  methods/properties/events in the stopClass or in its base classes
      *                  will be ignored in the analysis.
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listWriteMethods(Class<?> beanClass, Class<?> stopClass)
         throws IntrospectionException {
@@ -283,6 +293,7 @@ public class ReflectUtil {
      * @param beanClass The bean class to be analyzed.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listWriteMethods(Class<?> beanClass, Predicate<Method> predicate)
         throws IntrospectionException {
@@ -300,6 +311,7 @@ public class ReflectUtil {
      *                  will be ignored in the analysis.
      * @param predicate A method filter
      * @return all declared method
+     * @throws IntrospectionException happens during introspection error
      */
     public static Method[] listWriteMethods(Class<?> beanClass, Class<?> stopClass, Predicate<Method> predicate)
         throws IntrospectionException {
diff --git a/src/main/resources/I18N/message.zh-CN.properties b/src/main/resources/I18N/message.zh-CN.properties
index 7371933a..670c129f 100644
--- a/src/main/resources/I18N/message.zh-CN.properties
+++ b/src/main/resources/I18N/message.zh-CN.properties
@@ -4,7 +4,7 @@
 0003 = 所有Sheets写入数据完成，开始进行数据压缩...
 0004 = 压缩完成：{}。
 0005 = 删除临时文件。
-0006 = Excel写入完成：{}
+0006 = 写入完成：{}
 0007 = 开始解压缩模版文件...
 0008 = 解压缩模版文件完成。
 0009 = Sheet［{}］写入结束，总写入{}行记录。


Test patch:
diff --git a/src/test/java/org/ttzero/excel/entity/ListMapSheetTest.java b/src/test/java/org/ttzero/excel/entity/ListMapSheetTest.java
index 9aae2be4..dcffb64e 100644
--- a/src/test/java/org/ttzero/excel/entity/ListMapSheetTest.java
+++ b/src/test/java/org/ttzero/excel/entity/ListMapSheetTest.java
@@ -251,12 +251,12 @@ public class ListMapSheetTest extends WorkbookTest {
             .writeTo(defaultTestPath);
     }
 
-    static List<Map<String, ?>> createTestData() {
+    public static List<Map<String, ?>> createTestData() {
         int size = random.nextInt(100) + 1;
         return createTestData(size);
     }
 
-    static List<Map<String, ?>> createTestData(int size) {
+    public static List<Map<String, ?>> createTestData(int size) {
         List<Map<String, ?>> list = new ArrayList<>(size);
         for (int i = 0; i < size; i++) {
             Map<String, Object> map = new HashMap<>();
@@ -267,12 +267,12 @@ public class ListMapSheetTest extends WorkbookTest {
         return list;
     }
 
-    static List<Map<String, ?>> createAllTypeData() {
+    public static List<Map<String, ?>> createAllTypeData() {
         int size = random.nextInt(100) + 1;
         return createAllTypeData(size);
     }
 
-    static List<Map<String, ?>> createAllTypeData(int size) {
+    public static List<Map<String, ?>> createAllTypeData(int size) {
         List<Map<String, ?>> list = new ArrayList<>(size);
         for (int i = 0; i < size; i++) {
             Map<String, Object> map = new HashMap<>();
@@ -296,7 +296,7 @@ public class ListMapSheetTest extends WorkbookTest {
         return list;
     }
 
-    static List<Map<String, ?>> createNullTestData(int size) {
+    public static List<Map<String, ?>> createNullTestData(int size) {
         List<Map<String, ?>> list = new ArrayList<>(size);
         for (int i = 0; i < size; i++) {
             Map<String, Object> map = new HashMap<>();
diff --git a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
index f75551ae..62541250 100644
--- a/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
+++ b/src/test/java/org/ttzero/excel/entity/ListObjectSheetTest.java
@@ -306,7 +306,7 @@ public class ListObjectSheetTest extends WorkbookTest{
             .writeTo(defaultTestPath);
     }
 
-    private StyleProcessor sp = (o, style, sst) -> {
+    public static StyleProcessor sp = (o, style, sst) -> {
         if ((int)o < 60) {
             style = Styles.clearFill(style)
                 | sst.addFill(new Fill(PatternType.solid, Color.orange));
@@ -315,7 +315,7 @@ public class ListObjectSheetTest extends WorkbookTest{
     };
 
     // 定义一个int值转换lambda表达式，成绩低于60分显示"不及格"，其余显示正常分数
-    private IntConversionProcessor conversion = n -> n < 60 ? "不及格" : n;
+    public static IntConversionProcessor conversion = n -> n < 60 ? "不及格" : n;
 
     @Test
     public void testStyleConversion1() throws IOException {
@@ -403,7 +403,7 @@ public class ListObjectSheetTest extends WorkbookTest{
         private int id;
         private String name;
 
-        Item(int id, String name) {
+        public Item(int id, String name) {
             this.id = id;
             this.name = name;
         }
@@ -548,7 +548,7 @@ public class ListObjectSheetTest extends WorkbookTest{
         @ExcelColumn("成绩")
         private int score;
 
-        Student(int id, String name, int score) {
+        protected Student(int id, String name, int score) {
             this.id = id;
             this.name = name;
             this.score = score;
@@ -713,7 +713,7 @@ public class ListObjectSheetTest extends WorkbookTest{
     public static class ExtItem extends Item {
         private String nice;
 
-        ExtItem(int id, String name) {
+        public ExtItem(int id, String name) {
             super(id, name);
         }
 
@@ -721,6 +721,10 @@ public class ListObjectSheetTest extends WorkbookTest{
             return nice;
         }
 
+        public void setNice(String nice) {
+            this.nice = nice;
+        }
+
         public static List<Item> randomTestData(int n) {
             List<Item> list = new ArrayList<>(n);
             for (int i = 0; i < n; i++) {
diff --git a/src/test/java/org/ttzero/excel/entity/WorkbookTest.java b/src/test/java/org/ttzero/excel/entity/WorkbookTest.java
index 3c7f5cc3..d29f81ce 100644
--- a/src/test/java/org/ttzero/excel/entity/WorkbookTest.java
+++ b/src/test/java/org/ttzero/excel/entity/WorkbookTest.java
@@ -32,10 +32,10 @@ public class WorkbookTest {
      * The default output path
      */
     static Path defaultTestPath = Paths.get("target/excel/");
-    String author = "guanquan.wang";
-    static Random random = new Random();
+    public static String author = "guanquan.wang";
+    public static Random random = new Random();
 
-    static char[] charArray = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".toCharArray();
+    public static char[] charArray = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".toCharArray();
     private static char[] cache = new char[32];
     public static String getRandomString() {
         int n = random.nextInt(cache.length) + 1, size = charArray.length;
diff --git a/src/test/java/org/ttzero/excel/entity/csv/ListMapSheetTest.java b/src/test/java/org/ttzero/excel/entity/csv/ListMapSheetTest.java
new file mode 100644
index 00000000..181c0dbc
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/csv/ListMapSheetTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2019, 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.entity.csv;
+
+import org.junit.Test;
+import org.ttzero.excel.Print;
+import org.ttzero.excel.entity.ListMapSheet;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.Workbook;
+import org.ttzero.excel.entity.WorkbookTest;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.ttzero.excel.entity.ListMapSheetTest.createAllTypeData;
+import static org.ttzero.excel.entity.ListMapSheetTest.createNullTestData;
+import static org.ttzero.excel.entity.ListMapSheetTest.createTestData;
+
+/**
+ * Create by guanquan.wang at 2019-04-28 19:16
+ */
+public class ListMapSheetTest extends WorkbookTest {
+
+    @Test public void testWrite() throws IOException {
+        new Workbook("test map")
+            .watch(Print::println)
+            .addSheet(createTestData())
+            .addSheet(createTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testAllType() throws IOException {
+        new Workbook("test all type map")
+            .watch(Print::println)
+            .addSheet(createAllTypeData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testHeaderColumn() throws IOException {
+        new Workbook("test header column map")
+            .watch(Print::println)
+            .addSheet(createAllTypeData()
+                , new Sheet.Column("boolean", "bv", boolean.class)
+                , new Sheet.Column("char", "cv", char.class)
+                , new Sheet.Column("short", "sv", short.class)
+                , new Sheet.Column("int", "nv", int.class)
+                , new Sheet.Column("long", "lv", long.class)
+                , new Sheet.Column("float", "fv", float.class)
+                , new Sheet.Column("double", "dv", double.class)
+                , new Sheet.Column("string", "s", String.class)
+                , new Sheet.Column("decimal", "mv", BigDecimal.class)
+                , new Sheet.Column("date", "av", Date.class)
+                , new Sheet.Column("timestamp", "iv", Timestamp.class)
+                , new Sheet.Column("time", "tv", Time.class)
+                , new Sheet.Column("LocalDate", "ldv", LocalDate.class)
+                , new Sheet.Column("LocalDateTime", "ldtv", LocalDateTime.class)
+                , new Sheet.Column("LocalTime", "ltv", LocalTime.class)
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testHeaderColumnBox() throws IOException {
+        new Workbook("test header column box type map")
+            .watch(Print::println)
+            .addSheet(createAllTypeData()
+                , new Sheet.Column("Character", "cv", Character.class)
+                , new Sheet.Column("Short", "sv", Short.class)
+                , new Sheet.Column("Integer", "nv", Integer.class)
+                , new Sheet.Column("Long", "lv", Long.class)
+                , new Sheet.Column("Float", "fv", Float.class)
+                , new Sheet.Column("Double", "dv", Double.class)
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor1() throws IOException {
+        new Workbook("test list map sheet Constructor1")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor2() throws IOException {
+        new Workbook("test list map sheet Constructor2")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet("Map").setData(createTestData(10)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor3() throws IOException {
+        new Workbook("test list map sheet Constructor3")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet("Map"
+                , new Sheet.Column("boolean", "bv", boolean.class)
+                , new Sheet.Column("char", "cv", char.class)
+                , new Sheet.Column("short", "sv", short.class)
+                , new Sheet.Column("int", "nv", int.class)
+                , new Sheet.Column("long", "lv", long.class)
+                , new Sheet.Column("float", "fv", float.class)
+                , new Sheet.Column("double", "dv", double.class)
+                , new Sheet.Column("string", "s", String.class)
+                , new Sheet.Column("decimal", "mv", BigDecimal.class)
+                , new Sheet.Column("date", "av", Date.class)
+                , new Sheet.Column("timestamp", "iv", Timestamp.class)
+                , new Sheet.Column("time", "tv", Time.class)
+                , new Sheet.Column("LocalDate", "ldv", LocalDate.class)
+                , new Sheet.Column("LocalDateTime", "ldtv", LocalDateTime.class)
+                , new Sheet.Column("LocalTime", "ltv", LocalTime.class)
+            )
+                .setData(createAllTypeData(10)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor5() throws IOException {
+        new Workbook("test list map sheet Constructor5")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet(createAllTypeData(10)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor6() throws IOException {
+        new Workbook("test list map sheet Constructor6")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet("Map", createAllTypeData(10)))
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor7() throws IOException {
+        new Workbook("test list map sheet Constructor7")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet(createAllTypeData(10)
+                , new Sheet.Column("Character", "cv", Character.class)
+                , new Sheet.Column("Short", "sv", Short.class)
+                , new Sheet.Column("Integer", "nv", Integer.class)
+                , new Sheet.Column("Long", "lv", Long.class)
+                , new Sheet.Column("Float", "fv", Float.class)
+                , new Sheet.Column("Double", "dv", Double.class)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor8() throws IOException {
+        new Workbook("test list map sheet Constructor8")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet("MAP", createTestData(10)
+                , new Sheet.Column("ID", "id", int.class)
+                , new Sheet.Column("NAME", "name", String.class)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor9() throws IOException {
+        new Workbook("test list map sheet Constructor9")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet(createTestData(10)
+                , new Sheet.Column("ID", "id")
+                , new Sheet.Column("NAME", "name")))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConstructor10() throws IOException {
+        new Workbook("test list map sheet Constructor10")
+            .watch(Print::println)
+            .addSheet(new ListMapSheet("MAP"
+                , createTestData(10)
+                , new Sheet.Column("ID", "id", int.class)
+                , new Sheet.Column("NAME", "name", String.class)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testArray() throws IOException {
+        Map<String, Object> data1 = new HashMap<>();
+        data1.put("id", 1);
+        data1.put("name", "abc");
+
+        Map<String, Object> data2 = new HashMap<>();
+        data2.put("id", 2);
+        data2.put("name", "xyz");
+        new Workbook()
+            .watch(Print::println)
+            .addSheet(new ListMapSheet().setData(Arrays.asList(data1, data2)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testSingleList() throws IOException {
+        Map<String, Object> data = new HashMap<>();
+        data.put("id", 1);
+        data.put("name", "abc");
+
+        new Workbook()
+            .watch(Print::println)
+            .addSheet(new ListMapSheet().setData(Collections.singletonList(data)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testNullValue() throws IOException {
+        new Workbook("test map null value")
+            .watch(Print::println)
+            .addSheet(createNullTestData(10))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/entity/csv/ListObjectSheetTest.java b/src/test/java/org/ttzero/excel/entity/csv/ListObjectSheetTest.java
new file mode 100644
index 00000000..630df370
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/csv/ListObjectSheetTest.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2019, 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.entity.csv;
+
+import org.junit.Test;
+import org.ttzero.excel.Print;
+import org.ttzero.excel.annotation.ExcelColumn;
+import org.ttzero.excel.entity.CustomizeDataSourceSheet;
+import org.ttzero.excel.entity.ExcelWriteException;
+import org.ttzero.excel.entity.ListSheet;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.Workbook;
+import org.ttzero.excel.entity.WorkbookTest;
+import org.ttzero.excel.entity.style.Fill;
+import org.ttzero.excel.entity.style.PatternType;
+import org.ttzero.excel.entity.style.Styles;
+import org.ttzero.excel.entity.ListObjectSheetTest.AllType;
+import org.ttzero.excel.entity.ListObjectSheetTest.Item;
+import org.ttzero.excel.entity.ListObjectSheetTest.Student;
+import org.ttzero.excel.entity.ListObjectSheetTest.BoxAllType;
+import org.ttzero.excel.entity.ListObjectSheetTest.ExtItem;
+
+import java.awt.Color;
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+
+import static org.ttzero.excel.entity.ListObjectSheetTest.sp;
+import static org.ttzero.excel.entity.ListObjectSheetTest.conversion;
+
+/**
+ * Create by guanquan.wang at 2019-04-28 19:17
+ */
+public class ListObjectSheetTest extends WorkbookTest{
+
+    @Test
+    public void testWrite() throws IOException {
+        new Workbook("test object")
+            .watch(Print::println)
+            .addSheet(Item.randomTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testAllTypeWrite() throws IOException {
+        new Workbook("all type object")
+            .watch(Print::println)
+            .addSheet(AllType.randomTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testAnnotation() throws IOException {
+        new Workbook("annotation object")
+            .watch(Print::println)
+            .addSheet(Student.randomTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testAnnotationAutoSize() throws IOException {
+        new Workbook("annotation object auto-size")
+            .watch(Print::println)
+            .addSheet(new ListSheet<>(Student.randomTestData()))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testAutoSize() throws IOException {
+        new Workbook("all type auto size")
+            .watch(Print::println)
+            .addSheet(AllType.randomTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testIntConversion() throws IOException {
+        new Workbook("test int conversion")
+            .watch(Print::println)
+            .addSheet(Student.randomTestData()
+                , new Sheet.Column("学号", "id")
+                , new Sheet.Column("姓名", "name")
+                , new Sheet.Column("成绩", "score", n -> n < 60 ? "不及格" : n)
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testStyleConversion() throws IOException {
+        new Workbook("object style processor")
+            .watch(Print::println)
+            .addSheet(Student.randomTestData()
+                , new Sheet.Column("学号", "id")
+                , new Sheet.Column("姓名", "name")
+                , new Sheet.Column("成绩", "score")
+                    .setStyleProcessor((o, style, sst) -> {
+                        if ((int)o < 60) {
+                            style = Styles.clearFill(style)
+                                | sst.addFill(new Fill(PatternType.solid, Color.orange));
+                        }
+                        return style;
+                    })
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testConvertAndStyleConversion() throws IOException {
+        new Workbook("object style and style processor")
+            .watch(Print::println)
+            .addSheet(Student.randomTestData()
+                , new Sheet.Column("学号", "id")
+                , new Sheet.Column("姓名", "name")
+                , new Sheet.Column("成绩", "score", n -> n < 60 ? "不及格" : n)
+                    .setStyleProcessor((o, style, sst) -> {
+                        if ((int)o < 60) {
+                            style = Styles.clearFill(style)
+                                | sst.addFill(new Fill(PatternType.solid, new Color(246, 209, 139)));
+                        }
+                        return style;
+                    })
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testCustomizeDataSource() throws IOException {
+        new Workbook("customize datasource")
+            .watch(Print::println)
+            .addSheet(new CustomizeDataSourceSheet())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testBoxAllTypeWrite() throws IOException {
+        new Workbook("box all type object")
+            .watch(Print::println)
+            .addSheet(BoxAllType.randomTestData())
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testArray() throws IOException {
+        new Workbook()
+            .watch(Print::println)
+            .addSheet(new ListSheet<>()
+                .setData(Arrays.asList(new Item(1, "abc"), new Item(2, "xyz"))))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testSingleList() throws IOException {
+        new Workbook()
+            .watch(Print::println)
+            .addSheet(new ListSheet<>()
+                .setData(Collections.singletonList(new Item(1, "a b c"))))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test
+    public void testStyleConversion1() throws IOException {
+        new Workbook("object style processor1", "guanquan.wang")
+            .addSheet(new ListSheet<>("期末成绩", Student.randomTestData()
+                    , new Sheet.Column("学号", "id")
+                    , new Sheet.Column("姓名", "name")
+                    , new Sheet.Column("成绩", "score", conversion)
+                    .setStyleProcessor(sp)
+                )
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testNullValue() throws IOException {
+        new Workbook("test null value")
+            .watch(Print::println)
+            .addSheet(new ListSheet<>("EXT-ITEM", ExtItem.randomTestData(10)))
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testFieldUnDeclare() throws IOException {
+        try {
+            new Workbook("field un-declare")
+                .addSheet(new ListSheet<>("期末成绩", Student.randomTestData()
+                        , new Sheet.Column("学号", "id")
+                        , new Sheet.Column("姓名", "name")
+                        , new Sheet.Column("成绩", "sore") // un-declare field
+                    )
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testResetMethod() throws IOException {
+        new Workbook("重写期末成绩")
+            .addSheet(new ListSheet<Student>("重写期末成绩", Collections.singletonList(new Student(9527, author, 0) {
+                    @Override
+                    public int getScore() {
+                        return 100;
+                    }
+                }))
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+
+    @Test public void testMethodAnnotation() throws IOException {
+        new Workbook("重写方法注解")
+            .addSheet(new ListSheet<Student>("重写方法注解", Collections.singletonList(new Student(9527, author, 0) {
+                @Override
+                @ExcelColumn("ID")
+                public int getId() {
+                    return super.getId();
+                }
+
+                @Override
+                @ExcelColumn("SCORE")
+                public int getScore() {
+                    return 97;
+                }
+            }))
+            )
+            .saveAsCSV()
+            .writeTo(getOutputTestPath());
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/entity/csv/ResultSetSheetTest.java b/src/test/java/org/ttzero/excel/entity/csv/ResultSetSheetTest.java
new file mode 100644
index 00000000..69eb9af8
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/csv/ResultSetSheetTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.entity.csv;
+
+import org.junit.Test;
+import org.ttzero.excel.Print;
+import org.ttzero.excel.entity.ExcelWriteException;
+import org.ttzero.excel.entity.ResultSetSheet;
+import org.ttzero.excel.entity.SQLWorkbookTest;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.Workbook;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Create by guanquan.wang at 2019-04-28 21:50
+ */
+public class ResultSetSheetTest extends SQLWorkbookTest {
+    @Test public void testWrite() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("result set")
+                .watch(Print::println)
+                .setConnection(con)
+                .addSheet(rs
+                    , new Sheet.Column("学号", int.class)
+                    , new Sheet.Column("性名", String.class)
+                    , new Sheet.Column("年龄", int.class)
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException|IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor1() {
+        try {
+            new Workbook("test ResultSet sheet Constructor1")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet())
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor2() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor2")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet().setRs(rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor3() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor3")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student").setRs(rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor4() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor4")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student"
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class))
+                    .setRs(rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor5() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor5")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student"
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class))
+                    .setRs(rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor6() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor6")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet(rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor7() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor7")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student", rs))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor8() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor8")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet(rs
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor9() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor9")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student", rs
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor10() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor10")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet(rs
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor11() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet sheet Constructor11")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student", rs
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testDiffTypeFromMetadata() {
+        try (
+            Connection con = getConnection();
+            PreparedStatement ps = con.prepareStatement("select id, name, age from student limit 10");
+            ResultSet rs = ps.executeQuery()
+        ) {
+            new Workbook("test ResultSet different type from metadata")
+                .watch(Print::println)
+                .addSheet(new ResultSetSheet("Student", rs
+                    , new Sheet.Column("ID", String.class)  // Integer in database
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", String.class) // // Integer in database
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+
+}
diff --git a/src/test/java/org/ttzero/excel/entity/csv/StatementSheetTest.java b/src/test/java/org/ttzero/excel/entity/csv/StatementSheetTest.java
new file mode 100644
index 00000000..96f95b8e
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/entity/csv/StatementSheetTest.java
@@ -0,0 +1,339 @@
+/*
+ * 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.entity.csv;
+
+import org.junit.Test;
+import org.ttzero.excel.Print;
+import org.ttzero.excel.entity.ExcelWriteException;
+import org.ttzero.excel.entity.SQLWorkbookTest;
+import org.ttzero.excel.entity.Sheet;
+import org.ttzero.excel.entity.StatementSheet;
+import org.ttzero.excel.entity.Workbook;
+import org.ttzero.excel.entity.style.Fill;
+import org.ttzero.excel.entity.style.PatternType;
+import org.ttzero.excel.entity.style.Styles;
+
+import java.awt.*;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * Create by guanquan.wang at 2019-04-28 22:47
+ */
+public class StatementSheetTest extends SQLWorkbookTest {
+    @Test public void testWrite() {
+        try (Connection con = getConnection()) {
+            new Workbook("statement")
+                .watch(Print::println)
+                .setConnection(con)
+                .addSheet("select id, name, age from student order by age"
+                    , new Sheet.Column("学号", int.class)
+                    , new Sheet.Column("性名", String.class)
+                    , new Sheet.Column("年龄", int.class)
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testStyleProcessor() {
+        try (Connection con = getConnection()) {
+            new Workbook("statement style processor")
+                .watch(Print::println)
+                .setConnection(con)
+                .addSheet("select id, name, age from student"
+                    , new Sheet.Column("学号", int.class)
+                    , new Sheet.Column("性名", String.class)
+                    , new Sheet.Column("年龄", int.class)
+                        .setStyleProcessor((o, style, sst) -> {
+                            int n = (int) o;
+                            if (n < 10) {
+                                style = Styles.clearFill(style)
+                                    | sst.addFill(new Fill(PatternType.solid, Color.orange));
+                            }
+                            return style;
+                        })
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testIntConversion() {
+        try (Connection con = getConnection()) {
+            new Workbook("test int conversion statement")
+                .setConnection(con)
+                .watch(Print::println)
+                .addSheet("select id, name, age from student"
+                    , new Sheet.Column("学号", int.class)
+                    , new Sheet.Column("姓名", String.class)
+                    , new Sheet.Column("年龄", int.class, n -> n > 14 ? "高龄" : n)
+                        .setStyleProcessor((o, style, sst) -> {
+                            int n = (int) o;
+                            if (n > 14) {
+                                style = Styles.clearFill(style)
+                                    | sst.addFill(new Fill(PatternType.solid, Color.orange));
+                            }
+                            return style;
+                        })
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor1() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor1")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student limit 10"))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor2() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor2")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student", con, "select id, name, age from student limit 10"))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor3() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor3")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student where id between ? and ?", ps -> {
+                    ps.setInt(1, 10);
+                    ps.setInt(2, 20);
+                }))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor4() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor4")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student", con, "select id, name, age from student where id between ? and ?", ps -> {
+                    ps.setInt(1, 10);
+                    ps.setInt(2, 20);
+                }))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor5() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor5")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student limit 10"
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor6() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor6")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student", con, "select id, name, age from student limit 10"
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor7() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor7")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student where id between ? and ?"
+                    , ps -> {
+                        ps.setInt(1, 10);
+                        ps.setInt(2, 20);
+                    }
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor8() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor8")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student", con, "select id, name, age from student where id between ? and ?"
+                    , ps -> {
+                        ps.setInt(1, 10);
+                        ps.setInt(2, 20);
+                    }
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class)))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor9() {
+        try {
+            new Workbook("test statement sheet Constructor9")
+                .watch(Print::println)
+                .addSheet(new StatementSheet())
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (ExcelWriteException e) {
+            assert true;
+        }
+    }
+
+    @Test public void testConstructor10() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor10")
+                .watch(Print::println)
+                .addSheet(new StatementSheet()
+                    .setPs(con.prepareStatement("select id, name, age from student limit 10")))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor11() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor11")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student")
+                    .setPs(con.prepareStatement("select id, name, age from student limit 10")))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor12() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor12")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student")
+                    .setPs(con.prepareStatement("select id, name, age from student limit 10")))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testConstructor13() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet Constructor13")
+                .watch(Print::println)
+                .addSheet(new StatementSheet("Student"
+                    , new Sheet.Column("ID", int.class)
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", int.class))
+                    .setPs(con.prepareStatement("select id, name, age from student limit 10")))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException | IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    @Test public void testCancelOddStyle() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement sheet cancel odd")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student limit 10")
+                    .cancelOddStyle()
+                )
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testDiffTypeFromMetadata() {
+        try (Connection con = getConnection()) {
+            new Workbook("test Statement different type from metadata")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student limit 10"
+                    , new Sheet.Column("ID", String.class)  // Integer in database
+                    , new Sheet.Column("NAME", String.class)
+                    , new Sheet.Column("AGE", String.class) // Integer in database
+                ))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test public void testFixWidth() {
+        try (Connection con = getConnection()) {
+            new Workbook("test statement fix width")
+                .watch(Print::println)
+                .addSheet(new StatementSheet(con, "select id, name, age from student limit 10").fixSize(10))
+                .saveAsCSV()
+                .writeTo(getOutputTestPath());
+        } catch (SQLException |IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/test/java/org/ttzero/excel/util/CSVUtilTest.java b/src/test/java/org/ttzero/excel/util/CSVUtilTest.java
new file mode 100644
index 00000000..5f53a3c0
--- /dev/null
+++ b/src/test/java/org/ttzero/excel/util/CSVUtilTest.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (c) 2019, 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.util;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+import static org.ttzero.excel.util.FileUtil.isWindows;
+
+/**
+ * Create by guanquan.wang at 2019-02-14 17:02
+ */
+public class CSVUtilTest {
+    private Path path;
+    private Random random;
+    private final char[] base = "abcdefghijklmnopqrstuvwxyz,ABCDEFGHIJKLMNOPQRSTUVWXYZ\t0123456789\"".toCharArray();
+    private char[][] cache_char_array = new char[25][];
+
+
+    @Before public void before() {
+        for (int i = 0; i < cache_char_array.length; i++) {
+            cache_char_array[i] = new char[i + 1];
+        }
+        random = new Random();
+        URL url = CSVUtilTest.class.getClassLoader().getResource(".");
+        if (url == null) {
+            throw new RuntimeException("Load test resources error.");
+        }
+        path = isWindows() ? Paths.get(url.getFile().substring(1)) : Paths.get(url.getFile());
+        path = path.resolve("1.csv");
+
+        // Create a test file
+        if (!Files.exists(path)) {
+            testWriter();
+        }
+    }
+
+    @Test public void testReader() {
+        try {
+            List<String[]> rows = CSVUtil.read(path);
+            rows.forEach(t -> System.out.println(Arrays.toString(t)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testStream() {
+        try (CSVUtil.Reader reader = CSVUtil.newReader(path)) {
+            reader.stream()
+                .forEach(t -> System.out.println(Arrays.toString(t)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testStreamShare() {
+        try (CSVUtil.Reader reader = CSVUtil.newReader(path)) {
+            reader.sharedStream()
+                .forEach(t -> System.out.println(Arrays.toString(t)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteBoolean() {
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(true);
+            writer.write(false);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == 2;
+            assert String.valueOf(true).toUpperCase().equals(strings.get(0)[0]);
+            assert String.valueOf(false).toUpperCase().equals(strings.get(0)[1]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteChar() {
+        char c = base[random.nextInt(base.length)];
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.writeChar(c);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path, ',');
+            assert strings.size() == 1;
+            assert strings.get(0).length == 1;
+            // will be trim
+            if (c == '\n' || c == '\t') {
+                assert strings.get(0)[0].isEmpty();
+            } else {
+                assert String.valueOf(c).equals(strings.get(0)[0]);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteInt() {
+        int n1 = random.nextInt();
+        int n2 = random.nextInt(1024);
+        int zero = 0;
+        int min = Integer.MIN_VALUE;
+        int max = Integer.MAX_VALUE;
+        int min1 = min + 1;
+        int max1 = max - 1;
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(n1);
+            writer.write(n2);
+            writer.write(zero);
+            writer.write(min);
+            writer.write(max);
+            writer.write(min1);
+            writer.write(max1);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == 7;
+            assert String.valueOf(n1).equals(strings.get(0)[0]);
+            assert String.valueOf(n2).equals(strings.get(0)[1]);
+            assert String.valueOf(zero).equals(strings.get(0)[2]);
+            assert String.valueOf(min).equals(strings.get(0)[3]);
+            assert String.valueOf(max).equals(strings.get(0)[4]);
+            assert String.valueOf(min1).equals(strings.get(0)[5]);
+            assert String.valueOf(max1).equals(strings.get(0)[6]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteLong() {
+        long l1 = random.nextLong();
+        long l2 = random.nextLong();
+        long l3 = random.nextLong();
+        long l4 = random.nextLong();
+        long min = Long.MIN_VALUE;
+        long max = Long.MAX_VALUE;
+        long min1 = min + 1;
+        long max1 = max - 1;
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(l1);
+            writer.write(l2);
+            writer.newLine(); // new line
+            writer.write(l3);
+            writer.write(l4);
+            writer.newLine(); // new line
+            writer.write(min);
+            writer.write(max);
+            writer.newLine(); // new line
+            writer.write(min1);
+            writer.write(max1);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 4;
+            assert strings.get(0).length == 2;
+            assert strings.get(1).length == 2;
+            assert strings.get(2).length == 2;
+            assert strings.get(3).length == 2;
+            assert String.valueOf(l1).equals(strings.get(0)[0]);
+            assert String.valueOf(l2).equals(strings.get(0)[1]);
+            assert String.valueOf(l3).equals(strings.get(1)[0]);
+            assert String.valueOf(l4).equals(strings.get(1)[1]);
+            assert String.valueOf(min).equals(strings.get(2)[0]);
+            assert String.valueOf(max).equals(strings.get(2)[1]);
+            assert String.valueOf(min1).equals(strings.get(3)[0]);
+            assert String.valueOf(max1).equals(strings.get(3)[1]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+    }
+
+    @Test public void testWriteFloat() {
+        float f1 = random.nextFloat();
+        float f2 = random.nextFloat();
+        float f3 = random.nextFloat();
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(f1);
+            writer.newLine(); // new line
+            writer.write(f2);
+            writer.newLine(); // new line
+            writer.write(f3);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 3;
+            assert strings.get(0).length == 1;
+            assert strings.get(1).length == 1;
+            assert strings.get(2).length == 1;
+            assert String.valueOf(f1).equals(strings.get(0)[0]);
+            assert String.valueOf(f2).equals(strings.get(1)[0]);
+            assert String.valueOf(f3).equals(strings.get(2)[0]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteDouble() {
+        double d1 = random.nextDouble();
+        double d2 = random.nextDouble();
+        double d3 = random.nextDouble();
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(d1);
+            writer.write(d2);
+            writer.write(d3);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == 3;
+            assert String.valueOf(d1).equals(strings.get(0)[0]);
+            assert String.valueOf(d2).equals(strings.get(0)[1]);
+            assert String.valueOf(d3).equals(strings.get(0)[2]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriteString() {
+        int n = random.nextInt(10) + 1;
+        String[] src = new String[n];
+        for (int i = 0; i < n; i++) {
+            src[i] = randomString();
+        }
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            for (String s : src) {
+                writer.write(s);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == n;
+
+            assert Arrays.toString(src).equals(Arrays.toString(strings.get(0)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testEndWithLF() {
+        int n = random.nextInt(10) + 1;
+        String[] src = new String[n];
+        for (int i = 0; i < n; i++) {
+            src[i] = randomString();
+        }
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            for (String s : src) {
+                writer.write(s);
+            }
+            writer.newLine(); // end with LF
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == n;
+            assert Arrays.toString(src).equals(Arrays.toString(strings.get(0)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testLineEndWithComma() {
+        int n = random.nextInt(10) + 1;
+        String[] src = new String[n];
+        for (int i = 0; i < n; i++) {
+            src[i] = randomString();
+        }
+        char comma = ',';
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path, comma)) {
+            for (String s : src) {
+                writer.write(s);
+                writer.writeEmpty(); // comma is the last character
+                writer.newLine(); // new line
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == n;
+            for (int i = 0; i < n; i++) {
+                assert strings.get(i).length == 2;
+                assert src[i].equals(strings.get(i)[0]);
+                assert "".equals(strings.get(i)[1]);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testLF() {
+        String s1 = "abc,12\njiuh"; // LF
+        String s2 = "42\r\n843432545\"fjid";  // CR LF
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(123);
+            writer.write("中文");
+            writer.write(s1);
+            writer.write(s2);
+            writer.write(true);
+            writer.write("fjdskfhinsfainwepcfjskldafdnjsh fdslaf jdsk djsk djska hdsuiafgsdkafhdsuiafhsuiafusfhdsa");
+            writer.write("fsa");
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            assert strings.get(0).length == 7;
+            assert s1.equals(strings.get(0)[2]);
+            assert s2.equals(strings.get(0)[3]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testGBKCharset() {
+        String s1 = "打造中国千亿级MRO供应链平台";
+        String s2 = "让工业用品采购更简单";
+
+        Charset GBK = Charset.forName("GBK");
+
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path, GBK)) {
+            writer.write(s1);
+            writer.write(s2);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        // Use UTF-8
+        try {
+            CSVUtil.read(path);
+        } catch (IOException e) { // A charset.MalformedInputException occur
+            assert true;
+        }
+
+        // Use GBK
+        try {
+            List<String[]> strings = CSVUtil.read(path, GBK);
+            assert strings.size() == 1;
+            assert strings.get(0).length == 2;
+            assert s1.equals(strings.get(0)[0]);
+            assert s2.equals(strings.get(0)[1]);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+
+        // Reset charset
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            writer.write(s1);
+            writer.write(s2);
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testEuropeanComma() {
+        // A comma and the value separator is a semicolon
+        char comma = ';';
+        int n = random.nextInt(10) + 1;
+        String[] src = new String[n];
+        for (int i = 0; i < n; i++) {
+            src[i] = randomString();
+        }
+        System.out.println(Arrays.toString(src));
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path, comma)) {
+            for (String s : src) {
+                writer.write(s);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path, comma);
+            assert strings.size() == 1;
+            System.out.println(Arrays.toString(strings.get(0)));
+            assert Arrays.toString(src).equals(Arrays.toString(strings.get(0)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+
+        try {
+            List<String[]> strings = CSVUtil.read(path);
+            assert strings.size() == 1;
+            System.out.println(Arrays.toString(strings.get(0)));
+            assert Arrays.toString(src).equals(Arrays.toString(strings.get(0)));
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    @Test public void testWriter() {
+        int column = random.nextInt(10) + 1, row = random.nextInt(100) + 1;
+
+        // storage column type
+        // 0 string
+        // 1 char
+        // 2 int
+        // 3 float
+        // 4 double
+        int[] types = new int[column];
+        for (int i = 0; i < column; i++) {
+            types[i] = random.nextInt(5);
+        }
+
+        try (CSVUtil.Writer writer = CSVUtil.newWriter(path)) {
+            for (int i = 0; i < row; i++) {
+                for (int c = 0; c < column; c++) {
+                    switch (types[c]) {
+                        case 0:
+                            writer.write(randomString());
+                            break;
+                        case 1:
+                            writer.write(base[random.nextInt(base.length)]);
+                            break;
+                        case 2:
+                            writer.write(random.nextInt());
+                            break;
+                        case 3:
+                            writer.write(random.nextFloat());
+                            break;
+                        case 4:
+                            writer.write(random.nextDouble());
+                            break;
+                    }
+                }
+                // break row
+                writer.newLine();
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+            assert false;
+        }
+    }
+
+    private String randomString() {
+        int len = random.nextInt(cache_char_array.length - 1);
+        if (len < 5) len = 5;
+        char[] cache = cache_char_array[len - 1];
+        for (int j = 0; j < len; j++) {
+            cache[j] = base[random.nextInt(base.length)];
+        }
+        return new String(cache);
+    }
+}