Java

JavaのYAMLライブラリのSnakeYamlを使う

投稿日:

最近はプロパティファイルを書くときに人間が読みやすくて記述量が少ないという理由でYAMLをよく使っています。
インデントが違うとエラーになりますが、その規約のお陰で記述量の少なさと読みやすさにつながっていると思います。
JavaでYAMLを読み書きするためのライブラリはSpringBootでも使われているSnakeYamlが有名だと思います。
SpringBootは特に何もしなくてもプロパティからJavaObjectを作ってくれるのでSnakeYamlを意識することはあんまりないのですが、今回素で触る機会があったので色々触ってみました。
ソースコードはGitHubをご参照ください。
beeete2/snakeyaml-examples

今回は以下のようなファイルで色々試してみたいと思います。

name: server1
hosts:
- hostid: '1'
  name: host1
  items:
  - itemid: '1'
    key: item1
    startDate: 2017-05-01
    endDate: 2017-05-31
  - itemid: '2'
    key: item2
    startDate: 2017-05-01
    endDate: 2017-05-31

yamlファイルをロード

yamlファイルをロードするときは非常に簡単です。
キーと値のペアのMapに変換されます。リスト型はListに変換されます。
また日付はDateオブジェクトに変換されるようです。

        String text = readDefaultText();

        Yaml yaml = new Yaml();
        Map map = (Map) yaml.load(text);

        softly.assertThat(getString(map, "name")).isEqualTo("server1");

        List hosts = (List) map.get("hosts");
        softly.assertThat(hosts).hasSize(1);

        Map host = (Map) hosts.get(0);
        softly.assertThat(getString(host, "name")).isEqualTo("host1");

        List items = (List) host.get("items");
        softly.assertThat(items).hasSize(2);
        System.out.println(items);

        Map item = (Map) items.get(0);
        softly.assertThat(getString(item, "key")).isEqualTo("item1");
        softly.assertThat(item.get("startDate")).isInstanceOf(Date.class); // 日付はDateオブジェクトになる

yamlファイルをロードし任意の型にマッピングする

多分、多くの使い方がこの使い方だと思います。
任意の型にマッピングすることができます。
このときに日付をLocalDateTime型にマッピングしたいと思います。

任意の型をマッピングするときはConstructorを継承したクラスを作成してYamlインスタンス生成時のコンストラクタに指定します。

    // LocalDateTimePropertyConstructor
    class LocalDateTimePropertyConstructor extends Constructor {
        public LocalDateTimePropertyConstructor() {
            yamlClassConstructors.put(NodeId.scalar, new TimeStampConstruct());
        }

        class TimeStampConstruct extends Constructor.ConstructScalar {
            @Override
            public Object construct(Node node) {
                if (node.getTag().equals(Tag.TIMESTAMP)) {
                    Construct dateConstructor = yamlConstructors.get(Tag.TIMESTAMP);
                    Date date = (Date) dateConstructor.construct(node);
                    LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.of("UTC"));
                    return localDateTime;
                } else {
                    return super.construct(node);
                }
            }
        }
    }

    // Yamlインスタンスの生成
    Yaml yaml = new Yaml(new LocalDateTimePropertyConstructor());

以下のようにマッピングできます。

        String text = readDefaultText();

        Server server = main.loadAs(text, Server.class);
        softly.assertThat(server.name).isEqualTo("server1");
        softly.assertThat(server.hosts).hasSize(1);

        Host host = server.hosts.get(0);
        softly.assertThat(host.name).isEqualTo("host1");
        softly.assertThat(host.items).hasSize(2);

        Item item = host.items.get(0);
        softly.assertThat(dateFormat(item.startDate)).isEqualTo("2017-05-01T00:00:00"); // LocalDateTimeになる
        softly.assertThat(dateFormat(item.endDate)).isEqualTo("2017-05-31T00:00:00");// LocalDateTimeになる

もちろんオブジェクトからyamlファイルへダンプすることもできます。

ダンプ

        Server server = createServer();
        Yaml yaml = new Yaml();
        String text = yaml.dump(server);
        softly.assertThat(text).isNotNull();

こうなります。

!!b3.snakeyaml.entities.Server
hosts:
- hostid: '1'
  items:
  - endDate: {}
    itemid: '1'
    key: item1
    startDate: {}
  - endDate: {}
    itemid: '2'
    key: item2
    startDate: {}
  name: host1
