夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

YamlとJavaのエンティティをマッピング出来る?

こんにちは。

前回のUbuntuパッケージは・・・とりあえず、dh_makeとdebuildの方式だと
さっぱりうまくいかなかったので方針を見直し中です。

上手くいったら経緯も含めてまた投稿しますね。

そんなわけで、今回はStormで使用されている「Yaml」というファイル形式の話題。
Web上調べた際に実際のコードまで日本語で書いている例が見つからなかったので自分用のまとめとして書いておきます。

Stormではクラスタの設定、アプリケーションの設定に「SnakeYaml」というライブラリを介して
Yaml」という形式のファイルを使用しています。

1.設定ファイルって皆さんどんな形式使っていますか?

何かソフトウェアを作る際には必ず設定というものが必要となります。
XML、プロパティファイル、CSVファイルなどファイルの形式案としては色々ありますが、
個人的な感想としては下記のように各々の形式に性質があります。

  • XMLファイル

階層化した設定を記述しやすい。
ただし、設定ファイルの記述量はタグがある関係上多くなりがち。

  • プロパティファイル

キー:バリューで定義可能な設定については非常にシンプルでわかりやすい。
ただし、階層化した設定については記述方式に悩む。

  • CSVファイル

決まった形式の設定をリストのように列挙する分には非常に使いやすい。
ただし、複数の構造の設定を1ファイルの中に混在させることは困難。

・・・と、いくつか特徴があるわけです。

ただ、下記のような多段階層を持つような設定値を定義する際にはXMLでないと書きにくく、
でもXMLで記述すると記述量が多くなり、かつパース処理も自前で一部作りこむ必要がありました。

ユーザ情報
 - 住所情報
   - 郵便番号
   - 都道府県
   - 市町村
   - それ以降住所
 - 嗜好情報
   - よく読むニュースのジャンル(A)
     - Aで検索するキーワード
・・・

2.Yamlファイルを使うとXMLよりシンプルに設定が書ける?

・・・とまぁ、端的に言うとXMLファイルで俺俺構造設定ファイルを記述してあまりにも書きにくいという失敗をしたわけなんですよね^^;

そんなわけで、Stormで使っているYamlファイルを使えばもっと楽に設定ファイルがかけるのではないか、
と考え、Yamlとは何かを確認してみました。

Wikipediaを見てみるとYamlは下記のように記述されています。

YAML(ヤムル)とは、構造化データやオブジェクトを文字列にシリアライズ(直列化)するためのデータ形式の一種。
テキストのため可読である。その概念はXMLプログラミング言語であるC、PythonPerlからきている。
YAMLの原案はClark Evans、Brian Ingerson、Oren Ben-Kikが共同で出した。
YAML再帰的に定義された頭字語であり"YAML Ain't a Markup Language"(YAMLマークアップ言語ではない)の意味である。
初期には"Yet Another Markup Language"(もうひとつ別のマークアップ言語)の意味と言われていたが、
マークアップよりもデータ重視を目的としていたために後付されてできた名前である。
しかしながら XML(本当のマークアップ言語)がデータシリアライズ目的のために頻繁に使用されるため、
YAMLを軽量マークアップ言語と考えることもできる。類似の規格としてJSONがある。

実際の設定ファイルの形式として、下記のハッシュとリストを多階層で定義できるようです。

  • ハッシュ
 --- # ブロック
 name: John Smith
 age: 33
 --- # インライン
 {name: John Smith, age: 33}
  • リスト
 --- # お好みの映画、ブロック形式
 - Casablanca
 - Spellbound
 - Notorious
 --- # 買い物リスト、インライン形式、またはフロー形式
 [milk, bread, eggs]

インラインで記述する場合Wikiにもあった通りJSONと非常によく似ていますが、
インラインで無い形式であれば、シンプルな記述と、読みやすさを両立した設定ファイルになるのではなるように見えますね。

3.エンティティと設定ファイルの相互変換は出来る?

上記のような経緯でYamlファイルをとりあえず使ってみようと思い立ったのですが、
設定値というのは必然的に何かのエンティティにマッピングされて使用されます。

そのため、エンティティと設定ファイルを相互変換できるAPIの存在が重要です。
今回は相互変換のAPIが実際どういうことができるか、Stormで使用しているSnakeYamlを用いて試してみました。

とりあえず、望みの内容が出来そうなAPIとして、Yaml#dumpAs()メソッドと、Yaml#loadAs()メソッドがあります。
そのため、そのメソッドを使って何ができるかを試してみます。

4.実際に試してみる

では、実際に試してみます。
尚、今回のソースについてはGitHubにアップされていますので、もし興味がある方いたらどうぞ。

まずは、エンティティを定義します。
今回は親エンティティとなるSessionInfoと、子エンティティとなるConnectionInfoを定義しました。
・・・内容については仮なので突っ込みはご遠慮ください^^;
構造を下記に示します。

■SessionInfo

public class SessionInfo
{
    /** ユーザID */
    private String                      userId;

    /** 関連情報リスト*/
    private List<String>                relationList;

    /** 関連情報マップ*/
    private Map<String, String>         relationMap;

    /** コネクション情報リスト*/
    private List<ConnectionInfo>        connectionList;

