Y服務-你真的懂 Yaml 嗎

美碼師發表於2019-07-22

在Java 的世界裡,配置的事情都交給了 Properties,要追溯起來這個模組還是從古老的JDK1.0 就開始了的。

"天哪,這可是20年前的東西了,我居然還在用 Properties.."

Y服務-你真的懂 Yaml 嗎

然而,本文的主角並不是Properties,而是Yaml。這是新時代裡微服務架構上的寵兒,和 Properties 相比起來,Yaml 顯得有些弄潮兒。
以往的大多數專案裡,我們都可以發現 Properties配置檔案的蹤跡,這包括用作業務屬性配置的、機機介面互動的、國際化的等等用途。
而少量的一些情況下,也存在一些"混合式"的做法,比如:

  • 使用 Xml 來表示一些模板
  • 使用一個 Json 格式化的字串
  • 裸奔的文字格式,應用自解析
    ...

混雜的配置方式往往出現在一些充滿"壞味道"的專案裡頭,因為程式碼陳舊、斯人已矣 等原因,很難形成統一的方式。
然而,除開 Properties 屬性檔案這種簡單的配置方式之外,採用其他的方法不外乎都是為了適應配置複雜、多元化的訴求。

那麼,Yaml 就是應對這種場景而產生的,在 SpringBoot 的官方文件中,有不少篇幅是 使用了 Yaml 語法的配置格式。
下面介紹一下 Yaml 以及它是如何使用的。

一、什麼是 Yaml

來自百科的定義
"Yaml 是一個可讀性高,易用的資料序列化格式,由 Clark Evans 在2001年首次發表。"
可見 Yaml 並不是一個很新的東西,只是在以前接觸的人不多罷了。此外,Yaml也被各種程式語言及框架所支援, 通用性很高。
在Java體系中,一般的微服務框架都支援甚至優先推薦使用 Yaml 作為首選的配置語言。

而 Yaml 本身具有什麼特點? 看看下面的一個例項:

environments:
    dev:
        url: https://dev.example.com
  name: Developer Setup
    prod:
        url: https://another.example.com
        name: My Cool App

這段語法等價的 Properties 為:

environments.dev.url=https://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=https://another.example.com
environments.prod.name=My Cool App

可見, yaml 相對來說更加的結構化,更適合用來表達一個物件。
它在語法上有這樣的特點:

  • 大小寫敏感
  • 使用空格縮排表示層級關係,摒棄使用Tab鍵,這主要是考慮到不同平臺上文字展現時需要對齊
  • 縮排的空格數目不重要,只要相同層級的元素左側對齊即可
  • 使用 # 開頭作為註釋行
  • 使用 連線符(-)開頭來描述陣列元素

對比 Properties
Properties 可以很好的實現 Key-Value 的配置,包括作為一些國際化內容的配置方式。
但 Properties 很難表現多層級的巢狀關係,此時如果用 Yaml 可以較好的彌補該短板。

對比 Json
Yaml 與 Json本身沒有太多的優劣之分,兩者都是結構化的表示式語言,但是Json的設計重點在於簡單易用、方便傳輸的特性;
而 Yaml 則側重於可讀性(更加在乎外觀),幾乎可以把 Yaml 看做是 Json 的一個"超集",即可讀性更高(更漂亮) 的結構化格式。
此外,Json更加便於生成和解析,適合在各種跨語言、分散式的環境中傳輸和互動;與此同時, Yaml 則一般只是用作的配置較多。

關於 Yaml 的定義可以訪問下面的地址:
http://www.yaml.org/spec/1.2/spec.html

二、Yaml 的語法

Yaml 是非常簡單的, 它所定義的元素只有三個:

  • 物件:就是鍵值對的集合,對應於Java 中的 HashMap
  • 陣列:指一組按序排列的值,對應於Java 中的 List
  • 單值:單個的、不可再分的值,比如 3,"Jackson"

物件如何表示
一個物件的屬性、巢狀關係通過空格縮排對齊來表示,如下:

article:
    title: 一個人的自白書
    author:
        name: 陳玲
        gender: female

陣列如何表示
陣列的元素通過連線符(-)來表示,如下:

article:
    title: 一個人的自白書
    tags:
        - 傳記
        - 社會
        - 人物

構成物件、陣列內容的基本單元是單值,Yaml支援的單個值的型別有七種,如下:

型別 範例
字串 Bob
布林值 true
整數 199
浮點數 19.91
Null ~
時間 2001-12-14T22:14:09.10+08:00
日期 2019-01-09

其中,日期、時間使用的是 ISO 8601 國際標準格式,關於它的定義可以參考:
https://www.w3.org/TR/NOTE-datetime

