本文是实战每晚构建系列的第三篇,利用第二篇文章中叙述的开源技术对第一篇中的分析模型进行设计和实现。
1、构建信息显示系统的设计
这是一个典型的web应用系统,不过非常简单。根据《面向对象的系统分析和设计》所描述的,设计主要对四个部分进行描述:
- 问题域的细化:考虑将来实现语言的特性和利用某些设计模式,对分析模型进行细化,并作某些权衡。实现对未来系统"如何做事情"的描述。
- 人机界面设计:考虑和使用者的交互,对信息显示的布局和接收用户指令或数据的行为进行设计。
- 存储设计:考虑如何保存业务数据,主要考虑现在的存储方式,同时也要考虑未来存储方式的可能变化,即业务对象和存储方式的耦合性问题。
- 系统接口设计:主要考虑本系统和外在系统之间的合作协议,特别是不要忘了和操作系统或驱动程序的接口。
1.1 分析模型的细化
我们在分析模型中没有考虑实现的问题,现在我们必须细化构建信息显示系统的分析模型,使之达到可实现之地步。
根据前面velocity模版系统地介绍,我们扩充一下显示系统的类图。如下面的"显示系统设计类图"可知,由于Java语言不支持多继承,而且原来分析模型的BuildInfoLog类只有成员变量,我们把这个类改变成Java语言当中的接口来实现,我们把NightlyBuild变成了实现这个接口的一个类,因此同样拥有BuildInfoLog的数据成员。另外NightlyBuild还继承了Java.util.Hashtable类,使得其表现为一个哈西表,这个哈西表以项目的名字为Key,以该项目的构建标签列表为值。根据MVC模式的精神,NightlyBuild类是个业务对象类,应该从信息显示的格式化功能中解放出来,所以NightlyBuild类原来的list_ Buildinfo_table方法放到buildinfo_list.vm模版文件中实现。同时我们增加了一个NightlyBuildServlet类来响应构建信息关心者的访问需求,它是velocity中VelocityServlet类的子类,因此NightlyBuildServlet任MVC模式中的控制角色。
在下面的"显示系统MVC视图"中可以看出,业务对象NightlyBuild对象以nightlyBuild的名字保存在模型context中,当控制NightlybuildServlet用模版文件buildinfo_list.vm来满足构建信息关心者浏览构建信息目录的要求时,buildinfo_list.vm文件格式化保存在context中的NightlyBuild对象的信息并形成视图供浏览者浏览。
1.2 人机界面设计
人机界面设计就是设计浏览者和系统交互的方式,以及系统展示信息的布局。构建信息显示系统是个典型的web应用,浏览者首先在其浏览器中输入构建信息显示系统的URL,系统就会把构建信息目录显示给浏览者,浏览者通过点击构建信息目录页面上的超链就可以访问相应的信息。我们前面说过每个构建信息会在浏览器中自己显示自己,所以现在的任务就是设计构建信息目录显示页面。
下面的图就是构建信息目录显示页面,其中${key}表示某个项目的名字,${nightly_build_tag}为某个项目某次构建的标签。
管理日志超链的url为${logTopDir}/
构建日志超链的url为${projectLogTopDir}/${nightly_build_tag}.txt
度量信息超链的url为${logTopDir}/${nightly_build_tag}
测试记录超链的url为${testTopDir}/${nightly_build_tag}
覆盖率超链的url为${testCoverTopDir}/${nightly_build_tag}
产品超链的url为${distTopDir}/${nightly_build_tag}
每晚构建平台 | ||||||
| ||||||
${key} | ||||||
构建序列 | 构建日志 | 构建日志 | 度量信息 | 测试记录 | 覆盖率 | 产品 |
1 | 管理日志 | 构建日志 | 度量信息 | 测试记录 | 覆盖率 | 产品 |
2 |
|
|
|
|
|
|
... |
|
|
|
|
|
|
${key} | ||||||
构建序列 | 构建日志 | 构建日志 | 度量信息 | 测试记录 | 覆盖率 | 产品 |
1 | 管理日志 | 构建日志 | 度量信息 | 测试记录 | 覆盖率 | 产品 |
2 |
|
|
|
|
|
|
... |
|
|
|
|
|
|
1.3 存储设计
存储设计主要是指数据库的设计或者文件格式的设计,构建信息显示系统使用文件系统作为存储目标。构建信息显示系统的存储设计主要工作是定义分析模型中BuildInfoDir类也就是设计模型中BuildInfoDir接口的变量的值和格式。
1.3.1 BuildInfoDir接口
类名/接口名 | 构建信息存放位置接口 | 类英文名 | BuildInfoDir |
成员变量 | |||
变量名 | 变量说明 | 缺省值或值 | |
webAppDir | 是应用服务器存放web应用程序的根目录,这个值根据系统安装而定。 |
| |
nightlyWebAppName | 构建信息显示系统web应用程序的名字,同变量webAppDir一起构成了构建信息显示系统应用程序的根目录。 | "nightlybuild" | |
nightly_Build_Tags | 保存所有的构建标签,是构建信息显示系统web应用程序根目录下的一个文件的名字。 | "nightBuildLog" | |
logTopDir | 相对于显示系统应用程序的根目录下的一个目录名,保存构建管理服务运行的日志的目录 | "adminLogs" | |
statCVSTopDir | 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的项目度量结果的顶层路径 | "statCVSes" | |
projectLogTopDir | 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目构建服务实例运行日志的顶层路径 | "projectLogs" | |
testCoverTopDir | 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的测试覆盖率计算结果的顶层路径 | "testCovers" | |
distTopDir | 保存了所有应用项目的发布版的顶层路径,值和变量webAppDir相同 |
| |
testTopDir | 相对于显示系统应用程序的根目录下的一个目录名,保存了所有应用项目的测试结果的顶层路径 | "tests" |
下面是nightly_Build_Tags所指的文件格式:
每行包括一个项目构建标签,构建标签的格式为项目名-yyyymmdd_HHMMSS的格式
比如可能的文件内容如下:
nightlybuild-20030312_080100 cover-20030312_080100 nightlybuild -20030312_080100 cover-20030313_080100 |
这个文件内容表示:两个项目分别为nightlybuild和cover,他们在 2003 年 3 月 12 日 和13日的早上8点1分得到了构建。
1.4 系统接口设计
系统接口设计主要描述被设计的系统与外界系统特别是操作系统的接口的设计和描述。构建信息显示系统和构建系统的接口就是存储设计中的BuildInfoDir接口,和操作系统无接口。
2、构建系统的设计
2.1 分析模型设计
考虑到ant脚本的局限性,我们对构建系统分析模型作如下调整:
因为OSScheduler是个操作系统定时服务的脚本或配置,所以无法继承BuildInfoLog类,我们增加一个LogAdmin类用来启动和记录BuildAdmin运行的日志。为什么我们必须在OSScheduler和BuildAdmin中插入一个LogAdmin类?这是因为ant脚本无法自己记录自己的输出,必须靠调用者,我们必须使用<ant>任务来运行和记录另外一个类的输出。同样BuildAdmin会用<ant>任务来运行和记录ProjectBuild的输出日志。
LogAdmin用<ant>任务实现对BuildAdmin的单项联系;至于BuildAdmin和ProjectBuild之间的一对多的关系可用多个<ant>任务实现。由于要运行,所以类LogAdmin,BuildAdmin和ProjectBuild都必须是可运行的(runnable)。
一个项目的构建应该可以分成固定不变的脚本和项目相关脚本两部分,这个需求可以和设计模式中的模版方法模式的目的相吻合。
类ProjectBuild的test_project方法要完成下列步骤:
- 准备测试环境,包括为测试目的而进行的编译、打包、安装,其中编译和打包与具体项目有关;
- 测试,这个任务与具体项目有关;
- 生成测试报告,它与项目无关;
- 生成覆盖率信息,它也与项目无关。
类ProjectBuild的dist_project方法要完成的步骤有编译、打包和安装,安装与项目无关。这样运用了模版方法模式之后,构建系统的设计类图就变成了构建系统设计类图二所示。
下面的序列图显示了模版方法调用抽象方法的序列:
步骤1.1针对每个被管理的应用项目调用步骤 1.1.1 ;
步骤 1.1.1 .4.1 test_project是个模版方法,compile(),compiletestcases(),test_war(),test()是子类ProjectBuild要实现的方法;
步骤 1.1.1 .4.2 dist_project是个模版方法,compile(),dist_war()是子类ProjectBuild要实现的方法。
2.2 人机界面设计
不需要。
2.3 存储设计
见构建信息显示系统。
2.4 系统接口设计
构建系统和操作系统的接口在OSScheduler。在Linux下可以实现成一个调用ant LogAdmin的shell 可执行文件,并配置crond每晚某个时刻执行这个可执行文件。
3、实现
在这节中充分利用本文章系列中篇中所有的技术,并显示了部分源代码。
3.1 部署图
在实现时,第一个要考虑的就是类如何与源文件对应,这些源文件又是如何组织的,表示这些信息的图表称为部署图。图表的格式不一定要很标准,这要能表达意思就行。
从每晚构建部署图可以看出,这些类被分别组织在两个不同的目录下:work_nightly和work_nightlybuild。work_nightly目录存放的是跨项目的构建信息,称为每晚构建平台构建系统中的构建管理子系统,除了包括实现BuildAdmin,BuildInfoLog和LogAdmin的源代码外,还有应用服务器目录Tomcat412,编译和测试程序时常用jar类库目录lib,测试b/s架构的程序的配置信息目录cactusconf和生成ant配置文件依赖性图解的vizant目录。work_nightlybuild是一个支持每晚构建项目的目录,在这里是构建信息显示系统项目,这个目录包括了类ProjectBuild和ProjectBuildAbstract的源代码,同时还具有一个web项目该有的的文件和目录。值得指出的是为了不至于有多份实现BuildInfoLog类的源代码,在具体项目中包括了一个指向构建管理子系统顶级目录的文件称为指针文件。类ProjectBuild、ProjectBuildAbstract和指针文件组成了构建系统中的项目构建子系统。
3.2 存储接口BuildInfoLog
文件BuildInfoLog.properties实现存储接口,内容如下:
###=================================### logTopDir= "adminLogs" statCVSTopDir="statCVSes" testTopDir="tests" distTopDir="/usr/tomcat412/webapp/" nightly_Build_Tags=" nightBuildLog" projectLogTopDir="projectLogs" testCoverTopDir="testCovers" webAppDir="/usr/tomcat412/webapp/" nightlyWebAppName="nightlybuild" ###=============================###
|
这个文件实现的目录结构如下,这个目录结构在构建信息显示系统的web应用程序中创建。
3.3 类LogAdmin
<project name="BuildAdminSystem" default="execute" basedir="." > <!-- 定义方法 --> <target name="init" > <tstamp> <format property="DSTAMP" pattern="yyyyMMdd_HHmmss" locale="cn"/> </tstamp> <!-- 继承BuildInfoLog --> <property file="BuildInfoLog.properties"/>
</target> <target name="execute" depends="init" > <echo> ########################### LogAdmin.xml:target execute the BuildAdmin.xml's output will put into file: ${webAppDir}${nightlyWebAppName}/${logTopDir}/${logNamePrefix}-${DSTAMP}.txt ########################### </echo> <!-- 跨类的方法调用ant task -->
<ant antfile="BuildAdmin.xml" inheritAll="false" output="${webAppDir}${nightlyWebAppName}/${logTopDir}/${logNamePrefix}-${DSTAMP}.txt" > <!-- 传递参数 --> <property name="tagTime" value="${DSTAMP}"/>
</ant>
</target> </project> |
3.4 类BuildAdmin
<project name="BuildAdminSystem" default="execute" basedir="." > <!-- 定义方法 --> <target name="init" > <!-- 继承BuildInfoLog --> <property file="BuildInfoLog.properties"/> <property name="cvsroot" value=":pserver:anonymous@ 10.1.36 .135:/data/src" /> <property name="buildtemp.dir" value="../buildtemp" /> <property name="lib.dir" location="lib" /> <property name="AJPPORT" value="9887" /> <property name="HTTPPORT" value="9888" /> <property name="HTTPSPORT" value="9889" />
<property name="statcvs.jar" location="${lib.dir}/statcvs- 0.1.3 .jar"/> <taskdef name="statcvs" classname="net.sf.statcvs.ant.StatCvsTask"> <classpath> <pathelement path="${statcvs.jar}"/> </classpath> </taskdef> </target> <target name="execute" > <!-- you can add another module here --> <antcall target="build_One_Module"> <param name="module" value="nightlybuild"/> </antcall> <!-- you can add another module here --> <!-- <antcall target="build_One_Module"> <param name="module" value="testcvs"/> </antcall> --> </target> <!-- 方法调用之一depends --> <target name="build_One_Module" depends="init"> <!-- 方法调用之一antcall --> <antcall target="cvs_Check_Out"> <!-- the CVS module name which includes a build.xml itself --> <!-- 传递参数 --> <param name="module" value="${module}"/> </antcall> <!-- 生成构建标签 --> <property name="nightly_Build_Tag" value="${module}-${tagTime}" /> <echo> ############################################### BuildAdmin.xml:target build_One_Module: nightly_Build_Tag is set to ${nightly_Build_Tag} ############################################### </echo> <echo> ############################################### BuildAdmin.xml:target build_One_Module: append ${nightly_Build_Tag} to file ${webAppDir}/${nightlyWebAppName}/${nightly_Build_Tags} ############################################### </echo> <!-- 记录构建标签 --> <echo append="true" file="${webAppDir}/${nightlyWebAppName}/${nightly_Build_Tags}" > ${nightly_Build_Tag} </echo> <!-- 生成项目度量信息 --> <antcall target="statCVS"> <param name="module" value="${module}"/> <param name="build_tag" value="${nightly_Build_Tag}" /> </antcall> <echo> ############################################### BuildAdmin.xml:target build_One_Module: the software is checked out in ${buildtemp.dir}/tmp/${module} let's begin to build it and the output will be put into ${webAppDir}${nightlyWebAppName}/${projectLogTopDir}/${nightly_Build_Tag}.txt ############################################### </echo>
<ant dir="${buildtemp.dir}/tmp/${module}" target="execute" inheritAll="false" output="${webAppDir}${nightlyWebAppName}/${projectLogTopDir}/${nightly_Build_Tag}.txt" > <property name="nightly_Build_Tag" value="${nightly_Build_Tag}" /> <property name="AJPPORT" value="${AJPPORT}" /> <property name="HTTPPORT" value="${HTTPPORT}" /> <property name="HTTPSPORT" value="${HTTPSPORT}" /> </ant>
<!-- <ant dir="../work_nightlybuild" antfile="ProjectBuild.xml" target="execute" inheritAll="false" output="${webAppDir}${nightlyWebAppName}/${projectLogTopDir}/${nightly_Build_Tag}.txt" > <property name="nightly_Build_Tag" value="${nightly_Build_Tag}" /> <property name="AJPPORT" value="${AJPPORT}" /> <property name="HTTPPORT" value="${HTTPPORT}" /> <property name="HTTPSPORT" value="${HTTPSPORT}" /> </ant> --> </target>
<target name="cvs_Check_Out" > <!-- cvs checkout --> <echo> ############################################### BuildAdmin.xml:target cvs_Check_Out: the software will be checked out in ${buildtemp.dir}/tmp/${module} ############################################### </echo> <delete dir="${buildtemp.dir}/tmp/${module}" /> <mkdir dir="${buildtemp.dir}/tmp"/>
<cvs cvsRoot="${cvsroot}" package="${module}" dest="${buildtemp.dir}/tmp" />
<cvs dest="${buildtemp.dir}/tmp/${module}" command="log" output="${buildtemp.dir}/tmp/${module}/cvs.log"/>
</target> <!-- 生成项目度量信息 --> <target name="statCVS" depends="init" > <echo> ############################################### BuildAdmin.xml:target statCVS: the software will be measureed in ${webAppDir}${nightlyWebAppName}/${statCVSTopDir}/${build_tag} ############################################### </echo> <delete dir="${webAppDir}${nightlyWebAppName}/${statCVSTopDir}/${build_tag}" /> <mkdir dir="${webAppDir}${nightlyWebAppName}/${statCVSTopDir}/${build_tag}"/>
<statcvs projectName="${module}" projectDirectory="${buildtemp.dir}/tmp/${module}" cvsLogFile="${buildtemp.dir}/tmp/${location}/cvs.log" outputDirectory="${webAppDir}${nightlyWebAppName}/${statCVSTopDir}/${build_tag}" />
</target> </project> |
3.5 显示界面模版
显示界面模版在velocity模版文件buildinfo_list.vm中实现。这个文件引用了nightlyfrag.vm文件,另外定义宏headerCell的文在为GlobalMacros.vm。
模版文件buildinfo_list.vm:
<html> <head> <meta http-equiv="content-type" content="text/html; charset=GB2312"> <title>每晚构建</title> <link href="css/default.css" type="text/css" rel="stylesheet"> </head> <body bgcolor="#ffffff"> <center> <table border="2" cellspacing="0" cellpadding="3" bordercolor="#000000"> <tr> <td class="page-title" bordercolor="#000000" align="left" nowrap> <font size="+2"><b> 每晚构建 </b> </font> </td> </tr> </table>
<p/> <table border="1" cellspacing="0" cellpadding="3"> #parse( "nightlyfrag.vm" )
</table> </center> </html> |
模版文件nightlyfrag.vm:
#headerCell("header-center" "序号") #headerCell("header-left" "管理日志" ) #headerCell("header-left" "构建日志" ) #headerCell("header-left" "度量信息" ) #headerCell("header-left" "测试记录" ) #headerCell("header-left" "覆盖率" ) #headerCell("header-left" "产品" )
<ul> #foreach( $key in $BuildList.keySet() ) <tr> <td colspan="10" class="title">$key</td> </tr> #set($body=$BuildList.get($key)) #foreach($entry in $body) #set($tagtime=$BuildList.getTagTime($entry)) <tr> <td class="row-center">$velocityCount </td> <td class="row-left"><a href="${BuildList.LogTopDir}/${BuildList.logNamePrefix}${tagtime}.txt" >管理日志</a></td> <td class="row-left"><a href="${BuildList.projectLogTopDir}/${entry}.txt" >构建日志</a></td> <td class="row-left"><a href="${BuildList.statCVSTopDir}/${entry}" >度量信息</a></td> <td class="row-left"><a href="${BuildList.testTopDir}/${entry}" >测试记录</a></td> <td class="row-left"><a href="${BuildList.testCoverTopDir}/${entry}" >覆盖率</a></td> <td class="row-left"><a href="/${entry}" >产品</a></td>
</tr> #end #end </ul> |
宏定义文件GlobalMacros.vm:
#macro (headerCell $classStyle $body) <td class="$classStyle"> <b> $body </b> </td> #end |
4、结语
到此所有工作结束,我们可以享受自动构建带来的效益和好处。值得提及的是保持分析、设计和实现文档的一致性非常困难而且非常重要,工作要不断地进行反复。编写良好的文档,保持优秀的写作习惯需要单位和个人共同的努力。
5、文档书写辅助工具
1. word 文档书写排版工具
2. powerpoint,图片组织绘画工具
3. visio 绘制数据流图,ER图等的工具
4. rational rose,绘制UML图形的工具
5. windows 附件中的画图来截取图片
6. 操作系统的全屏打印功能
- 进一步学习面向对象的系统分析和设计:《面向对象的系统分析和设计》Ronald J. Norman
- 《实用面向对象软件工程教程》殷人昆 田金兰 马晓勤 译
- 良好的用例编写风格可以从这里获得:《编写有效用例》 Alistair Cockburm
- 进一步理解cvs和nightlybuild技术的相关背景资料:《cvs和nightlybuild技术》 杨锦方
- cvs源代码版本系统在:http://www.cvshome.org
- statcvs 项目工作量分析工具在:http://statcvs.sf.net/
- clover测试覆盖率分析工具在: http://www.cortexebusiness.com.au/
- ant构建工具在:http://ant.apache.org
- junit单元测试工具在:http://www.junit.org
- apache web程序测试工具在:http://jakarta.apache.org/cactus/
关于作者
龚永生,热衷J2EE技术,主要研究门户系统软件。你可以通过gongys@legend.com与他联系。
地址(addr): 北京市海淀区上地信息产业基地开拓路7号联想大厦
邮编 100085
电话(tel): 010-62986638-5749
手机(mobile): 13910304330
传真(fax): 010-62975824