    /** コネクション情報マップ*/
    private Map<String, ConnectionInfo> connectionMap;
}

■ConnectionInfo

public class ConnectionInfo
{
    /** ID */
    private String id;

    /** ホスト名*/
    private String name;

    /** 使用ポート */
    private int    port;
}

これらのエンティティを用いて下記のようなdump/loadの処理を書いてみました。

/**
 * Yamlクラスを用いたエンティティのダンプ&復元テスト<br/>
 * 1階層のパターン、2階層のパターンを検証する。
 * 
 * @param args プログラム起動引数(未使用)
 */
public static void main(String[] args)
{
    ConnectionInfo connectionInfo1 = new ConnectionInfo();
    connectionInfo1.setId("TestId1");
    connectionInfo1.setName("TestName1");
    connectionInfo1.setPort(10);

    Yaml yaml = new Yaml();

    // 1階層のダンプテスト
    String singleDumpResult = yaml.dumpAs(connectionInfo1, Tag.MAP, FlowStyle.BLOCK);
    System.out.println(singleDumpResult);
    System.out.println("---------------------------------------");

    // 1階層の復元テスト
    ConnectionInfo recoveriedConnection = yaml.loadAs(singleDumpResult, ConnectionInfo.class);
    System.out.println(recoveriedConnection);
    System.out.println("---------------------------------------");

    ConnectionInfo connectionInfo2 = new ConnectionInfo();
    connectionInfo2.setId("TestId2");
    connectionInfo2.setName("TestName2");
    connectionInfo2.setPort(20);

    ConnectionInfo connectionInfo3 = new ConnectionInfo();
    connectionInfo3.setId("TestId3");
    connectionInfo3.setName("TestName3");
    connectionInfo3.setPort(30);

    ConnectionInfo connectionInfo4 = new ConnectionInfo();
    connectionInfo4.setId("TestId4");
    connectionInfo4.setName("TestName4");
    connectionInfo4.setPort(40);

    SessionInfo sessionInfo = new SessionInfo();

    List<ConnectionInfo> connectionInfoList = new ArrayList<ConnectionInfo>();
    connectionInfoList.add(connectionInfo1);
    connectionInfoList.add(connectionInfo2);

    Map<String, ConnectionInfo> connectionInfoMap = new TreeMap<String, ConnectionInfo>();
    connectionInfoMap.put("TestId3", connectionInfo3);
    connectionInfoMap.put("TestId4", connectionInfo4);

    List<String> relationList = new ArrayList<String>();
    relationList.add("connectionInfo1");
    relationList.add("connectionInfo2");

    Map<String, String> relationMap = new TreeMap<String, String>();
    relationMap.put("TestId3", "connectionInfo3");
    relationMap.put("TestId4", "connectionInfo4");

    sessionInfo.setUserId("TestUserId1");
    sessionInfo.setConnectionList(connectionInfoList);
    sessionInfo.setConnectionMap(connectionInfoMap);
    sessionInfo.setRelationList(relationList);
    sessionInfo.setRelationMap(relationMap);

    // 2階層のダンプテスト
    String dualDumpResult = yaml.dumpAs(sessionInfo, Tag.MAP, FlowStyle.BLOCK);
    System.out.println(dualDumpResult);
    System.out.println("---------------------------------------");

    // 2階層の復元テスト
    SessionInfo recoveriedSession = yaml.loadAs(dualDumpResult, SessionInfo.class);
    System.out.println(recoveriedSession);
    System.out.println("---------------------------------------");
}

すると、結果は下記のようになりました。

id: TestId1
name: TestName1
port: 10

---------------------------------------
ConnectionInfo[id=TestId1,name=TestName1,port=10]
---------------------------------------
connectionList:
- id: TestId1
  name: TestName1
  port: 10
- id: TestId2
  name: TestName2
  port: 20
connectionMap:
  TestId3:
    id: TestId3
    name: TestName3
    port: 30
  TestId4:
    id: TestId4
    name: TestName4
    port: 40
relationList:
- connectionInfo1
- connectionInfo2
relationMap:
  TestId3: connectionInfo3
  TestId4: connectionInfo4
userId: TestUserId1

---------------------------------------
SessionInfo[userId=TestUserId1,relationList=[connectionInfo1, connectionInfo2],relationMap={TestId3=connectionInfo3, TestId4=connectionInfo4},connectionList=[ConnectionInfo[id=TestId1,name=TestName1,port=10], ConnectionInfo[id=TestId2,name=TestName2,port=20]],connectionMap={TestId3=ConnectionInfo[id=TestId3,name=TestName3,port=30], TestId4=ConnectionInfo[id=TestId4,name=TestName4,port=40]}]
---------------------------------------

フィールドの名称を用いてYamlファイル⇔エンティティの変換が出来ていることが確認できました。
理想を言えば各フィールドにアノテーションでキー名を記述してファイルとマッピングしたいですが、
さすがにそういった機能はないようです。

5.何が嬉しいの?

記述しやすいYaml形式のファイルで、Yamlファイル⇔エンティティの変換が出来ることが確認できました。
そのため、構造化したデータをシンプルに記述でき、それをエンティティと即マッピング出来ることができるようになったということですね。