基於 java 註解的 csv 讀寫框架更加簡單靈活

CSV

基於 java 註解的 csv 讀寫框架。

相關框架

Apache commons-csv

super-csv

簡單看了下,這兩個框架提供的特性都非常的基礎。

創作原由

以前覺得 csv 文件的讀寫非常簡單,就懶得封裝。

最近一個月寫了兩次 csv 文件相關的東西,發現要處理的細節還是有的,還浪費比較多的時間。

比如:

  1. UTF-8 中文編碼使用 excel 打開亂碼,因爲缺少 BOM 頭。

  2. 不同類型字段轉化爲字符串,順序的指定,head 頭的指定,如果手寫都會很繁瑣。

  3. 讀取的時候最後 , 後無元素,split 會缺失等。

爲了解決上述問題,此框架應運而生。

特性

  • Fluent 流式寫法

  • 基於 java 註解,支持自定義的轉換和靈活配置

  • 內置 8 大基本類型以及 String 類型轉換

  • 解決 Excel 直接打開,utf-8 亂碼問題

  • 支持集合、數組、Map 的存取

  • 支持對象中內嵌其他對象

  • 支持特殊字符轉義

變更日誌

CHANGE_LOG.md

開源地址

csv

快速開始

環境

jdk7+

maven 3.x

maven 引入

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>csv</artifactId>
    <version>0.0.6</version>
</dependency>

示例代碼

  • User.java

演示基本類型的轉換

public class User {

    private String name;

    private int age;

    private float score;

    private double money;

    private boolean sex;

    private short level;

    private long id;

    private char status;

    private byte coin;

    //Getter & Setter & toString()
}
  • 對象列表構建
    /**
     * 構建通用測試列表
     * @return 列表
     */
    private List<User> buildCommonList() {
        User user = new User();
        short s = 4;
        byte b = 1;
        user.age(10)
        .name("你好")
        .id(1L)
        .score(60)
        .coin(b)
        .level(s)
        .money(200)
        .sex(true)
        .status('Y');
        return Arrays.asList(user);
    }

寫入

  • 測試代碼
public void commonTest() {
    final String path = "src\\test\\resources\\common.csv";
    CsvWriteBs.newInstance(path)
            .write(buildCommonList());
}
  • 文件生成
name,age,score,money,sex,level,id,status,coin
你好,10,60.0,200.0,true,4,1,Y,1

讀取

public void commonTest() {
    final String path = "src\\test\\resources\\common.csv";
    List<User> userList = CsvReadBs.newInstance(path)
            .read(User.class);
    System.out.println(userList);
}
  • 日誌信息
[User{name='你好', age=10, score=60.0, money=200.0, sex=true, level=4, id=1, status=Y, coin=1}]

引導類

爲什麼需要引導類

爲了靈活的配置和默認配置並存,使用工具類會大大降低靈活性。

爲了用戶使用的便利性,和後期拓展的靈活性。

引導類

CSV 有兩個引導類:

名稱 作用
CsvWriteBs csv 文件寫入引導類
CsvReadBs csv 文件讀取引導類

CsvWriteBs

方法 默認值 說明
newInstance(final String path) 必填 創建實例,並且指定待寫入文件路徑。
path (final String path) 配置文件路徑,只有重新指定 path 路徑時需要調用。
writeHead(boolean writeBom) true 是否寫入 head 頭,如果想指定名稱,可以結合註解。只有無 head 信息時,會寫入。
writeBom(boolean writeBom) true 是否寫入 UTF8 BOM 頭,只有文件爲空時纔會寫入。
charset(String charset) UTF-8 指定文件編碼
sort(ISort sort) NoSort 默認不進行字段排序
write(List<T> list) 待寫入的文件列表
escape false 是否進行特殊字符的轉換

CsvReadBs

方法 默認值 說明
newInstance(final String path) 必填 創建實例,並且指定待讀取文件路徑。
path (final String path) 配置文件路徑,只有重新指定 path 路徑時需要調用。
charset(String charset) UTF-8 指定文件編碼
sort(ISort sort) NoSort 默認不進行字段排序
startIndex(int startIndex) 1 文件的第二行,默認第一行是 head
endIndex(int endIndex) 文件的最後一行
escape false 是否進行特殊字符的轉換

Csv 註解

註解屬性說明

用於待處理對象的字段上。

    /**
     * 字段顯示名稱
     * 1. 默認使用 field.name
     * @return 顯示名稱
     */
    String label() default "";

    /**
     * 讀取是否需要
     * @return 是
     */
    boolean readRequire() default true;

    /**
     * 寫入是否需要
     * @return 是
     */
    boolean writeRequire() default true;

    /**
     * 讀取轉換
     * @return 處理實現類
     */
    Class<? extends IReadConverter> readConverter() default CommonReadConverter.class;

    /**
     * 寫入轉換
     * @return 處理實現類
     */
    Class<? extends IWriteConverter> writeConverter() default StringWriteConverter.class;

