第二部分
核心原理篇
第3章
ODL基本對象的設計與實作
在Java的世界裡,一切皆為對象。本章我們一起看一下ODL的基礎架構中的幾個基本對象的設計與實作,這些對象是構成ODL MD-SAL架構的基礎,相當于建構高樓大廈的鋼筋水泥。我們已經知道,ODL核心架構是YANG模型驅動的服務抽象層。是以,ODL中的基本對象就與YANG語言有直接的淵源。YANG是對資料模組化的語言,YANG将資料的層次結構模組化為樹,稱為資料樹。資料樹中每個節點都有一個名稱,以及一個值或一組子節點。YANG提供了清晰簡潔的節點描述,以及這些節點之間的互動。本章介紹的基本對象就是對YANG語言裡元素命名、資料樹的索引和資料節點定義的抽象,也即QName、YangInstanceIdentifier和NomalizedNode三種對象。
3.1 QName
名不正則言不順,在一個概念體系裡,按照什麼規範定義元素的名稱是最基本的一個問題,本節就先介紹一下ODL對MD-SAL架構中基本元素的命名定義的抽象—QName。
3.1.1 QName定義
QName(Qualified Name,限定名)簡單了解就是添加了命名空間的成員名稱。QName來源于XML,由XML的名字空間和XML元素名稱組成,構成格式是名字空間(namespace)字首以及冒号(:)再加一個元素名稱(local name)。以代碼清單3-1為例。
代碼清單3-1 XML文本
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
version="1.0">
<xsl:template match="foo">
<hr/>
</xsl:template>
</xsl:stylesheet>
xsl是名字空間字首,template是元素名稱,xsl:template就是一個QName,而template稱為localName。舉個例子友善大家了解,三國演義中,兩将對陣,第一句就是問來者何人,一般回答類似“吾乃常山趙子龍是也”。這裡的“常山趙子龍”就可以稱為QName,常山對應的就是namespace,趙子龍對應的就是localName。它對“趙子龍”添加了“地域”(對應命名空間)的限制,使得表達上更加準确。
為什麼要從這個QName的定義講起呢?從上述段落我們也可以看到,QName是XML元素的限定名稱,也是組成XML的最基本要素,隻有了解了它才能進一步描述更加複雜的概念和關系。ODL的yangtools項目裡對QName的定義與XML裡的定義非常類似,但又不是完全相同的。那有什麼不同嗎?相較于XML裡QName的定義,ODL裡對于QName的定義增加了YANG模型定義檔案裡面的revision這個元屬性。也就是說,ODL裡QName包含namespace、localName和revision這3個字元串類型的屬性确定。
QName類的定義源代碼在yangtools子項目的yang-common子產品内,即yang/yang-common/目錄下。下面我們先看一下QName類及相關類的類圖,然後再講解其中關鍵源代碼實作。
從QName的定義中,我們知道其包含local-Name、namespace和revision這3個屬性,而在YANG語言中,是通過namespace和revision這兩個元屬性來辨別一個YANG的Module。即在ODL的設計中,設計了QNameModule與Revision這兩個類來封裝這兩個元屬性及其相關操作。QName、QName-Module與Revision類關系如圖3-1所示。

