开发人员:Java
使用 Ant 扩展 Oracle WebLogic Workshop 10
作者:John Liptak
了解如何使用生成的和自定义的 Ant 构建文件组合来扩展 Oracle WebLogic Workshop 的内置功能。
2008 年 7 月发布
Oracle WebLogic Workshop 在版本 10 中作出了重大改进,尤其是在“设计-构建-运行”开发周期方面。Workshop 的发布功能可提高生产率,也可更好地确保开发过程的质量。 诸如 Oracle WebLogic Portal 等高级产品很难掌握,通常来说,如果没有 IDE 的帮助很难使用。
因为 Agile 软件开发而盛行的单元测试 (JUnit)、持续集成和代码覆盖工具已成为多种开发方法的业界最佳实践。
能够集成自定义的命令行 Ant 功能对许多开发团队来讲不再是可有可无。不应为使用 IDE 而放弃这些利用价值极高的工具。 答案不是选择其一而是兼而用之。
本文将向您介绍如何使用生成的和自定义的 Ant 构建文件组合来扩展 Oracle WebLogic Workshop 的内置功能。同时利用这两者的优势并不难,只需少量额外的工作、计划和一些 Ant 新特性的帮助。
对命令行使用的最低要求如下:
-
可以使用 CruiseControl。
-
单元测试的目标是测试整个应用程序而非一个项目。 除了这个 CruiseControl 必需的目标之外,最好继续遵守开发实践,即在检查源代码控制的共享分支前运行所有的单元测试。
-
无需查看生成的或自定义的构建文件的能力。
-
测试和生产是用命令行构建在专用的集成机上而非开发人员的个人工作站。 构建机上无需安装 Workshop IDE;它们仅有运时支持文件。
-
能够持续生成环境特定的文件。自动安装新的软件版本对身处外包运营环境的企业内部开发人员来说很重要。
升级到 WebLogic Workshop 10
首次启动 WebLogic Workshop 10 应用程序时,同旧的 8.1 应用程序之间的巨大差异使我必须去寻找集成构建流程的新方法。CruiseControl,尤其是 junit 任务,将是一项挑战。
使用 WebLogic Workshop 8.1 为 junit 任务设置类路径相当简单:将 ./.workshop/output/<子项目> 和所有的 jar 文件添加到类路径,各方面都会运作良好。
WebLogic Workshop 10 的类路径结构远比这复杂。图 1 说明了如何组合不同的组件以组成类路径。
图 1. 类路径组件
新结构的功能更丰富。它允许每个项目在 APP-INF/lib 中拥有一份 jar 文件的副本。也允许收集将作为 WLS 库安装的共享库。
多种方法可以解决此问题,但都有重大缺陷:
我想找到更好的方法。好消息是, 有 一个更好的方法,一旦掌握技术原理就很简单了。第 1 步是使用 Workshop 生成一组基本的 Ant build.xml 文件。我将对此过程进行详述,您可以据此生成自己的。
导出 Workshop Ant 脚本
此处介绍的是如何将项目导出到一系列 Ant 构建文件。 简言之,您需要导出的是整个应用程序的构建文件(在 ear 项目中)、相关库和 Web 项目的构建文件以及元数据文件。
为 ear 项目生成的 build.xml 文件会调用每个子项目的 build.xml 文件。将会正确构建全部必需的子项目,然后组装 ear 文件。
现在您已经获取了全部生成的 build.xml 文件。如果不喜欢扩展构建文件的明显解决方案,应该怎么做?我将使用 Ant <import> 模式进行描述。
将应用程序项目添加到工作区
同时支持命令行 Ant 和 Workshop IDE 方法的第 2 步是确保它们互不使用对方的构建文件。 我们的团队选择在工作区中的 ear 项目“上”创建应用程序项目。
创建单独的应用程序项目主要有两个原因。其一,因为 Workshop 不会为那个项目生成 build.xml 文件,因此我们不必担心 Workshop 为项目生成任何相互冲突的构建说明。其二,它是存放配置文件、WLST 域创建和配置脚本、属性文件和部署计划等的良好场所。
因为我们在不同版本的 Workshop 下开发了不同的应用程序,因此需要配置 CruiseControl 以调用 build.xml 构建脚本。与其为 Workshop 应用程序和早先基于 Eclipse 版本的 Workshop 另行配置,不如使用应用程序项目构建脚本来提供另一级别的重定向以解决冲突。
Ant <import> 解决方案
Ant 是一种常用的构建工具,和我们使用的所有其他工具一样一直在不断完善。1.6.5 版引入了新的 <import> 功能,允许您扩展现有的构建文件。此任务的一般说明是允许您重构构建文件并将共享的、不变的定义、属性和任务放入一个公共文件以便其他构建文件重复使用。
同时,我重构了一些旧代码以通过 Spring 使用相关性注入,并想到可以使用导入特性将生成的 build.xml 注入到构建流程中。
那正是我们所做的:
-
创建另一个构建文件
-
<import> Workshop 生成的构建文件:
<import file="./build.xml" />
-
使用生成的构建文件中的任何目标作为目标:
<target name="unitTest" depends="build">
<echo>Did the imported build. Moving on...</echo>
...
-
使用 Workshop 创建的任何属性和任务类型。
ear 项目
除目标名称 unitTest 和 Ant 文件名称 buildCustom.xml 以外,以下是 ear 项目生成文件的 build 目标的确切内容副本:
...
<for-each-project-in-build-order>
<if>
<not><equals arg1="${.project.name}"
arg2="${project.name}"/></not>
<then>
<antex
antfile="${.project.dir}/buildCustom.xml"
dir="${.project.dir}"
target="unitTest"
inheritUserProps="false"
inheritAll="false"
inheritRefs="false">
<property name="wl.home"
value="${wl.home}"/>
<property name="patch.home"
value="${patch.home}"/>
<property name="workspace"
value="${workspace}"/>
<property name="echo.metadata"
value="${echo.metadata}"/>
<property name="init.typedefs.executed"
value="true"/>
<propertyset>
<propertyref regex="${path.vars.regex}"/>
</propertyset>
</antex>
</then>
</if>
</for-each-project-in-build-order>
...
此任务正如看上去那样:它遍历 ear 依赖的所有项目并执行单元测试。
子项目
ear 子项目与此类似。有了调用的 unitTest 目标后,需要对其进行定义:
<target name="unitTest" depends="build">
<echo level="info" >
Did the imported build.
Moving on...
</echo>
<for-each-java-src-path>
<if>
<available file="${.java.src.dir}"/>
<then>
<echo level="info" >
JUnit tests for ${.java.src.dir}
built to directory ${.java.src.output}
</echo>
<junit printsummary="yes"
haltonfailure="on"
reloading="false">
<classpath>
<path refid="java.classpath"/>
</classpath>
<formatter type="plain"
usefile="false"/>
<formatter type="xml"/>
<batchtest
todir="../wls10app/build/unitTest">
<fileset
dir="${.java.src.output}"
includes="**/*Test.class"/>
</batchtest>
</junit>
</then>
</if>
</for-each-java-src-path>
...
返回到应用程序项目后,可以这样调用 Workshop 生成的构建目标:
<target name="build" depends="prepare">
<ant dir="${earProject.dir}"
antfile="build.xml"
target="build" inheritRefs="false" />
</target>
新创建的自定义目标为:
<target name="unitTest" depends="prepare">
<mkdir dir="./build/unitTest" />
<ant dir="${earProject.dir}"
antfile="buildCustom.xml"
target="unitTest" inheritRefs="false" />
</target>
使用 CruiseControl 连续集成
CruiseControl 是常用的开源持续集成框架。CruiseControl 会监视源代码控制系统以及执行构建、运行测试并在每次签入后发布结果。 这极大地改善了开发流程,因为任何人都可以看到源代码的状态。正因为如此,它可以同时作为正向和逆向流程控制。每个人都知道谁在签入代码、编写单元测试、中断构建和修复构建。如果没有进行代码签入或单元测试,开发人员就不能说完成了任务的 80%。
在我的工作场所中,从版本 1 开始就使用 CruiseControl。此外,我们还采用了 <发行名称>-<迭代编号>-<内部版本号码> 的构建标签模式,因此所有构建通知的组合会给出代码库变化的即时概要。CruiseControl 中内置报告服务器也提供相同的信息。
自那篇文章后,CruiseControl 又有所发展,此处给出了一个使用项目插件集成多个应用程序的示例配置文件。它允许我们一次性定义检查源代码控制中的更改、构建、测试并发布构建流程结果的一般流程。然后,我们可以通过简单地考虑各个应用程序间的差异来控制多个应用程序:
<cruisecontrol>
<property environment="env" toupper="true" />
<property name="reportdir"
value="${env.CCDIR}/webapps/cruisecontrol"/>
<property name="projectdir"
value="/projects/javaapps/${project.name}"/>
<property name="testdir"
value="${projectdir}/build/unitTest"/>
<property name="logdir"
value="/projects/javaapps/buildlogs/${project.name}" />
<property name="cvs_tag"
value="HEAD"/>
<property name="builder.name"
value="John Liptak" />
<property name="builder.email"
value="John.Liptak@companyname.com" />
<property name="builder2.email"
value="Other.Person@companyname.com" />
<property name="builder3.email"
value="Other.Person2@companyname.com" />
<plugin name="cvs"
cvsroot="your cvs root here"
module="${project.name}"
property="fileschanged"
propertyondelete="filesdelted"
tag="${cvs_tag}" />
<plugin name="ant"
antscript="/projects/javaapps/app2/buildcc.bat"
antWorkingDir="${projectdir}"
target="CCbuildAndTest"
uselogger="true"
usequiet="true">
<property name="cvs_tag" value="${cvs_tag}" />
</plugin>
<plugin name="htmlemail"
buildresultsurl="http://yourCCserver/${project.name}"
mailhost="mailgate.qintra.com"
returnaddress="${builder.email}"
returnname="${builder.name}"
subjectprefix="[BUILD ${project.name}]"
defaultsuffix="@companyname.com"
reportsuccess="always
spamwhilebroken="true"
xsldir="${reportdir}/xsl"
css="${reportdir}/css/cruisecontrol.css" />
<plugin name="currentbuildstatuslistener"
file="${logdir}/buildstatus.html"/>
<plugin name="labelincrementer" classname="net.sourceforge
.cruisecontrol.labelincrementers.CVSLabelIncrementer"/>
<plugin name="project.apptype1.type"
classname="net.sourceforge.cruisecontrol.ProjectConfig">
<bootstrappers>
<cvsbootstrapper
localWorkingCopy="/projects/javaapps/${project.name}"
file="build.xml" />
</bootstrappers>
<listeners>
<currentbuildstatuslistener/>
</listeners>
<log dir="${logdir}" encoding="ISO-8859-1">
<merge dir="${testdir}"/>
</log>
<modificationset quietperiod="60">
<cvs/>
</modificationset>
<schedule interval="3600">
<ant/>
</schedule>
<publishers>
<htmlemail logdir="${logdir}" >
<always address="${builder.email}"/>
<failure address="${builder.email}"/>
<always address="${builder2.email}"/>
<failure address="${builder2.email}"/>
</htmlemail>
</publishers>
</plugin>
<project.apptype1.type
name="app1" buildafterfailed="false">
</project.apptype1.type>
<project.apptype1.type
name="app2" buildafterfailed="false">
</project.apptype1.type>
<project.apptype1.type
name="app2" buildafterfailed="false">
</project.apptype1.type>
<project.apptype1.type
name="app3" buildafterfailed="false">
</project.apptype1.type>
<project.apptype1.type
name="app4" buildafterfailed="false">
</project.apptype1.type>
<project.apptype1.type
name="wls10app" buildafterfailed="false">
<property name="builder2.email"
value="Another.Person@companyname.com" />
<log dir="${logdir}" encoding="ISO-8859-1">
<merge dir="${projectdir}/appdir/build/unitTest" />
</log>
</project.apptype1.type>
<project.apptype1.type name="app5" buildafterfailed="false">
<property name="cvs_tag" value="branch_name" />
<property name="builder2.email"
value="John.Liptak@companyname.com" />
<log dir="${logdir}" encoding="ISO-8859-1">
<merge dir="${projectdir}/builds/unitTest" />
</log>
</project.apptype1.type>
</cruisecontrol>
Ant 任务定义的核心部分是在 CruiseControl 期望的地方为输出指定目标。在本示例中, ../wls10app/build/unitTest 是所有单元测试输出的公共位置。
合并目录很重要,因为 CruiseControl 配置中的 ${projectdir}/builds/unitTest 和构建文件中的值相匹配,所以报表中包含全部的单元测试。这允许所有子项目的单元测试从 CruiseControl 合并成一个报表。
另一个重要的配置问题是 CruiseControl 将调用的 Ant 目标 — 本例中为 CCbuildAndTest。 为此目标选择一个好位置也很重要。
生成环境特定的文件(使用 <macrodef>)
我们将通过两个步骤来生成环境特定的部署文件。因为 Ant 不允许用户重新定义已经设置的属性,所以我们将使用唯一的姓名前缀为每个支持的环境创建属性。例如, buildEnv.test1.JAVA_HOME 可能有一个值为 /opt/bea/home10.0.1/jrockit_150_11。与其在构建文件中重复分配属性、fileset 和其他变量,我们使用了 Ant 的 <macrodef> 特性:
<target name="envTokenFilters"
depends="tokenFilter">
<macrodef name="env.macro">
<attribute name="envFile" />
<attribute name="envSuffix" />
<sequential>
<!-- Define properties per environment -->
<property
name="buildTypeFile.@{envSuffix}"
location="${config.env}/@{envFile}.properties" />
<dirname
property="buildTypeFile.@{envSuffix}.dirname"
file="${buildTypeFile.@{envSuffix}}" />
<basename
property="buildTypeFile.@{envSuffix}.basename"
file="${buildTypeFile.@{envSuffix}}" />
<available
file="${buildTypeFile.@{envSuffix}}"
property="buildTypeFile.@{envSuffix}.present"/>
<property
file="${buildTypeFile.@{envSuffix}}"
prefix="buildEnv.@{envSuffix}" />
<!-- Define token filterset per environment -->
<filterset id="tokenFilter.@{envSuffix}"
begintoken="@" endtoken="@">
<filtersfile
file="${commonBuildPropsFile}"/>
<filtersfile
file="${buildTypeFile.@{envSuffix}}" />
</filterset>
<!-- Define filelist per environment -->
<filelist id="tokenFilterFiles.@{envSuffix}"
dir="${buildTypeFile.@{envSuffix}.dirname}"
files="${buildTypeFile.@{envSuffix}.basename}" />
</sequential>
</macrodef>
<env.macro envFile="devUnix" envSuffix="dev"/>
<env.macro envFile="integration" envSuffix="int"/>
<env.macro envFile="integration2" envSuffix="int2"/>
<env.macro envFile="itv" envSuffix="test"/>
<env.macro envFile="itv2" envSuffix="test2"/>
<env.macro envFile="itv3" envSuffix="test3"/>
<env.macro envFile="e2e" envSuffix="e2etest"/>
<env.macro envFile="production" envSuffix="prod"/>
</target>
随后是类似的目标(由于空间原因,移除了所有的相关性检查,但在真正的构建脚本中需要进行相关性检查),它对各个环境类型进行映射以生成输出文件:
<macrodef name="copyPropertyFiles.macro">
<attribute name="envSuffix" default=""/>
<sequential>
<!-- Perform token replacements -->
<copy overwrite="true"
todir="${dist.config}" >
<fileset
dir="${config.dir}/properties">
<patternset
refid="propertyFiles" />
</fileset>
<filterset
refid="tokenFilter@{envSuffix}" />
<mapper type="glob"
from="*"
to="*@{envSuffix}"/>
</copy>
<!-- A real build file has many copies of this
copy block. -->
</macrodef>
<copyPropertyFiles.macro/>
<copyPropertyFiles.macro envSuffix=".dev"/>
<copyPropertyFiles.macro envSuffix=".int"/>
<copyPropertyFiles.macro envSuffix=".int2"/>
<copyPropertyFiles.macro envSuffix=".test"/>
<copyPropertyFiles.macro envSuffix=".test2"/>
<copyPropertyFiles.macro envSuffix=".test3"/>
<copyPropertyFiles.macro envSuffix=".e2etest"/>
<copyPropertyFiles.macro envSuffix=".prod"/>
这样,只需构建一次就可生成所有环境特定的文件。 测试人员和操作人员会感到高兴,因为所有环境只需一次构建。
如果将同样的处理添加到 WLST 脚本以生成和配置域,则可以切身感受到无触安装过程。与手动输入不同的 WebLogic、数据库和其他证书从而使得 SOX 审计人员疲惫不堪不同,这几乎不需要手动安装。
限制条件
此解决方案有一些限制需要注意:
- 不同项目中的重复类会混淆 CruiseControl 的报告。 这并非重大限制,因为它也会混淆开发人员,因此不允许在项目中出现这种情况 。
- 全部的 JUnit 类在
Test 结束。如此选择的原因是在导航器视图中测试类会显示在各自定义的旁边。一些人喜欢对整棵源树使用相同的目录结构,这样测试类看上去像在同一个程序包中但却可以排除在生产构建外。
- Oracle 目前不支持自定义 Ant 任务的并发处理。 一些 Ant 任务认为它们可以独占访问临时目录,如果有一个以上的构建在执行就会失败,即使来自另一个工作区和/或项目。
结论
本文介绍了如何使用 Ant 的 <import> 和 <macrodef> 特性根据需要扩展 WebLogic Workshop 生成的构建文件。 <import> 技术展示了如何使用 <junit> 任务生成必要的类路径。也可以使用此技术集成其他基于类路径的处理。
在构建流程中于 ear 项目之上创建应用程序项目使我们可以集成 CruiseControl。它也为环境特定的文件(例如属性文件、WLST 脚本和部署计划)提供了生成位置。
对构建流程的谨慎设置使您可同时拥有命令行 Ant 的丰富特性集和 WebLogic Workshop 的高级 IDE 特性。
John Liptak 是 Qwest 的资深软件工程师,自 1997 年起一直从事 Java 开发。
|