IBM Rational Team Concert(RTC)作为软件协同开发工具,被逐渐应用在大型项目的生产过程中,维系着规模庞大的项目组织团队,有条不紊地管理每一项开发任务,从而为创造高质量的软件产品打下坚实基础。
RTC 构建模块支持项目开发小组创建构建引擎、配置构建定义。用户可以手动或者自动启动构建请求,从而快速方便地完成项目代码的构建工作。图 1 为 RTC 构建功能模块的整体结构。
图 1. RTC 构建整体结构图
RTC 构建结构中采用分层设计方式,把跟服务器打交道和跟用户打交道的配置划分为两个层次。较低层次为构建引擎。构建引擎(Build Engine)用来在构建服务器端配置参数。例如指定 Java 编译器版本、底层权限账号、构建文件空间等。这些参数主要针对于底层服务器端,因此对一般用户来说不可见,只有少数具备管理权限的用户可以访问并进行设置。构建引擎并不唯一,用户可以根据不同需要创建自己的构建引擎,比如:为不同引擎指定不同的编译器版本。或者为每个开发子项目小组创建自己的引擎,便于管理。图 2 为构建引擎的基本配置。
图 2. RTC 构建引擎基本配置
较高一层分布着大量构建定义。构建定义可以由开发人员来创建,用来记录构建需要的一些参数,这些参数面向用户而非服务器。例如:关联的 workspace,等等。图 3 为构建定义基本配置。
图 3. RTC 构建定义基本配置
预先配置好构建引擎和构建定义之后,接下来就可以由开发人员按照自身需要执行构建请求。 下图是如何执行和查看构建结果。
图 4. RTC 构建结果查看
构建引擎和构建定义为项目组的随需构建提供了方便,可以有效地将繁琐的构建工作变为一键请求或自动激活处理。但是,对于规模大、模块多的项目而言,每一次的构建往往要持续数小时。因此,还需要进一步从脚本代码的层面做出调整优化,以达到提高效率的目标。
回页首
本部分从代码层面对构建任务进行并行调优,从而提高构建速度。首先介绍 demo 项目实例,对其进行构建耗时分析。然后进行并行优化,对比前后时间花费。最后,介绍如何使用 Ant 内置并行机制实现脚本并行。针对 Ant 内置机制的不足,介绍如何进行扩展开发,从而达到我们需要的并行要求。
大型项目结构复杂,为了形象地讲解并行优化的过程及带来的好处,在本文中采用 demo 项目的方式介绍。下图为 demo 项目构建子任务以及相互依赖。
图 5. Demo 项目任务及依赖关系
如图所示,当源代码已经载入完毕,并开始构建的时候,有三个任务 B1-B3 分别用于构建各自的基本 jar 包。然后进入 war 包准备阶段。将这些基本的 jar 包分别作为相应的 war 的原材料。当所有 war 包准备完毕时,就可以进行最终 EAR 包的生成工作 E1。最终再将 EAR 包上传并部署到服务器。
图中每个任务表明了需要执行的时间,不同的任务因为工作量不同,执行时间也有差别。如果采用原始的串行执行方式,则虚线方框内所有任务的总时间为各个任务之和。其构建脚本代码如下:
清单 1. 串行方式脚本代码
<?xml version="1.0"?> <project name="ws=security" default="build-all" basedir="."> <target name="build-all"> <!—- 此处采用串行方式,依次调用各个任务。前一个任务结束之后,下一个任务才能开始执行 -> <echo message="Start to execute tasks with serial..." /> <antcall target="B1" /> <antcall target="B2" /> <antcall target="B3" /> <antcall target="W1" /> <antcall target="W2" /> <antcall target="E1" /> <echo message="Complete the execute tasks with serial..." /> </target> </project>
为了使每个任务花费相应的执行时间,采用 sleep 的方式模拟执行。例如任务 B1 的代码:
清单 2. 任务 B1 实现代码
<target name="B1"> <!—- 采用 sleep 模拟任务执行所花费的时间 -> <echo message="B1 is starting..." /> <sleep seconds="10" /> <echo message="B1 is completed." /> </target>
通过计算得知,采用串行执行的方式所花费的总时间为各个任务执行时间之和,结果为 73 秒。从打印出的日志可以看出执行过程。
图 6. 串行执行方式日志输出结果
为了减少总体执行时间,可以采用并行执行的方式。Ant 提供自带的 Parallel 方式将任务包装并行,并可以指定并行线程数。这种方式的优点是无需做任何开发,可以直接使用。缺点是并行的任务之间不能有依赖性,否则会报错。下面代码为采用这种方式之后的结果。
清单 3. 内置并行方式脚本代码
<?xml version="1.0"?> <project name="ws=security" default="build-all" basedir="."> <target name="build-all"> <!—- 此处采用内置并行方式,将同一阶段内的任务并行,不同阶段之间串行执行 -> <echo message="Start to execute tasks with embedded parallel method..." /> <!—- 阶段一设置 3 个线程,用来并行执行 B1 至 B3 任务 -> <parallel threadCount="3"> <antcall target="B1" /> <antcall target="B2" /> <antcall target="B3" /> </parallel> <!—- 阶段二设置 2 个线程,用来并行执行 W1 和 W2 任务 -> <parallel threadCount="2"> <antcall target="W1" /> <antcall target="W2" /> </parallel> <!—- 阶段三只有 E1 任务,因此无需并行执行 -> <antcall target="E1" /> <echo message="Complete the execute tasks with embedded parallel method..." /> </target> </project>
由代码可以看出,同一个并行 tag 里面包裹的任务之间不能有依赖性。所以将所有的任务分在了三个阶段执行,每个阶段的时长为最长那个任务的耗时。总时长为 43 秒。
图 7. 内置并行方式日志输出结果
为了弥补 Ant 自带并行方式的不足,可以自行设计开发,将并行引入依赖机制。主要的思想为: 为每个任务扩展前导属性列表。把该任务依赖的所有任务全部加入到前导属性列表中。例如任务关系中,W1 必须在 B1 和 B2 结束之后才能执行。因此把 B1 和 B2 的标识加入到 W1 的前导列表中。空闲进程会遍历所有待执行的任务,如果前导列表不为空,这个任务就不会被空闲的进程执行。当 B1 执行结束之后,会像所有任务发出通知。各个任务收到通知之后会把 B1 从自己的前导列表中移除。因此,当一个任务前导列表变成空时,这个任务就变成可以执行的状态。
清单 4. 任务的核心代码
import org.apache.tools.ant.Task; import org.apache.tools.ant.TaskContainer; public class ParallelTask extends Task implements TaskContainer { protected String name = null; protected String depends = null; // 依赖任务列表。存储该任务所依赖的前驱任务集合。 // 当发现一个前驱任务时,将其加入列表;当一个前驱任务完成时,将此任务从该列表删除。 // 当前驱列表为空时,说明该任务已经没有前驱依赖,可以开始执行。 protected ArrayList dependList = new ArrayList(); // 通知列表。存放依赖于本任务的后置任务。当本任务完成时,通知所有后置任务更新其前驱列表。 protected ArrayList notifyList = new ArrayList(); // 设置依赖关系。前驱任务加入前驱列表中。 public void setDepends(String depends) { this.depends = depends; if ((depends != null) && (depends.length() > 0)) { StringTokenizer st = new StringTokenizer(depends, ","); while (st.hasMoreTokens()) { String depend = StringUtil.trim(st.nextToken()); if ((depend != null) && (depend.length() > 0)) this.dependList.add(depend); } } // 从前驱列表中移除一个任务。 public void removeDepend(String task) { if (this.dependList.contains(task)) this.dependList.remove(task); } // 本任务结束时,通知所有后置任务更新其前驱列表。 public void finish() { for (int i = 0; i < this.notifyList.size(); i++) { ParallelTask notify = (ParallelTask)this.notifyList.get(i); notify.removeDepend(getName()); } } }
实际使用过程中,首先需要把 jar 包放到目录下,并且在构建脚本中指定。然后声明三个 tag。最后直接使用即可。
清单 5. 扩展并行方式脚本代码
<?xml version="1.0"?> <project name="ws=security" default="build-all" basedir="."> <!—- 指定文件路径,引入扩展代码 jar 包 -> <path id="lib.path"> <fileset dir="/web/build/lib_common" includes="parallel-build.jar"/> </path> <!—- 指定扩展对象所属类 -> <taskdef name="paraltask" classname="com.ibm.dsw.ant.taskdefs.ParallelTask" classpathref="lib.path" loaderref="lib.path.loader" /> <taskdef name="paralgroup" classname="com.ibm.dsw.ant.taskdefs.ParallelGroup" classpathref="lib.path" loaderref="lib.path.loader" /> <taskdef name="topoparal" classname="com.ibm.dsw.ant.taskdefs.TopoParallel" classpathref="lib.path" loaderref="lib.path.loader" /> <!—- 开始构建 -> <target name="build-all"> <echo message="Start to execute tasks with extended parallel method..." /> <topoparal logdir="logs"> <!—- 设置并行线程数目。登记每个任务及其所依赖的前驱任务 -> <paralgroup name="sample-group" maxThreadCount="3"> <paraltask name="B1" depends=""> <antcall target="B1" /> </paraltask> <paraltask name="B2" depends=""> <antcall target="B2" /> </paraltask> <paraltask name="B3" depends=""> <antcall target="B3" /> </paraltask> <paraltask name="W1" depends="B1,B2"> <antcall target="W1" /> </paraltask> <paraltask name="W2" depends="B2,B3"> <antcall target="W2" /> </paraltask> <paraltask name="E1" depends="W1,W2"> <antcall target="E1" /> </paraltask> </paralgroup> </topoparal> <echo message="Complete the execute tasks with extended parallel method..." /> </target> </project>
使用这种方式,由于加入了依赖,各个任务会按照配置好的依赖关系依次执行。采用这种方式与第二种相比较,W 阶段的任务无需等待所有 B 执行完毕,而是可以根据可用线程资源以及依赖完成情况执行。整个任务的执行时间为关键路径上的总时长 33 秒。
图 8. 扩展并行方式日志输出结果
三种方式的总结和比较如下表所示:
表 1. 三种方式特征比较
名称 | 实现 | 优点 | 缺点 |
---|---|---|---|
串行方式 | 最简单 | 逻辑最简单 | 大型项目耗时过长 |
内置并行方式 | 使用内置的标签实现 | 配置简单,上手快 | 定制性差,无法处理依赖关系 |
扩展并行方式 | 使用自定义的 jar 和标签实现 | 可按照要求灵活定制 | 需额外开发工作,配置比较复杂 |
在实例中的资源使用及时长:
图 9. 资源使用分配及时间花费
本部分以 demo 项目为例,详细介绍串行方式、内置并行方式和扩展并行方式的特征和使用。并对这三种方式进行总结比较。便于读者理解和使用。但是,日志中过多的信息不便于读者了解当前构建进展情况,下一步的将介绍如何自定义状态反馈。
回页首
构建日志打印出构建过程中代码执行的详细信息,但是对于大型项目,日志中过量的信息不便于用户迅速了解构建状态,也无法生成统计数据。在这一部分,将介绍如何通过脚本中进度检查点的设置,从而在构建活动里显示出自定义步骤的信息更新。
采用 startBuildActivity 与 completeBuildActivity 在特定的步骤中加入输出状态反馈定制。实例代码如下:
清单 6. 带反馈标签的脚本代码
<!—- 定义反馈标签所属类 -> <taskdef name="startBuildActivity" classname="com.ibm.team.build.ant.task.StartBuildActivityTask" /> <taskdef name="completeBuildActivity" classname="com.ibm.team.build.ant.task.CompleteBuildActivityTask" /> <!—- 带有反馈标签的元素 -> <target name="createEAR_Demo" if="build.bpeorder"> <!—- 定义反馈标签开始点,以及相关参数设置 -> <startBuildActivity label="Build Demo "repositoryAddress="${repositoryAddress}" userId="${userId}" passwordFile="${passwordFile}" activityIdProperty="build-demo" autoComplete="true" verbose="true" buildResultUUID="${buildResultUUID}"/> <echo message="Start build for createEAR_Demo.xml ${build_target}" /> <ant antfile="createEAR_ Demo.xml" target="${build_target}" /> <!—- 定义反馈标签结束点,以及相关参数设置-> <completeBuildActivity activityId="${compilingBarActivityId}" buildResultUUID="${buildResultUUID}" repositoryAddress="${repositoryAddress}" userId="${userId}" password="${password}" /> </target>
label 属性为设置该活动在面板中的显示名称。repositoryAddress 是要使用的存储库地址,该地址在启动构建引擎时作为输入参数传入。userId,passwordFile 用于访问的验证和授权,通常已经配置在构建引擎中。activityIdProperty 属性是用于识别该活动的唯一值。autoComplete 属性默认为 false,意味着需要配合 completeBuildActivity 设置结束点。当设置为 true 时,将自动标记该活动完成。触发条件是下一个对等活动开始,或者父亲活动完成,或者整个构建完成。buildResultUUID 属性用于传递结果 ID,为构建引擎内置属性值。
当构建执行时,活动面板就会显示出如下各个自定义步骤的状态信息和时间统计信息。
图 10. 活动面板中的构建状态
本部分介绍如何在构建脚本中插入自定义构建检查点,当构建执行到检查点时,活动面板状态信息就会被自动更新,从而使用户能灵活方便的了解自定义的构建反馈。
回页首
本文针对大型项目中代码量大、任务繁多、依赖关系复杂的特点,提出了将构建任务并行执行的方法,从而大大降低构建过程的时间耗费。通过详细介绍和对比串行方式、内置并行方式和扩展并行方式,使读者了解其原理和实施方法。可以根据自身项目的复杂性,选择采用相应的并行方式。此外,针对日志中细致繁杂的输出信息,本文介绍了如何在脚本中插入自定义检查点,从而实现构建状态的按需反馈。