name: server1

LocalDateTimeの値が変換できていないようです。
対象のオブジェクト(クラス)の完全修飾名が表示されています。
あと、これは好みかもしれませんが、プロパティにインデントがないのが少し気になります。
できれば、こうしたい。

hosts:
  - hostid: '1'
    items:
      - endDate: 2016-05-31
        itemid: '1'
        key: item1
        startDate: 2016-05-01
      - endDate: 2016-05-31
        itemid: '2'
        key: item2
        startDate: 2016-05-01
    name: host1
name: server1

書式と任意の型を変換する

まず書式(フォーマット)はDumperOptionsのインスタンスを生成し好みに設定します。
色々設定できるみたいなのですが、インデントとスタイルの設定は以下のようにしました。

        DumperOptions options = new DumperOptions();
        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // ブロックスタイルにする
        options.setIndicatorIndent(2); // プロパティのインデントを2に設定する
        options.setIndent(4); // インデントを4に設定する

任意の型の変換はRepresenterを使って設定します。
LocalDateTimeを変換する例は以下のようになります。

    class LocalDateTimeRepresenter extends Representer {

        public LocalDateTimeRepresenter() {
            multiRepresenters.put(LocalDateTime.class, new RepresentLocalDateTime());
        }

        private class RepresentLocalDateTime extends RepresentDate {
            public Node representData(Object data) {
                LocalDateTime localDateTime = (LocalDateTime) data;

                String date = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
                return representScalar(getTag(data.getClass(), Tag.TIMESTAMP), date, null);
            }
        }
    }

以下のようになります。

        // LocalDateTimeの変換設定
        Representer representer = new LocalDateTimeRepresenter();
        representer.addClassTag(Server.class, Tag.MAP); // 完全修飾名を表示しない

        // 書式の設定
        DumperOptions options = new DumperOptions();
        options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        options.setIndicatorIndent(2);
        options.setIndent(8);

        Yaml yaml = new Yaml(representer, options);
        return yaml.dump(object);

実行すると以下のようになります。

hosts:
  - hostid: '1'
    items:
      - endDate: 2016-05-31
        itemid: '1'
        key: item1
        startDate: 2016-05-01
      - endDate: 2016-05-31
        itemid: '2'
        key: item2
        startDate: 2016-05-01
    name: host1
name: server1

まとめ

大体望むようなyamlファイルを生成することができました。
あとはフィールドの並び順の調整が出来ればよかったのですが、そこまでは対応しませんでした。
ちなみに並び順はJavaBeansのアルファベット順になっているみたいです。

define order for JavaBean properties. Order may be very informative: for instance ID or name are normally expected to be the first. (Currently it is alphabetically sorted by default.)

Dumping a custom YAML document

-Java

執筆者:


comment

メールアドレスが公開されることはありません。

関連記事

Spockを使いたいからEclipseからIntelliJ IDEAへ移行した話

Javaを書くときはずーとEclipseを使っていましたが最近IntelliJ IDEAへ移行しました。 確かEclipse2のときから使っていたので10年以上は使っていたと思います。 Eclipse …

no image

Windows7でSeleniumServer(RC)を使ってFireFoxを起動する方法

ちょうどWEBアプリを作っているので複雑な画面をSeleniumでテストケース作っておけばいいかなーと思って作りました。 FireFoxDriverを起動しています。 Jenkinsでテストとテストサ …

Spring MVCでHelloWorld EclipseのTomcat上で起動する

前回はMaven2で構成管理しつつEclipseの設定を行いました。 今回は、いよいよSpringMVCのアプリケーションを動かしてみたいと思います。 手順は ・サンプルプログラムの作成 ・Tomca …

JUnitのアサーションにAssertJを使う

Junit3時代はassertEqualsでアサーションしてJunit4になると当初はhamcrestでアサーションしておりましたが最近はAssertJを使っております。 AssertJはFluent …

JenkinsでwarファイルをTomcatにデプロイする方法

Tomcatサーバーへのデプロイは手動で行ってきました。 Antでリリースパッケージを作成して、SCP or FTPでリリース。原始的な方法です。 ですがJenkinsにTomcatのデプロイ用のプラ …