你真的懂 Yaml 嗎?

  • 2019 年 11 月 13 日
  • 筆記

本文公眾號來源:美碼師

作者:美碼師

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

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

然而,本文的主角並不是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: 'HellonWorldn' }

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

text1: |+    Hello      text2: |-    Hello

對應的結果為:

{ text1: 'Hellonnn', text2: 'Hello' }

除此之外,Yaml 還可以支援引用、函數、正則表達式等高級用法,但項目上一般很少用到。

三、操作 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 組件來完成類似功能,這裡不再過多贅述,有興趣的朋友可以自行嘗試。