一般情況下單個值會在一行內結束。但如果遇到多行的字串,可以使用一些特殊字元表示,
比如:

text: |
  Hello
  World

對應的結果為:

{ text: 'Hello\nWorld\n' }

可以用+表示保留字串末尾的換行,-表示刪除字串末尾的換行:

text1: |+
  Hello
  

text2: |-
  Hello

對應的結果為:

{ text1: 'Hello\n\n\n', text2: 'Hello' }

除此之外,Yaml 還可以支援引用、函式、正規表示式等高階用法,但專案上一般很少用到。

三、操作 Yaml

Y服務-你真的懂 Yaml 嗎

目前用來操作 Yaml 的常用元件是 Snake Yaml,這個庫支援標準的 Yaml 1.1 版本

SpringBoot 官方文件也介紹了整合該框架的方式,參考下面的地址:
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-loading-yaml

下面提供 將SnakeYaml 整合到專案的樣例。

A. 引入框架

在Maven的pom.xml檔案中新增:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.21</version>
</dependency>

B. 程式碼片段

實現載入配置檔案

如下面的程式碼,實現了從類路徑config.yml檔案中載入 yaml 配置內容:

InputStream inputStream = YamlUtil.class.getClassLoader()
        .getResourceAsStream("config.yml");

Yaml yaml = new Yaml();
Map<String, Object> objectMap = yaml.load(inputStream);
System.out.println(objectMap.get("path"));

實現物件轉換

定義如下的Pojo 物件:

public static class A{
    private String name = "hello";
    private List<B> bs = new ArrayList<B>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<B> getBs() {
        return bs;
    }

    public void setBs(List<B> bs) {
        this.bs = bs;
    }
}

public static class B{
    private String id = UUID.randomUUID().toString();

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }
}

通過 SnakeYaml 將物件輸出為 Yaml 格式的程式碼:

A a = new A();
a.getBs().add(new B());
a.getBs().add(new B());

Yaml yaml = new Yaml();
String aString = yaml.dumpAsMap(a);
System.out.println(aString);

輸出結果如下:

bs:
- id: b3688f05-ea7e-436b-bc9a-9c5df555c7fd
- id: 7906224d-8ecc-43b8-bc3b-07985bc18ebd
name: hello

此時如果希望將Yaml 文字反過來轉換為 A 物件,可以執行下面的程式碼:

A a1 = new Yaml().parseToObject(aString, A.class);
...

C. 完整案例

最終,我們可以將 Yaml 文件的操作封裝為一個工具類,方便在業務程式碼中整合。

YamlUtil.java

public class YamlUtil {

    /**
     * 從資原始檔載入內容,並解析為Map物件
     *
     * @param path
     * @return
     */
    public static Map<String, Object> loadToMap(String path) {
        if (StringUtils.isEmpty(path)) {
            return Collections.emptyMap();
        }

        InputStream inputStream = YamlUtil.class.getClassLoader()
                .getResourceAsStream(path);

        Yaml yaml = new Yaml();
        Map<String, Object> objectMap = yaml.load(inputStream);
        return objectMap;
    }

    /**
     * 將字串解析為Map物件
     *
     * @param content
     * @return
     */
    public static Map<String, Object> parseToMap(String content) {
        if (StringUtils.isEmpty(content)) {
            return Collections.emptyMap();
        }

        Yaml yaml = new Yaml();
        Map<String, Object> objectMap = yaml.load(content);
        return objectMap;
    }

    /**
     * 將字串解析為類物件
     *
     * @param content
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T parseToObject(String content, Class<T> clazz) {
        if (StringUtils.isEmpty(content) || clazz == null) {
            return null;
        }

        Yaml yaml = new Yaml(new Constructor(clazz));
        T object = yaml.load(content);
        return object;
    }

    /**
     * 格式化物件
     *
     * @param object
     * @return
     */
    public static String format(Object object) {
        Yaml yaml = new Yaml();
        return yaml.dumpAsMap(object);
    }

}

至此,我們已經完成了 Yaml 的讀寫。當然,除了上述的Snake Yaml 之外,還可以使用 流行的 Jackson 元件了進行解析,這裡不再過多贅述,有興趣的朋友可以自行嘗試。

參考文件

阮一峰-YAML語言教程:
http://www.ruanyifeng.com/blog/2016/07/yaml.html

SnakeYaml 官方文件:
https://bitbucket.org/asomov/snakeyaml/wiki/Documentation

Yaml 1.2 規範:
http://www.yaml.org/spec/1.2/spec.html

SpringBoot-LoadingYaml
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-loading-yaml

點選檢視美碼師的 SpringBoot 補習系列

相關文章