屬性概覽表

屬性 默認值 說明
label 字段名稱 用於 csv 頭生成
readRequire true 是否需要從 csv 文件讀取
writeRequire true 當前字段是否需要寫入 csv 文件
readConverter CommonReadConverter 將 csv 中的字符串轉化爲當前字段類型,支持 8 大基本類型+String
writeConverter StringWriteConverter 直接調用當前字段值 toString() 方法,null 直接爲空字符串

其中 readConverter/writeConverter 支持用戶自定義

字段註解

對象定義

public class UserAnnotation {

    @Csv(label = "名稱")
    private String name;

    @Csv(label = "密碼", readRequire = false, writeRequire = false)
    private String password;

    @Csv(label = "生日", readConverter = ReadDateConvert.class, writeConverter = WriteDateConvert.class)
    private Date birthday;

    //Getter & Setter & toString()
}

ReadDateConvert/WriteDateConvert

使我們自定義的針對 Date 的轉換實現。

  • Write
public class WriteDateConvert implements IWriteConverter<Date> {

    @Override
    public String convert(Date value) {
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
        return dateFormat.format(value);
    }

}
  • ReadDateConvert
public class ReadDateConvert implements IReadConverter<Date> {

    @Override
    public Date convert(String value, Class fieldType) {
        try {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            return dateFormat.parse(value);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

}

寫入文件

public void annotationTest() {
    final String path = "src\\test\\resources\\annotation.csv";
    CsvWriteBs.newInstance(path)
            .write(buildAnnotationList());
}

其中列表構建:

/**
 * 構建基於註解的測試列表
 * @return 列表
 */
private List<UserAnnotation> buildAnnotationList() {
    UserAnnotation user = new UserAnnotation();
    user.name("你好")
            .password("123")
            .birthday(new Date());
    return Arrays.asList(user);
}
  • 生成文件內容
名稱,生日
你好,20190603

讀取文件測試

public void annotationTest() {
     final String path = "src\\test\\resources\\annotation.csv";
     List<UserAnnotation> userList = CsvReadBs.newInstance(path)
             .read(UserAnnotation.class);
     System.out.println(userList);
}
  • 日誌信息
[UserAnnotation{name='你好', password='null', birthday=Mon Jun 03 00:00:00 CST 2019}]

集合類

有時候對象中會包含數組、Map、Collection 等常見集合。

爲了存儲的便利性,默認提供集合的相關支持。

特性和普通字段保持一致,如果指定註解轉換,則以註解爲準。

使用示例

  • UserCollection.java

用於演示集合的對象

public class UserCollection {

    private String[] arrays;

    private LinkedList<String> lists;

    private Map<String, String> maps;

    private Set<String> sets;

    //Getter/Setter/toString()
}

存儲

  • 待存儲對象的構建
/**
 * 構建基於集合的測試列表
 * @return 列表
 * @since 0.0.3
 */
private List<UserCollection> buildCollectionList() {
    UserCollection user = new UserCollection();
    String[] arrays = new String[]{"a", "b", "c"};
    LinkedList<String> lists = new LinkedList<>(Arrays.asList(arrays));
    Map<String, String> maps = new HashMap<>();
    maps.put("key", "value");
    maps.put("key2", "value2");
    Set<String> sets = new HashSet<>();
    sets.add("set1");
    sets.add("set2");

    user.setLists(lists);
    user.setArrays(arrays);
    user.setMaps(maps);
    user.setSets(sets);
    return Arrays.asList(user);
}
  • 執行存儲
public void collectionTest() {
    final String path = "src\\test\\resources\\collection.csv";
    CsvWriteBs.newInstance(path)
            .write(buildCollectionList());
}
  • 存儲效果
arrays,lists,maps,sets
a|b,a|b|c,key2=value2|key=value,set1|set2

讀取

  • 測試類
public void collectionTest() {
    final String path = "src\\test\\resources\\collection.csv";
    List<UserCollection> userList = CsvReadBs.newInstance(path)
            .read(UserCollection.class);
    System.out.println(userList);
}
  • 測試日誌
[UserCollection{arrays=[a, b], lists=[a, b, c], maps={key=value, key2=value2}, sets=[set2, set1]}]

注意

爲了保證 csv 以 , 分隔的統一性。

集合使用 | 進行分隔,其中 map 的 key/value 分隔,用到了 =

在使用時要注意,不要包含上述的符號,否則會出現解析錯亂。

ps: 如果確實用到這些字符,可以見後面的特殊字符轉義功能

支持內嵌對象

有時候我們希望像使用 mongoDB 一樣,非常方便的存取 csv 的嵌套對象。

對於普通的 csv 都沒有實現這個特性,本次做了一個嘗試,支持內嵌對象的存取。

取捨

就像 csv 的簡單,需要用到符號 , 一樣。

內嵌對象爲了不破壞 csv 的規範,使用了符號 :

換言之,也就是對象內容中不能使用這個符號。

後期會針對出現的符號進行轉義,避免這種衝突。

測試案例

示例對象

  • UserEntry.java
public class UserEntry {

    /**
     * 名稱
     */
    private String name;

    /**
     * 內嵌的用戶信息
     */
    @CsvEntry
    private User user;

    //Getter/Setter/ToString
}

這裏在需要內嵌的對象上使用註解 @CsvEntry 表示需要進行內嵌的對象轉換。

  • User.java

其中 User 對象是原來使用的普通 java 對象

public class User {

    private String name;

    private int age;

    private float score;

    private double money;

    private boolean sex;

    private short level;

    private long id;

    private char status;

    private byte coin;

    //Getter/Setter/ToString
}

寫入測試

public void entryTest() {
    final String path = "src\\test\\resources\\entry.csv";
    CsvWriteBs.newInstance(path)
            .write(buildEntryList());
}
  • buildEntryList()

負責對象構建代碼,內容如下:

/**
 * 用戶明細列表
 * @return 明細列表
 * @since 0.0.5
 */
private List<UserEntry> buildEntryList() {
    UserEntry userEntry = new UserEntry();
    userEntry.name("test");
    userEntry.user(buildCommonList().get(0));
    return Collections.singletonList(userEntry);
}
  • buildCommonList()
private List<User> buildCommonList() {
    User user = new User();
    short s = 4;
    byte b = 1;
    user.age(10)
    .name("你好")
    .id(1L)
    .score(60)
    .coin(b)
    .level(s)
    .money(200)
    .sex(true)
    .status('Y');
    return Arrays.asList(user);
}

生成文件效果

name,user
test,你好:10:60.0:200.0:true:4:1:Y:1

如你所見,這裏內嵌對象的屬性使用了 : 進行分隔。

讀取測試

public void entryTest() {
    final String path = "src\\test\\resources\\entry.csv";
    List<UserEntry> userList = CsvReadBs.newInstance(path)
            .read(UserEntry.class);
    System.out.println(userList);
}

輸出信息

[UserEntry{name='test', user=User{name='你好', age=10, score=60.0, money=200.0, sex=true, level=4, id=1, status=Y, coin=1}}]

特殊字符轉義

在實際使用中,有時候我們會用到 ,|:=

這幾個被使用的特殊字符。

如果你希望這些特殊的字符被正確的存取,那麼可以使用 escape 屬性執行。

特殊字符的轉換

原始 轉義後
, &CSV_COMMA;
| &CSV_OR;
: &CSV_COLON;
= &CSV_EUQAL;

下面演示一下如何使用

暫時轉義字符不支持自定義。

測試代碼

寫入測試

public void escapeTest() {
    final String path = "src\\test\\resources\\escape.csv";
    CsvWriteBs.newInstance(path)
            .escape(true)
            .write(buildUserEscapeList());
}
  • 生成文件效果
name,map,nameList,user
one&CSV_COMMA;one,key&CSV_EUQAL;key=value&CSV_EUQAL;value,one&CSV_OR;one|two&CSV_OR;two,entry&CSV_COLON;name:0:0.0:0.0:false:0:0: :0

相關代碼

  • UserEscape.java

其中用到的對象爲:

public class UserEscape {

    /**
     * 使用 ,
     */
    private String name;

    /**
     * 使用 map =
     */
    private Map<String, String> map;

    /**
     * 使用 |
     */
    private List<String> nameList;

    /**
     * 使用 :
     */
    @CsvEntry
    private User user;

    //Getter & Setter & ToString()
}
  • buildUserEscapeList()

構建時,特意使用了特殊的字符。

private List<UserEscape> buildUserEscapeList() {
    UserEscape escape = new UserEscape();
    Map<String, String> map = new HashMap<>();
    map.put("key=key", "value=value");
    User user = new User();
    user.name("entry:name");

    escape.name("one,one");
    escape.nameList(Arrays.asList("one|one", "two|two"));
    escape.map(map);
    escape.user(user);

    return Collections.singletonList(escape);
}

讀取測試

public void escapeTest() {
    final String path = "src\\test\\resources\\escape.csv";
    List<UserEscape> userList = CsvReadBs.newInstance(path)
            .escape(true)
            .read(UserEscape.class);
    System.out.println(userList);
}
  • 日誌信息
[UserEscape{name='one,one', nameList=[one|one, two|two], user=User{name='entry:name', age=0, score=0.0, money=0.0, sex=false, level=0, id=0, status= , coin=0}, map={key=key=value=value}}]

後續設計

更豐富的類型支持

支持更多的 java 常見類型。

更靈活的配置

比如支持用戶自定義轉義字符

支持文件的寫入模式等等。

開源地址

csv

可以查看相關代碼。

爲了便於其他人閱讀和使用,代碼擁有詳細的註釋。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章