ODL项目管理设计详解
“罗马不是一天建成的”。同样,ODL也是历经多年才不断发展壮大。作为一个开源项目,在参与ODL的志愿者们的共同努力下,ODL在架构与项目管理方面持续得到演进,功能变得越来越多,架构日趋合理,项目管理的层次越来越清晰,社区及项目中的各种问题也逐步被解决。
在第1章我们了解到ODL采用了模块化的架构设计,现在ODL的子项目多达上百个,每个子项目又分为若干模块,可以说有数百个模块。这数百个模块从OSGi的视角来看就是几百个Bundle;从Maven的视角来看,就是几百个pom。对这种规模的项目进行管理不是一件容易的事情。因此,本章首先描述ODL社区在管理众多子项目过程中会遇到的若干问题,并将和读者一起回顾社区通过Maven工具解决这些问题的思路和设计原则,以及如何一步步优化并完善解决方案,最后给出社区总结的对于项目管理的最佳实践。
2.1 问题的提出
最初接触ODL项目时,笔者曾被它的庞大和繁杂吓到了。有个比喻说,“ODL是一只会跳舞的大象”。一方面是因为ODL包含了大量的功能特性;另一方面是因为最初的ODL版本中的各子项目是由参与创立ODL的各公司独立开发贡献到社区的,根本没有统一的架构设计,也很难做到统一的项目管理,而且一些子项目本身也没有进行很好的设计。比如,ODL最初的版本中,Cisco主导贡献的ODL最核心的框架部分:controller项目里,就有如下典型问题:
- 该项目没有统一规划parent,比如mdsal就定义了两个parent:sal-parent和comp-atibility parent,版本号都是1.1-SNAPSHOT,但这两个parent都继承自另一个版本号为1.4.2-SNAPSHOT的parent pom。
- 很多模块的版本号缺乏规划,没有从其直接的parent继承,使其定义混乱。比如UserManager的pom中,其parent的版本定义为1.4.2-SNAPSHOT,该模块的版本号却被定义成0.4.2-SNAPSHOT。
- 依赖不是从parent pom继承,而是直接罗列在当前的pom中。
其他子项目也有类似的问题主要包括以下几个方面,1)整个ODL项目缺乏统一规划的parent pom,各模块继承层次不清晰,构建依赖混乱,版本编译构建和集成的工作经常出现各种问题,无法稳定;2)各项目和模块的版本号缺乏统一规划,导致ODL的子项目的几百个模块都有不同的版本号,维护与演进非常麻烦和困难;3)发布版本过程中,需要花费大量的人力修复出错的pom文件。
这些问题不仅给项目自身的维护和开发带来了困难,同时给学习和使用ODL的用户也带来了困扰。用户很难梳理清楚各项目和模块的不同版本,特别是ODL最初的几个发布版本。因此,用户在加载和运行初始发布的ODL版本的过程中,要面对非常多的干扰。
2.2 解决思路
所有用Maven管理的项目都应该是分模块的,每个模块都对应着一个pom.xml,pom.xml文件是Maven进行工作的主要配置文件。在这个文件中我们可以配置groupId、artifactId和version等Maven项目必需的元素,可以配置Maven项目需要使用的远程仓库,可以定义Maven项目打包的形式,也可以定义Maven项目的资源依赖关系等。对于一个最简单的pom.xml来说,必须包含modelVersion、groupId、artifactId和version这4个元素,当然其中的元素可以是从它的父项目中继承的。在Maven中,通过使用groupId、artifactId和version组成groupdId:artifactId:version的形式来确定唯一的一个项目(模块)。
对于如何处理所构建的多个模块间的关系,Maven给我们提供了一个pom的继承和聚合的功能。所谓聚合是可以通过一个pom将所有的要构建模块整合起来;所谓继承是在构建多个模块的时候,往往有多个模块会有相同的groupId、version或依赖,为了减少pom文件的配置,同面向对象的设计中类的继承一样,在父工程中配置了pom,子项目中的pom就可以继承。总之,聚合是为了方便高速构建项目,继承是为了消除重复配置,在简化pom的时候还能促进各个模块配置的一致性。两者的共同点是其packaging都是pom,聚合模块与继承关系中的父模块除了pom之外都没有实际内容。
ODL项目是通过Maven工具进行管理的,因此,对于上文ODL项目管理中碰到的问题,如何优化pom.xml的设计就是一个解决思路。对于公共编译配置和依赖管理的问题,ODL社区从第二个版本发布开始,成立了odlparent项目。该项目最初只有一个pom文件,这个pom文件里包含所有配置和依赖管理。顾名思义,这个项目的pom就是ODL所有其他项目的parent。从第1章我们了解到,ODL的模块是遵循OSGi规范而设计的,所有模块都要编译为bundle部署到OSGi容器里才能加载运行。因此,从第三个版本发布开始,bundle构建的公共配置也被放在odlparent中,也即bundle-parent。同时,编译构建bundle时的checkstyle配置文件也统一放在了odlparent项目中。第1章在搭建编译环境时,需要下载的Maven的settings.xml文件,也放到了odlparent中进行维护。在后续发布的版本中,odlparent项目中陆续加入了feature管理的parent,以及karaf打包的parent。这样,在ODL的子项目中,只需继承odlparent中相应的parent pom,就可继承这些公共配置,极大地简化了ODL子项目中pom的配置。
当然,我们不要忘了ODL的核心框架MD-SAL。在mdsal子项目中,有一个binding-parent的pom,这个pom包含了通过yangtools解析YANG模型所生成的binding代码的默认构建配置。对于所有遵循MD-SAL开发的模块(bundle)的构建,只需要继承这个pom,然后在你的pom中添加模块的自身依赖即可。
不仅是对parent pom的设计,包括各子项目中版本号规划,社区都给出了指导性建议,列在下面供读者参考:
- 统一定义全局的parent pom,对所有子项目提供公共配置。
- 除非必要,否则每个子项目只能有一个parent。
- 所有子项目的parent都继承自全局parent pom。
- 每个子项目中的模块的版本号要统一,版本号要按照..格式进行定义。
- 项目依赖的版本号在parent pom里统一进行管理,在子项目的模块pom里不能对版本号硬编码。
- 要对所有子项目使用一致的命名规范。
ODL的命名规范包括代码目录命名、模块坐标的命名规范、feature的命名规范。这里就不再详细说明了,如果想进一步详细了解的可以参考:https://wiki.opendaylight.org/view/CrossProject:HouseKeeping_Best_Practices_Group:Project_layout及https://wiki.opendaylight.org/view/CrossProject:Integration_Group:About_User_Facing_Features。
图2-1是按照odlparent项目发布的最新版本设计的parent pom继承关系示意图。
图2-1 odlparent中父pom设计
在图2-1中,各pom的说明简单如下:
- odlparent-lite—最基础的父pom,被所有的ODL项目的Maven模块直接或者间接继承。可以供不生成发布坐标(artifacts)的Maven模块直接继承(比如聚合的POM)。
- odlparent—公共的父pom,供包含Java代码的Maven模块继承。
- bundle-parent—供生成OSGi Bundle的Maven模块继承的父pom。
- binding-parent—遵循MD-SAL架构设计的Maven模块继承的父pom。
- single-feature-parent—供生成单个Karaf 4 feature的Maven模块继承的父pom。
- feature-repo-parent—供生成Karaf 4 feature库的Maven模块继承的父pom。
- features-odlparent—ODL依赖的公共库的feature集合的库。
- karaf4-parent—供生成Karaf 4发布包的Maven模块所继承的父pom。
图2-1中虚框内的两个pom(dom-parent和binding-parent)不是在odlparent项目里定义的,而是在mdsal项目里定义的。
以上parent pom的详细说明以及在子项目中的继承应用参见2.3节和2.4节。
2.3 实现详解
本节我们将对图2-1中pom文件的设计,主要是对pom包含的配置以及主要用途进行详细说明。
2.3.1 基础parent设计
最基础的parent pom包括两个—odlparent-lite和odlparent,前者包括一些公共配置信息,后者则在前者的基础上增加了通用的第三方库的依赖。
1. odlparent-lite
odlparent-lite是ODL所有的Maven项目和模块最基础的父pom,主要提供了以下公共配置信息。
- license information(许可证信息)。
- organization information(组织信息)。
- issue management information (a link to our Bugzilla)(问题管理)。
- continuous integration information (a link to our Jenkins - setup)(持续集成信息,Jenkins的链接)。
- default Maven plugins (maven-clean-plugin, maven-deploy-plugin, maven-install-plugin, maven-javadoc-plugin with HelpMojo support, maven-project-info-reports-plugin, maven-site-plugin with Asciidoc support, jdepend-maven-plugin)(默认的Maven插件)。
- distribution management information(发布管理信息)。
- source code management(源码管理)。
pom中的具体配置如代码清单2-1所示。
代码清单2-1 odlparent-lite中的公共配置信息
ODL的问题跟踪现已改为JIRA(https://jira.opendaylight.org/),原来的Bugzilla虽然还可以访问,但已不能在Bugzilla上提交Bug。
另外,在这个pom中还定义了几个有用的profile,以满足不同的环境或不同的构建要求。举一个例子,如代码清单2-2所示的这个profile。
代码清单2-2 odlparent-lite中的快速构建的profile
使用该profile,只需要在执行构建命令时,加上参数-Pq(即mvn clean install -Pq),即可跳过测试、忽略代码检查、不生成site和文档等,加速构建的过程。
这个pom可直接被ODL子项目中聚合的pom所继承。
2. odlparent
该pom继承自odlparent-lite,主要提供ODL项目的依赖和插件管理。
如果ODL的项目或你自己写的模块依赖到如下第三方库,就需要继承odlparent这个pom。
- Akka (and Scala)
-
Apache Commons:
- commons-codec
- commons-fileupload
- commons-io
- commons-lang
- commons-lang3
- commons-net
Apache Shiro
- Guava
- JAX-RS with Jersey
-
JSON processing:
- GSON
- Jackson
-
Logging:
- Logback
- SLF4J
- Netty
-
OSGi:
- Apache Felix
- core OSGi dependencies (core,compendium…)
-
Testing:
- Hamcrest
- JSON assert
- JUnit
- Mockito
- Pax Exam
- PowerMock
-
XML/XSL:
- Xerces
- XML APIs
因为ODL所依赖的第三方库是动态的,可能会增加或删去某个第三方库依赖,所以这个列表不一定是完整准确的。
这个pom还配置了对Java源码的Checkstyle规则设置,特别的是对于所有Java代码,其强制必须附带EPL License的声明头。声明头格式要求如代码清单2-3所示。
代码清单2-3 EPL license header
其中,${year}是代码发布年份,${holder}是发布该代码的版权所有者。
2.3.2 模块构建
ODL的模块遵循OSGi规范,在OSGi中,模块即bundle。因此,构建ODL项目中的某个模块,即编译构建OSGi的bundle。另外,ODL的核心框架是MD-SAL,由于YANG模型是贯穿模块设计的,且其是模块交互的基础,因此,以YANG模型驱动的模块设计是ODL中普适的设计模式,所以构建包含YANG模型的模块就成了ODL各子项目的公共需求。
1. bundle-parent
该pom继承自odlparent,这个pom主要用来配置OSGi bundle的构建。在该pom中:
- maven-javadoc-plugin被激活,用以构建Javadoc的JAR包。
- maven-source-plugin被激活,用以构建源码的JAR包。
- maven-bundle-plugin被激活(包括扩展),用以构建OSGi bundles(使用“bundle”类型打包)。
另外,在该pom中也增加了JUnit作为test范围的默认依赖,因此在bundle的编译构建过程中,默认是会执行单元测试的。
2. dom-parent
这个pom不是在odlparent中定义的模块,而是在mdsal项目中。其继承自bundle-parent,在bundle-parent基础上只增加了对yangtools和mdsal的依赖管理(dependencyMan-agement)。
3. binding-parent
该pom继承自dom-parent,也是在子项目mdsal中定义的。该pom增加了通过yang-maven-plugin插件对YANG文件的解析及根据解析的YANG文件自动生成代码的配置。遵循MD-SAL框架设计的模块pom都可以继承自该pom,生成的代码默认输出到target/ generated-sources/mdsal-binding/,读者可以到这个目录下查看自动生成的代码。不过依据Maven的约定优于配置原则,不建议大家自行修改这个默认配置。
2.3.3 feature组织
feature是Karaf中引入的一个概念,通常是若干相关的bundle及其配置的集合。安装这个feature的时候,相应的bundle也会被安装上去。通过feature,极大地方便了Karaf中对于bundle的管理。在ODL中,每个子项目的功能特性的发布也是按照feature发布的。
1. single-feature-parent
该pom继承自odlparent,用以生成Karaf 4的features:
- karaf-maven-plugin被激活,用以构建Karaf features,即以“feature”类型打包的pom(“kar”类型也支持)。
- 基于pom中定义的依赖生成feature.xml文件,你也可以通过配置src/main/feature/feature.xml文件来实现对feature.xml的初始化。
- Karaf feature在构建完成后会被测试,以确保这些feature能在Karaf容器里安装激活。
在生成feature.xml过程中,传递依赖默认会被添加,这样,我们只需要在pom中定义最重要的直接依赖即可,其他的依赖会自动查找。
配置文件“configfiles”既要在pom中定义依赖,也要作为feature.xml的元素定义。feature依赖的feature需要被定义为“xml”类型和“features”类别,具体配置可参考代码清单2-4和代码清单2-5。
代码清单2-4 pom中feature依赖配置及configfile依赖配置
代码清单2-5 feature.xml文件中configfile标签配置
2. feature-repo-parent
该pom继承自odlparent,用以构建Karaf 4的feature库,且与single-feature-parent遵循同样的设计原则,其是为feature库专门设计的,它会把pom中的所有依赖的feature构建为一个feature库,并在构建过程中提供对feature的测试支持。
3. features-odlparent
这是一个包含了ODL依赖的基础feature库的pom,包括Karaf自带的feature,如jdbc、jetty、war、feature等,也包括ODL依赖的其他第三方库的feature,比如akka、guava、netty、lmax、triedmap、jersey、javassist、gson、jackson、apache-commons等。
2.3.4 版本打包
1. karaf-plugin
ODL社区设计了一个Maven插件karaf-plugin,用来实现把所有依赖的组件发布到本地仓库的目的,其基本实现原理是根据打包的feature列表把其中所有依赖的bundle和配置都复制到本地仓库。插件源码实现不太复杂,感兴趣的读者可以直接下载odlparent项目阅读。
2. karaf4-parent
该pom用于构建Karaf 4的发布包,这个pom里用到了karaf-plugin这个插件,其会把所有运行时依赖的feature依赖都打包在这个发布包中,依赖的组件会被复制到发布包的system目录下,该目录的默认配置为Karaf的本地仓库。pom文件中的属性karaf.local-Feature用来指定初始启动的feature(除了standard外的feature)。
构建的发布包可用于本地测试。
2.4 项目模板
2.4.1 项目目录布局设计
按照前面的pom层次设计及规范要求,一个典型的ODL子项目目录布局可设计如下:
ODL子项目或者你自己定义的项目一般都包含多个模块,为了方便处理,在上面的目录布局中,api和impl可以一起放在一个单独的目录中。比如module1,如果增加一个模块,那么就再增加一个子目录module2,里面仍然可以按照api和impl的结构布局,api和impl这两个目录名字也可以按照实际情况重新命名。
对于聚合的pom或者集成测试模块,我们一般是不需要发布该pom和模块的,如果我们不想发布pom和模块,可在pom文件里加上如代码清单2-6所示的配置。
代码清单2-6 不准备发布的pom配置
2.4.2 ODL模板项目
Maven中有一个概念叫achetype(原型),也可称之为项目模板,学会了上文的项目目录规划和布局,我们就可以设计一个Maven的模板项目(pom中打包发布类型为maven-achetype),后续如果我们想创建具有类似目录结构的项目,可以直接通过Maven的命令mvn archetype:generate创建该模板生成项目骨架,然后直接基于这个骨架把我们自己开发的代码添加进去。
其实,Maven archetype项目在ODL最初发布的几个版本中就已经有了,不过这个项目原来是属于controller子项目的一部分,并且其提供的项目原型的目录结构和继承的pom也在一直调整。不过从氟版本(第9个发布版本)开始,原型项目就被独立出来,单独成立了一个achetypes项目,这样方便对原型项目的测试和发布。当前这个项目只包括一个opendaylight-startup项目原型,但并没有在氟版本中正式发布,如果读者想基于这个原型来创建基于ODL氟版本的自定义项目,可以考虑使用SNAPSHOP版本,通过下载achetypes源码,在本地构建安装这个项目,这样就可以在本地使用了。
2.5 本章小结
ODL的项目管理用到的工具和基础设施除了Maven,其他还有JIRA、Jenkins、Nexus等,因为其他工具与我们关注的项目源代码本身关系不大,所以就不介绍了。本章主要介绍了ODL利用Maven这个项目管理工具,不断地对自身进行优化和演进,逐步梳理清楚ODL中的项目间的层次结构关系,使其越来越合理,越来越容易被理解和掌握。
ODL从碳版本到氮版本,完成了Karaf 3到Karaf 4的升级,虽扯非常广,但由于对于ODL的所有项目的配置和依赖做到了通过odlparent统一管理,使得项目的构建配置和依赖配置设计十分清晰合理。因此,本次升级只跨了一个版本就完成了,这也验证了ODL社区在项目管理领域的不断成熟。
社区也曾经讨论过用Gradle取代Maven作为项目管理的工具,但并未确定是否会真的实施,目前看可能性不大。就算真的把Maven迁移为Gradle,读者也无须担心,变化总是会向好的方面发展。其实无论是ODL的项目管理,还是ODL的基础架构,都是处于一个持续发展的过程中,静止不变的开源项目是没有活力的。我们在理解ODL的基础架构时,也要时刻注意这一点,ODL的基础架构也是逐步演进才达到目前的状态的,并且还处于不断向前的过程中,我们要以动态的眼光看待ODL,这才是正确的态度。接下来我们将基于前面介绍的基础知识,聚焦ODL核心项目模块的设计原理和实现源码,让读者对ODL当前最新版本中的核心框架和实现原理有一个详尽的了解。