圖3-1 QName、QNameModule與Revision類關系圖
通過圖3-1,我們應該能清晰地看出這3個類之間是組合關系,即QName包含1個QName-Module對象變量,QNameModule包含1個Revision對象變量。這3個類包含的屬性我們可以看作是字元串類型變量,是以設計這樣3個類應該不是什麼複雜的事情,下面直接上QName、QNameModule和Revision的類圖設計(圖3-2)和源碼來對照看看。
圖3-2 QName類設計圖
看QName類的源代碼的話,我們可以看到源碼中定義了兩個類成員變量localName和module,localName就是一個String類型的對象,而module就是為QNameModule類定義的對象。在圖3-2,QName的4個屬性就來源于這2個成員變量,如代碼清單3-2所示。
代碼清單3-2 QName類定義
從代碼清單3-2中,我們看到QName中包含了一個QNameModule類型的變量,下面是QNameModule類的設計圖(圖3-3)及源碼,如代碼清單3-3所示。
圖3-3 QNameModule類設計圖
代碼清單3-3 QNameModule類定義
從上文也能看出它包含了一個Revision類型的變量,在YANG中,revison元屬性是一個日期格式的字元串,類似2019-03-14。在ODL早期版本的Revison定義中,通過使用Java中的Date類型定義處理這個變量,但我們知道,Java使用基本類庫中的java.util.Date對象來封裝目前的日期和時間,Date對象内部儲存的隻是一個long型的變量,儲存的是自格林尼治時間(GMT)1970年1月1日0點至Date對象所表示時刻所經過的毫秒數。是以,如果某一時刻遍布于世界各地的程式員同時執行new Date語句,這些Date對象所存的毫秒數是完全一樣的。也就是說,Date裡存放的毫秒數是與時區無關的。把Date對象解析為具體的時間時,需要先讀取作業系統目前所設定的時區,然後根據這個時區将把毫秒數解釋成該時區的時間。即同一個Date對象,按不同的時區來格式化,這樣就會得到不同時區的時間。
不過這樣一來就把問題搞複雜了,本來就是一個簡單字元串,如果通過Date對象來表示,不僅涉及兩者的互相轉換,要考慮格式,還要考慮時區,這确實有一些問題。這啟示我們,在進行設計時,千萬不要過度設計,甯缺毋濫。在最新的ODL版本Revison的類定義中,去掉了Date類型變量的定義,隻包含了一個字元串變量,其類圖(圖3-4)和源碼如代碼清單3-4所示。
代碼清單3-4 Revision類定義
圖3-4 Revision類設計圖
從上面3個類圖和源碼的實作中能看出每個類的設計都不複雜,每個類隻包含基本屬性及相關操作,遵循了面向對象的類設計中的單一職責的設計原則。
知識點 所謂單一職責的設計原則是指每一個類應該有且隻有一個變化的原因。當需求變化時,将通過更改職責相關的類來展現。如果一個類擁有多于一個的職責,則多個職責耦合在一起,會有多個原因導緻這個類發生變化。一個職責的變化可能會影響到其他的職責,另外,把多個職責耦合在一起,會影響複用性。
從圖3-2無序列化接口和SerialVersionUID屬性能看到,它們都實作了Serializable和Comparable接口,也即支援序列化和比較的功能,我們也看到,每個類都定義了一個serial-VersionUID屬性。
知識點 serialVersionUID适用于Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一緻性的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一緻的,可以進行反序列化,否則就會出現序列化版本不一緻的異常,即是InvalidCastException。Java序列化的最佳實踐就是顯式地聲明Serial-VersionUID,避免反序列化過程中可能出現的問題,是以實作接口Serializable的類必須聲明一個static,final并且是long類型的SerialVersionUID屬性,根據相容性确定是否變更這個屬性值。
對于實作Comparable接口的類,就要實作compareTo()方法類實作對象的比較。我們知道,在Java中關于對象的比較,還可以通過equals()方法和“==”方法,那對于QName的比較,到底是如何實作的,我們在3.1.2節會介紹。
3.1.2 QName對象比較
從QName類的定義中我們看到其實作了Comparable接口,也即實作了compareTo()方法,具體實作如代碼清單3-5所示。
代碼清單3-5 QName比較實作
從這個方法中可以看出,兩個QName作比較時,localName會先比較,如果local-Name相同,再繼續比較其包含的QNameModule對象,我們再看一下QNameModule的compareTo()方法實作,如代碼清單3-6所示。
代碼清單3-6 QName比較實作
QNameModule比較時,會先比較namespace,如果namespace相同,則繼續比較revision。其中Revision.compare()的實作最終調用了Java裡String類的compareTo()方法,比較的傳回值就是String類的compareTo()方法的傳回值,即相等時傳回0,不等時,傳回兩個字元串第一個不同的字元的內插補點。通過以上代碼,對于QName的比較的過程及原理,我相信讀者應該會比較清楚了。
Java中對象的比較,equals、==、compareTo這3種方式是有差別的,==是對象引用(位址)的比較,傳回值為true或false。equals方法依賴于==,但對于String類型,equals是對字元串内容的比較,因為String重寫了equals方法。對于自定義對象的,如果想比較對象内容,也必須重寫equals方法,否則,其實作與==等同。而compareTo是按照Character對象比較,對于字元串對象來說,是按照字典的順序來比較字元串,如果兩個字元串相等則為0,若不等,則前面的字元串按照字典順序較大則為正數,反之為負數。
3.1.3 QName對象建立
在了解了QName對象的定義和比較後,再看看建立QName對象的方法,從圖3-2能看到QName提供了多個create方法建立QName對象,讀者可以靈活使用上述方法建立QName對象。其中一點值得我們注意的地方是,在ODL代碼裡,經常看到在建立QName後,最後加上intern()方法,如代碼清單3-7所示。
代碼清單3-7 QName對象建立
這是為什麼呢?我們知道,在QName的定義中,namespace、revision、localName都可以看作是在YANG檔案中定義的常量字元串,而Java中String類也設計了intern()方法,其設計的初衷就是利用字元串常量池重用String對象,以節省記憶體消耗。我們看一下QName的intern()方法的實作代碼,如代碼清單3-8所示。
代碼清單3-8 intern()方法實作
代碼中,INTERNER的定義為:
private static final Interner INTERNER = Interners.newWeakInterner();
Interners為guava庫的類,為什麼用guava庫而不直接用JDK中字元串的intern方法呢?因為JDK不同版本(JDK6,7,8)中String實作的intern方法的機制不太一樣,使得在使用時可能導緻出現某些問題,是以不建議直接用String的intern方法。而guava庫中的Interners類對intern做了許多的優化,如使用弱引用包裝你傳入的字元串類型等。這樣就不會對記憶體造成較大的影響。使用該類的intern(str)來進行對字元串intern,解決了直接使用String類中intern()方法可能存在的問題。從這裡也可以看出,ODL對于QName這個類的實作上十分用心。
3.2 YangInstanceIdentifier
我們已經知道YANG定義的資料模型就是一個資料樹,在YANG語言中,有一個内建類型instance-identifier用來唯一辨別資料樹中某個節點。對應的,在ODL中也定義了一個基本的類YangInstanceIdentifier。這是一個分層的、基于内容的、唯一的辨別符,用來對資料樹中資料項的尋址,代表了資料樹中某個節點的路徑。下面我們可以看到其類定義用到了3.1節中QName。
3.2.1 Path接口定義
說到路徑,我們最熟悉的路徑就是在計算機中檔案系統的目錄路徑,另外還有一個可能大家不怎麼熟悉,即XPath(XML Path language),它是一種用類似目錄樹的方法來描述在XML文檔中的路徑,這兩種路徑的共同點是都使用"/"來表示上下層級間的間隔,中間是節點或層次的名稱。在XPath中,我們還能使用運算符(帶謂語的表達式),類似于
/bookstore/book[price>35.0]這樣對樹中的條目進行過濾和篩選。YANG中instance-identifier文法格式是XPath的簡化格式的子集。
那路徑有什麼特點呢?首先是路徑具有相對性,我們描述一條路徑一定是說從某個節點(樹的根節點也是節點)到另一個節點的路徑;其次,把若幹條路徑拼接起來,其形式還是路徑,把一條路徑從分割符"/"處拆成幾部分,每一部分也還是路徑的形式,也就是說路徑在形式上是自包含的。在ODL中,定義了一個Path接口來描述上面的特性,下面看一下Path接口的定義,如代碼清單3-9所示。
代碼清單3-9 Path接口定義
該接口的定義巧妙地用到了Java中範型,定義了一個contains方法,該接口定義描述了上面我們說的路徑的本質。這個接口定義很簡潔、精練。讀者可以仔細體會一下其蘊含的魅力。
知識點 Java中範型即“參數化類型”。顧名思義,就是将原來具體的類型參數化,類似于方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然後在使用/調用時傳入具體的類型(類型實參)。泛型的好處是在編譯時能檢查類型安全,并能捕捉類型不比對的錯誤,而且所有的強制轉換都是隐式的和自動的,提高了代碼的重用率。
3.2.2 YangInstanceIdentifier的類定義
本節講的YangInstanceIdentifier類實作了這個Path接口,是以可以說YangInstance-Identifier類就是表示了資料樹中的節點通路路徑的定義。接下來,我們看一下yangtools項目裡對YangInstanceIdentifier類的定義,如代碼清單3-10所示,其源碼路徑在yang/yang-data-api目錄下。
代碼清單3-10 YangInstanceIdentifier類定義
這是一個抽象類,為了簡潔,類中省略了一些方法聲明。我們知道檔案系統的目錄路徑由檔案夾名稱組成,XPath由XML的“元素名稱+謂語表達式”組成。在ODL中,YangInstanceIdentifier由PathArgument組成,即PathArgument就是組成YangInstanceIdentifier的要素,具體來說就是一組有序的PathArgument清單構成一條通路路徑(一個YangInstance-Identifier對象)。
PathArgument,顧名思義,即構成路徑的參數,其定義如代碼清單3-11所示。
代碼清單3-11 PathArgument定義
從這段代碼中我們看到PathArgument是一個接口,它定義了兩個方法QName getNode-Type()和String toRelativeString(PathArgument previous)。第一個方法表示它的定義裡包含QName,這表示構成路徑的基本參數就是資料樹中的節點名(QName),第二個方法表示它可以表示成一個包含其前驅節點路徑參數的字元串。
圖3-5是PathArgument接口及其實作類的類圖。
PathArgument接口及其實作類的定義都位于YangInstanceIdentifier類定義檔案中,具體實作PathArgument這個接口的3個子類分别為NodeIdentifier、NodeIdentifierWithPredicates和NodeWithValue。其分别代表了辨別YANG定義的資料樹的container和leaf路徑參數,辨別資料樹的list中的條目的路徑參數與辨別資料樹中leaf-list的路徑參數。
了解了構成YangInstanceIdentifier的參數和要素PathArgument,下面看下YangInstance-Identifier類圖,如圖3-6所示。
從圖3-6的YangInstanceIdentifier類設計,能看到它包含一組PathArgument,這個類定義中還包含了幾個建立(create、of、node)類執行個體的方法,還提供了一個builder()方法傳回建構類以實作該類對象的建構。由于YangInstanceIdentifier隻是一個抽象類,要構造類對象,就必須要有具體實作類。在yangtools項目源碼中,其實作類有兩個:FixedYangInstance-Identifier和StackedYangInstanceIdentifier。這兩個實作類主要差別是在其内部一個按照普通的清單處理方式來實作的,一個是按照棧的邏輯實作的。由于這兩個實作類不是public的,是以這兩個類在其定義的package外面是無法被通路的。是以,我們隻能通過YangInstance-Identifier類提供的構造方法或者提供的Bunilder來構造YangInstanceIdentifier執行個體,這樣的設計保證了通路和構造YangInstanceIdentifier對象的安全性。
圖3-5 PathArgument接口及實作類
圖3-6 YangInstanceIdentifier類圖
3.2.3 YangInstanceIdentifier的比較
因為YangInstanceIdentifier本質是通路資料樹的路徑,那麼在查詢和檢索資料樹時,就避免不了進行YangInstanceIdentifier對象的比較。YangInstanceIdentifier有兩個方法進行比較,一個是equals,其實作代碼如代碼清單3-12所示。
代碼清單3-12 YangInstanceIdentifier的equals方法實作
這個方法覆寫了Object的equals()方法,這段實作代碼裡第一個if判斷即如果引用一緻,則兩個對象一定相等;第二個if判斷,如果兩者類型不一緻,則肯定不相等,也對後面的強制類型轉換做了保護,來避免出現異常。再看上面的部分代碼,比較兩個對象的hash值,如果兩者hash值不同,則兩者肯定不相等,最後再調用一個方法去比較Yang-InstanceIdentifier的PathArgument是否都相同。這段實作代碼充分考慮到了效率和異常保護,值得我們參考和借鑒。
另外一個比較方法就是Path接口定義的contains()方法,實作代碼如代碼清單3-13所示。
代碼清單3-13 YangInstanceIdentifier的contains方法實作
從這段代碼實作上,可以看出其比較的過程,從第一個PathArgument開始比較,依次疊代進行比較所有源路徑裡PathArgument是否與目标路徑(方法入參other)裡的PathArgument相等,如果兩者比較過程中源路徑已到末尾,且源路徑最後一個PathArgument仍然相等,則傳回true。簡單了解上述處理邏輯就是PathArgument依次比較且都相等的情況下,短的路徑包含長的路徑。
3.2.4 InstanceIdentifier類
其實,在基于ODL進行應用開發時,經常使用到的是binding接口,而binding接口的定義,并沒有直接使用到YangInstanceIdentifier這個類,而是用的InstanceIdentifier這個類,這個類的定義不在yangtools項目中,而是在mdsal項目的binding/yang-binding目錄下,代碼清單3-14是它的類定義源碼。
代碼清單3-14 YangInstanceIdentifier的contains方法實作
這個類也代表路徑,其内部包含了一個疊代器類型變量pathArguments,可以看作是PathArgument清單。但這個類的定義裡包含了一個Class類型的變量targetType,使其把路徑與根據yang檔案生成的Java類關聯了起來,以友善大家可以直接使用根據yang生成的類。
InstanceIdentifier也提供了一個builder類以實作InstanceIdentifier對象的建立,使用方法如下所示:
InstanceIdentifierBuilder.builder(Nodes.class).child(Node.class, new NodeKey(new NodeId("openflow:1")).build();
Binding與Binding-dependent接口,在後續的章節中還會介紹,在此不再詳述。
3.3 NomalizedNode
本節将介紹ODL中資料樹節點的抽象定義。要講資料節點的抽象定義,我們首先要了解YANG中如何定義資料樹中各種節點。在YANG語言中,提供了container、list、leaf-list、leaf、choice、augment等關鍵詞來定義資料樹的層次和節點。在ODL的最初版本中,yangtools項目下有一個類Node作為所有資料節點的基礎抽象。為了更加符合YANG的規範中的實際含義,從锂版本開始,依據YANG規範重新定義了NormalizedNode類來作為資料節點的基礎資料節點的抽象。這個新的資料抽象節點定義。
3.3.1 NormalizedNode類的定義
YANG語言裡支援的節點類型有多種,比如leaf、list、leaf-list、choice、augment等,對于這麼多資料節點類型,如何使用Java定義一個通用的接口來統一表示上述節點類型呢?
先來看一下NormalizedNode及其子類的設計,使我們對YANG中各類型節點定義和ODL中資料節點定義有一個總的認識。
NormalizedNode -樹結構中表示一個節點的基礎類型;所有其他類型都繼承自該基礎類型。它包含了一個identifier和一個value。
DataContainerNode -所有可包含子節點的節點,在YANG文法中無直接對應的表示。
ContainerNode -容器Node,非重複的,可包含多個子節點的節點,對應YANG中的container。
MapEntryNode -表示一個可多次出現的節點,可以通過它的key進行唯一辨別,MapEntryNode可能包含多個子葉子節點。MapEntryNode對應YANG中list的一條執行個體。
ChoiceNode -表示一個非重複出現,但可能包含不同類型的值的節點,對應YANG中choice語句,類型對應choice下的case描述。
AugmentationNode -對應YANG中的augment節點定義,非重複的。
LeafNode -葉子節點,非重複節點,包含一個簡單類型的值,不包含子節點。對應YANG裡的leaf節點。
LeafSetEntryNode -可多次重複出現的葉子節點,對應YANG裡的leaf-list定義的節點的一條執行個體。
LeafSetNode -特殊節點,其包含特定類型的LeafSetEntryNode節點,對應YANG裡的leaf-list。
MapNode -特殊節點,包含MapEntryNode 節點,對應YANG裡的list。
上面的節點定義與YANG中的節點概念對照關系如表3-1所示。
表3-1 YANG語句與ODL節點抽象的對應
上面定義的各種節點抽象接口定義的繼承關系,從圖3-7可以更清楚地看出來。
以上接口的定義都是繼承自NormalizedNode,下面通過代碼清單3-15看一下Norma-lizedNode這個接口的源碼。
代碼清單3-15 NormalizedNode接口定義
從該接口,能看到其給出了一個節點的通用抽象。每個節點都需要有一個名字,即是QName;要能唯一辨別,即定義了繼承自PathArgument的K;要能包含值,即定義了V。并在接口的定義中用到了泛型,這樣上述K、V就能根據需要,替換為各種具體類型。雖然具體的實作類型多樣,但所有接口又能按照NormalizedNode這個通用接口來進行處理和引用,這就為我們在代碼實作時按照統一的方式處理各種類型提供了便利,這可以看作運用Java面向對象的多态特性的一個非常好的例子。
知識點 何為多态性?在面向對象的語言中,接口的不同種類實作方式即為多态,在具體使用的過程中,允許将子類類型的引用指派給父類類型的引用,指派之後,父對象就可以根據目前指派給它的子對象的特性以不同的方式執行。
多态性實作主要依靠動态綁定原理。由于是動态綁定,是以可以統一用Normalized-Node類型的引用指向其子接口或子類,極大地友善了程式設計實作。後述章節中,ODL中大量接口的定義就是以NormalizedNode接口作為入參的。
圖3-7 NormalizedNode及其子接口繼承關系圖
從上面我們看到,對于資料節點的接口定義及抽象,ODL定義的層次還是比較清晰的,但是YANG語言在規範中含有大量的細節,使其在代碼實作層面一次性考慮的面面俱到是不可能的。是以,社群代碼中這一部分發現的Bug是比較多的,社群也在不斷優化這部分的設計和實作。
3.3.2 NormalizedNode執行個體的建立
上面介紹的隻是一些接口定義,我們是無法直接使用其來建立執行個體的,如果我們想建立節點執行個體,可以使用ODL裡封裝的一系列Builder。這些類定義在yangtools項目的yang/ yang-data-impl子產品内。圖3-8列出了ODL提供的建立資料樹節點的各種Builder類。
圖3-8 ODL中的節點構造者
使用這些Builder構造節點對象時,其入參會用到3.1節介紹的QName,YangInstance-Identifier,建構的執行個體代碼可以參考ODL社群中yangtools項目裡的yang-data-impl裡的測試代碼。
3.4 本章小結
本章主要介紹了ODL建構核心架構MD-SAL的最基礎的幾個對象。“合抱之木,生于毫末,九層之塔,起于壘土”。這幾個對象是建構ODL大廈的基石,了解這幾個最基礎的對象,是了解ODL核心架構源碼的前提,後續章節将繼續基于這個基礎,介紹其核心資料結構—DataTree。