核心原理篇
第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。