持续集成学习总结(四)
(四)基于 Ant 搭建敏捷开发过程中的持续集成环境
持续集成(CI)是敏捷开发过程中至为关键的一个环节,在每个迭代开发周期中,合理地对软件产品进行持续集成,将有效协调软件编码,测试以及版本发布各个团队的工作进程,降低软件开发风险,对客户需求做出最及时有效的反馈。Apache Ant 提供了丰富的核心任务以及扩展任务来完成持续集成过程中的各项工作,同时开源社区 Ant-Contrib提供的 Ant 任务更是大大增强了 Ant 的可编程性,使得 Ant 有能力完成更为复杂的逻辑操作。本文中将展示一个典型的 Web 2.0 应用在敏捷开发过程中的持续集成环境,并展示每个部分如何由 Ant 来具体实现。
1.基本的持续集成环境
一个持续、稳定的构建是整个持续集成过程中的关键。在每个迭代周期的开发过程中,软件开发团队应当及时将最新的功能代码进行提交和构建,以便使软件测试团队能够进行功能或系统测试,及早发现缺陷并尽快解决。同时,在相应迭代周期的后期,版本发布团队应该能够获取经过验证后的最新的产品构建,并将其打包成可交付产品或进行线上产品的更新,交由产品的项目关系人或最终客户进行使用,确保客户需求与软件产品一致。目前在 Web 2.0 应用的敏捷开发过程中多采用这样一种集成环境,以满足 Web 2.0 应用最为典型的“Always Beta”特性。
下图展示了一种基本的持续集成环境的拓扑结构。
图 1. 持续集成环境的拓扑结构
持续构建服务器上的 Ant 脚本首先从源代码管理库获取最新的代码,并按照特定的构建策略执行构建,比如在固定时间触发每日构建,而后将构建结果自动上传至 FTP 服务器用以保存和分发;功能测试 (FVT) 或系统测试 (SVT) 环境则包括了测试服务器和测试数据等信息,其上的 Ant 脚本则负责从 FTP 服务器获取最新的构建,提取更新所需的产品代码(如果需要,还要提取必要的数据库更新脚本,完成数据库架构的重构)来完成测试环境的更新,而后调用测试脚本进行测试并产生测试报告;产品环境则是面向终端用户的产品运行环境,其上的 Ant 脚本可以将通过测试的构建生成可交付的产品或线上产品更新包,这一过程常伴随产品版权信息验证、产品包压缩以及产品部署等动作。
2.Ant 如何帮助持续集成
Ant 作为 Java 开发领域应用最为广泛的自动构建工具,不仅可以帮助开发团队实施每日构建生成构建包,更支持在此构建包基础之上,生成测试团队和版本发布团队所需要的构建包以完成后续的产品测试与发布工作,最终使得整个迭代周期过程的产品集成实现自动化。本文不会对 Ant 的基本概念和所有任务进行逐一介绍,而是将作者在实践过程中认为对持续集成有所帮助的概念和任务加以解释阐明,以期读者更好地了解 Ant 的能力,并加以灵活的运用。
3.<Ant> 与 <Antcall>(<AntFetch> 与 <AntCallback>)
<ant> 任务提供了在一个构建脚本内调用外部脚本特定目标(target)的能力,这种能力可以很好的帮助我们管理整个持续集成过程。特别是在有多个项目构建需求的情况时,设计一个独立的控制脚本,借助 <ant> 任务使其通过调用不同的项目构建脚本完成整体项目的集成,这样做的一个最明显好处是使我们可以快速的适应项目变动,符合随需应变(On Demand)的开发模式。
<antcall> 任务区别于 <ant> 任务之处在于,其只能调用同一个构建脚本之内的构建目标,他所提供的是对一个构建脚本自身的清晰管理。以往依赖于 depends 属性的方式使得我们很容易迷失在复杂的目标依赖关系中,而使用 <antcall> 则能够将每个构建脚本的任务以显式的、易修改的方式呈现给项目构建者。
<antfetch> 和 <antcallback> 是 ant-contrib 开源项目提供的扩展任务,是增强版本的 <ant> 和 <antcall> 任务,他们不仅具备前二者的基本能力,还可以返回外部脚本或同脚本其他目标中的属性,可类比编程语言中带返回值的方法调用。
4.Available 与 Condition
在构建过程中,构建脚本不可避免地会对许多外部资源(文件,目录,URL 等)进行访问甚至修改,而为了能够有效的对这些资源进行操作,所需做的第一步通常是验证资源的可用性。<available> 任务可以帮助我们对各种外部资源进行判断,通过设置相应的属性来表明判断结果,进而引导后续的构建操作。
另外,在某些情况下,构建脚本或许需要对多个资源同时进行判断而不仅仅是单个资源。<condition> 任务通过支持丰富的内嵌标记(nested element),如 <and>/<or>/<xor> 等,具备了对资源进行更加复杂的逻辑判断能力。
5.For 与 If
当我们使用 Ant 脚本编写一些较为复杂的逻辑功能,比如循环和流程判断时,自然希望 Ant 能支持这种编程能力。然而 Ant 核心任务中并没有提供 <if> 任务,只是在 <target> 任务的属性中支持 if 属性,比如 <target name="build-module-A" if="module-A-present"/>,即表示只有 module-A-present 属性存在才执行 build-module-A 目标。但是,必须注意的一点是,这里的 if 并不是判断 module-A-present 属性是否设置为特定值,而仅仅是检查该属性是否被设置了,因而其可编程性并不是很强。
Ant-contrib 为 Ant 提供了与通常所使用的编程语言功能相同的 <if> 和 <for> 任务,在构建过程中灵活运用这两个任务,将大大增强 Ant 对逻辑操作的控制能力,这其实就是一种基于 XML 脚本的编程。
在具体的实践过程,有一点需要特别注意:在使用 <for> 任务的过程中,如果我们期望在循环体内使用一个变量,而非 Ant 的 property,则需借助由 ant-contrib 提供的 <variable> 任务(ant-contrib 任务)来实现。尽管这与 property 的值一经设置便无法改变的设计原则相抵,但有时能够给构建脚本很大程度上的灵活性。
6.Replace 与 ReplaceRegExp
在由开发构建包向产品构建包转变的过程中,替换与开发环境相关的属性值是主要工作之一 , 比如我们不能假设用户会将 JDK 安装在与开发环境相同的路径下,这时便可以使用 ant 的 <replace> 任务,<replace> 任务可以针对特定的字符串的执行替换操作。不仅如此,利用 Ant 扩展任务所提供的 <replaceRegExp> 任务,还可以实现基于正则表达式的替换。
例如,要将 test.bat 文件中的行首“java”字符串替换为“../../java/bin/java”而不影响其他“java”字符串,可以使用如下 ant 脚本:
<replaceregexp file="test.bat" byline="true" match="^java " replace="http://www.cnblogs.com/ java/bin/java " /> |
7.Filterchain 与 Mapper
Filterchain 和 Mapper 是在集成脚本中经常用到的 ant 概念。Filterchain 增强了面向数据传输的 ant 任务的能力,如 Concat,Copy,Loadproperties 和 Move,借助于各种不同功能的 filter,使得这些任务具备了数据筛选和处理的能力,非常类似于 Unix 系统中的管道的概念。
例如,要实现将 A 文件夹复制到 B 文件夹,同时对 B 文件夹中所有 jsp 文件的文件头添加 copyright.txt 文件内容的任务,可以使用如下 ant 脚本。
<copy todir="${B}"> <fileset dir="${A}" includes="*.jsp"/> <filterchain> <concatfilter prepend="copyright.txt"/> </filterchain> </copy> |
Mapper 则常出现于 Copy,Move 或 Unzip 任务中,它的作用在于为这些任务增加指定输出文件的能力,使得我们不仅可以通过 <fileset> 来指定源文件集,更可以通过各种不同功能的 mapper,来实现重新命名输出文件文件名或更改输出文件目录结构的能力,这在构建持续集成环境中起到了极为灵活的作用,很好的理解这两个概念有助于写出简单而功能全面的 ant 脚本。
另外,在一个复杂的持续集成环境中,我们不可避免地会涉及一些商业产品或者开源项目来搭建整个环境,比如使用 CVS,SVN 或 IBM ClearCase 作为项目源代码库,使用 Apache Tomcat,IBM WebSphere Application 作为测试或产品环境的部署服务器,使用 LiquiBase,DBdeploy 作为产品数据库的持续重构工具等。而 Ant 借助其易扩展的特性,对所有这些工具提供了很好的支持,外部工具的提供者只要实现特定的 Ant 任务接口,就可以提供自定义的 Ant 任务,我们只需要通过 <taskdef> 任务引入这些特定的 Ant 任务,便可以实现与这些工具的连接,实现通过 Ant 脚本来管理整个集成环境的目的。
9.实现一个基本的持续集成环境
在一个典型的线上 Web 2.0 应用的迭代开发周期中,持续集成通常涉及构建、部署、测试和上线等一系列动作,而这些动作能够自动运行的前提是获取各自需要的产品包(比如基于 Java EE 的产品都须提供的 WAR 或 EAR 文件)。因此,在构建服务器上调用一个综合性的 Ant 构建脚本(清单 1),产生其它动作所需要的产品包,则成为整个持续集成过程中最为核心的一步。
清单 1. 产生其它动作所需要的产品包
<?xml version="1.0" encoding="UTF-8"?> <project name="SampleOverall" basedir="." default="fetch_Code"> <property file="SampleOverall.properties" /> <taskdef name ="teamFetch" classname="com.ibm.team.build.ant.task.TeamFetchTask" /> <taskdef name ="teamAccept" classname="com.ibm.team.build.ant.task.TeamAcceptTask" /> <tstamp><format property="build.time" pattern="yyyy-MM-dd$hh-mm-ss" /></tstamp> <target name="perform_DailyBuild"> <antcall target="generate_SmokeTest_Package"/> <antcall target="mail"/> </target> <target name="perform_FVTBuild"> <antcall target="generate_FVTTest_Package" /> <antcall target="upload_to_FTP" /> </target> <target name="perform_ProductBuild"> <antFetch dir="${basedir}" antfile="checkLicense.xml" target="checkLicense" return="reportFile" /> <available property="reportFile_exist" file="${reportFile}"/> <fail message="Unlicensed file found, please check ${reportFile}" if="${reportFile_exist}"/> <antcall target="generate_Product_Package" /> <antcall target="upload_to_FTP" /> </target> <target name="fetch_Code"> <teamAccept repositoryAddress="${repositoryAddress}" userId="${userId}" password="${password}" workspaceName="${workspaceName}" verbose="true" /> <teamFetch repositoryAddress="${repositoryAddress}" userId="${userId}" password="${password}" workspaceName="${workspaceName}" destination="${destination}" verbose="true" /> </target> <target name="generate_SmokeTest_Package" depends="fetch_Code"> <!-- may also include other project,like SampleApp2, SampleApp3 --> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="war" inheritAll="false" /> </target> <target name="generate_FVTTest_Package" depends="fetch_Code"> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="ear_FVT" inheritAll="false" /> </target> <target name="generate_Product_Package" depends="fetch_Code"> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="product" inheritAll="false" /> </target> <target name="upload_to_FTP"> <zip destfile = "${BuildPackage}/SampleApp1.zip" basedir="${BuildPackage}" /> <ftp action="mkdir" server="${FTPAddress}" userid="${FTPUserName}" password="${FTPPassword}" remotedir="${FTPSharedFolder}/${build.time}"/> <ftp server="${FTPAddress}" userid="${FTPUserName}" password="${FTPPassword}" remotedir="${FTPSharedFolder}/${build.time}"> <fileset file="${BuildPackage}/SampleApp1.zip" /> </ftp> </target> <target name="mail"> <mail mailhost="${MailServer}" mailport="${MailServerPort}" subject="Build Report Mail" tolist="${MailList}" messagemimetype="text/html" messagefile="mailcontent.html"> <from address="${fromMailAddr}" /> </mail> </target> </project> |
不难看出,<antcall> 任务通过调用不同的任务组合达到了为不同构建目的提供不同构建动作的目的,其中包括对“冒烟”测试,功能测试以及产品环境安装的特定支持,而各个环境所需要的产品包也因 <ant> 任务目标的不同而不同,这种松散组合的方式为今后脚本的维护和更新提供了良好的基础。
在为产品环境提供产品包(perform_productBuild)的目标中,<antFetch> 扩展任务通过调用外部的 checkLicese.xml 脚本来对产品进行版权核查,任何没有版权信息的文本文件都将被记录到 reportFile 中。虽然类似的这种功能可以使用多种脚本语言来方便的实现,比如 Python 和 Ruby 等,但这里给出了基于 Ant 的实现,以更好的展示 Ant 脚本的灵活性和可编程性。清单 2 是使用 Ant 实现版权信息检查的部分脚本。
清单 2. 使用 Ant 实现版权信息检查的部分脚本
<target name="checkLicense" > <for list="${scanFolderList}" param="folderList"> <sequential> <for list="${scanFileType}" param="fileType"> <sequential> <for param="file"> <path> <fileset dir="@{folderList}" includes=**/*.@{fileType}> <not> <contains text="${licenseFragment}" /> </not> </fileset> </path> <sequential> <echo file="${reportFile}" message="@{file},${line.separator}" append="true" encoding="UTF-8"/> </sequential> </for> </sequential> </for> </sequential> </for> </target> |
对于测试环境和产品环境而言,获取产品包并自动的进行产品部署是两者共同的首要工作,而这个过程中所面临的主要问题通常涉及不同操作系统的脚本移植性问题。幸好,Ant 具备了良好的跨平台能力,我们不必为不同的部署环境(Windows 或 Linux)去编写不同的部署脚本,只需将精力集中于产品包的获取和针对不同应用服务器的部署即可,清单 3 展示了如何从 FTP 服务器获取产品包,并自动发布于 IBM WebSphere 应用服务器的过程。
清单 3. 获取产品包并自动部署
<?xml version="1.0" encoding="UTF-8"?> <project name="SampleProductEnv" basedir="." default="updateProduct"> <taskdef name="wsadmin" classname="com.ibm.websphere.ant.tasks.WsAdmin"/> <taskdef name="wsStopServer" classname="com.ibm.websphere.ant.tasks.StopServer"/> <taskdef name="wsStartServer" classname="com.ibm.websphere.ant.tasks.StartServer"/> <target name="getFromFTP" > <ftp action="get" server="${FTPAddr}" userid="${FTPUsr}" password="${FTPPasswd}" remotedir="${product_FTP}"> <fileset dir="${basedir}/FTPDownload"> <include name="production.*"/> </fileset> </ftp> </target> <target name="generateUpdatePack" depends="getFromFTP"> <unzip src="${basedir}/FTPDownload/Production.zip" dest="${basedir}/Production"> <globmapper from="code_build" to="app1"/> </unzip> </target> <target name="updateProduct"> <wsadmin script="${basedir}/updateApp1.py" user="${MMC_user_name}" password="${MMC_user_password}" conntype="NONE" failonerror="yes"> <arg value="${update_files_location}/app1/WEB-INF/web.xml" /> </wsadmin> <antcall target="stop-server" /> <copy todir="${product_Home}" overwrite="true"> <fileset dir="${update_files_location}/app1 "> <exclude name="**/web.xml"/> </fileset> </copy> <antcall target="start-server" /> </target> <target name="start-server"> <wsStartServer server="server1" noWait="false" trace="true" username="${MMC_user_name}" password="${MMC_user_password}" failonerror="yes"> </wsStartServer> </target> <target name="stop-server"> <wsStopServer server="server1" noWait="false" trace="true" username="${MMC_user_name}" password="${MMC_user_password}" failonerror="yes"> </wsStopServer> </target> </project> |
10.结束语
本文首先介绍了一种在敏捷开发环境中最基本的持续集成环境,然后结合作者自身实践,讲述了几种能够为持续集成提供重要支持和能力的 Ant 任务及概念,最后通过示例性 Ant 脚本片段展示如何使用 Ant 脚本来快速地搭建这样一种环境。
11.参考资料
学习
- Apache Ant 用户手册,从这里可以了解到 Ant 相关的详细内容。
- Ant-Contrib Task, 从这里了解更多 Ant-contrib 任务。
- 查看 敏捷专区,了解更多有关敏捷开发相关知识。
- 查看 IBM WebSphere Application Server 信息中心,获取更多对 WAS 对 Ant 所提供的支持。
- “敏捷软件开发基础:持续集成环境的构建”(developerWorks,2005 年 6 月):本文中,作者将介绍如何构建持续集成所需要的环境。
- “实现持续集成”(developerWorks,2005 年 12 月):本文是对持续集成的概念和实践的一个介绍。
- developerWorks Java 技术专区:查找数百篇有关Java 编程各方面的文章。
获得产品和技术
- 下载 IBM 软件试用版,体验强大的 DB2®,Lotus®,Rational®,Tivoli®和 WebSphere®软件。
讨论
- 查看 developerWorks 博客 的最新信息。