posts - 56, comments - 77, trackbacks - 0, articles - 1
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Ant: 大规模应用中的应用

Posted on 2008-01-12 22:03 切尔斯基 阅读(4048) 评论(0)  编辑  收藏

Large Scale 的应用通常意味着:

  1. 目录较多, 层次较深

  2. 依赖较多, 构建脚本依赖的第三方Ant Task, 项目依赖的第三方库等

  3. 测试较多, 构建时间反馈周期较长

  4. 需要在不同操作系统上运行

  5. 需要在不同团队成员的机器上运行

  6. 由于以上原因, 导致Ant脚本较长

 

1. 目录较多, 层次较深

通常有两种风格的解决方案

  1. 一是使用Ant-Contrib中的<foreach>来遍历子目录并依次调用其中的构建脚本, 一般是缺省的target

  2. 另外一种是用Ant自身的<subant>命令来搜索构建脚本并调用特定的target

第一种方案可看作深度优先, 不同的子目录通常是系统的不同组件, 这种方式是把单个组件全部构建完再构建另外的组件

第二种方案可看作广度优先, 类似于模板方法, 要求每个子系统的构建脚本都要定义特定的约定的target, Root构建脚本会先调用所有构建脚本的某个target, 再调用另外的target

当然也可以用<foreach>来实现第二种风格

2. 依赖较多, 构建脚本依赖的第三方Ant Task, 项目依赖的第三方库等

  1. 明确区分构建脚本的依赖和你的项目的依赖. 那些第三方的Ant Task通常你只会在构建过程中用到它们, 而不会在产品中用到, 如 Checkstyle, Emma 等, 把它们放在单独的目录中, 打包时不要理会它们. 关于区分不同依赖的经验, 参考<<CruiseControl Enterprise 最佳实践 (2) : Keep your dependencies to yourself>>

  2. 使用 ivy 来管理你项目的依赖

  3. 清理不必要的依赖或干扰:

  1. 总是提供 <clean> target, 并借助工具保证在需要的时刻总是能够得到执行, 参见后面的章节

  2. 定义单独的目录来存放项目所有的输出, 通常是顶级目录下的名字叫做 target或者build或者dist之类的目录, 其内部的层次结构应该与源文件的目录结构一致

  3. 构建过程中拷贝需要的资源到上面定义的输出目录中, 而不是直接对资源操作

3. 测试较多, 构建反馈周期较长

一般你不会希望直到构建过程的末尾某个任务才失败, 这样你修正后不得不从头再跑一遍来校验, 有几种方式来来缩短反馈时间

  1. 一种是先运行最可能失败的任务, 利用前面提到的<subant><foreach>

  2. 一种是为每个任务都定义单独的target, 或用"property"来选择或忽略特定的target. 参见 target 的 if/unless 属性, 及 Condition 元素. 如用property来控制运行所有测试还是某个测试:

    <junit ...>

      <!-- ... -->

      <test name="${testcase}" if="testcase"/>

      <batchtest todir="${test.data.dir}" unless="testcase">

          <fileset dir="${test.classes.dir}" includes="**/*Test.class" />

      </batchtest>

    </junit>

另外你希望一次构建能够发现尽可能多的问题, 而不是出现第一个问题后构建就停止, 这样的话可以批量修复它们然后再重新运行一次构建, 而不是一遍一遍的运行构建来逐个找出错误

  1. 可以用一些任务提供的 haltonerror, haltonfailure, errorproperty 等来控制构建结束的时机和最后的结果, 如:

    <junit haltonerror="false" haltonfailure="false" errorProperty="test.failed" failureProperty="test.failed" ...>
        <!-- ... -->
    </junit>
    <fail if="test.failed">Tests failed.  Check log or reports for details</fail> 

4. 需要在不同操作系统上运行

  1. 利用 <exec> 的 os 或 osfamily 属性来控制不同操作系统上的行为

  2. 路径分隔符统一使用 "/"

5. 需要在不同团队成员的机器上运行, 每个人环境都不一样

其实这是配置管理的问题, 项目的所有文档和依赖, 甚至包括Ant本身都应该包含在一个单根目录下, 并且Check in到版本控制系统中. 里面所有涉及路径的地方都使用相对路径. 这样项目就可以即拷即用

但总有一些对外部环境的依赖, 这是就要借助 Ant 强大而合理的 immutable property 体系

  1. 所有可能变化的地方都使用 property 来引用

  2. 优先使用 JVM 的系统属性, 而不是环境变量

  3. 使用 user.properties 文件定义环境相关的property, 并在构建脚本中最先引入 <property file="${user.home}/user.properties" />

  4. 在引入 user.properties 之后, 为所有属性定义合理的缺省值, 以处理 user.properties 不存在不完整的情况

  5. 后面两步也可以用 <propertyfile> 来代替

一个应用就是可以在命令行传入用户名和密码而不是把它们写在脚本中, 这样就避免了安全和隐私问题

其它的例子包括用 property 来定义 test filter, 或者来定义碰到第一次错误是退出还是继续运行构建等

关于目录的处理

  1. 为根目录定义属性, 以此为起点定义子目录的属性

  2. 总是使用 ${basedir} 作为相对路径的前缀. 这样可以保证即使 Ant 的工作路径不同, 只要传递了正确的 basedir 属性, 所有的相对路径还是正确的.

  3. 使用 location 定义路径, 而不是 value. Ant会将 location 展开为绝对路径, 这样即使传递到另外的 Ant Project 中, 它的值也不会变

6. 由于以上原因, 导致Ant脚本较长

Ant不是Script Language, 你更应该像编写产品代码一样认真对待它的编写风格

  1. 前面说过通过命令行参数控制执行的target, 参数多了, 就要有 Usage, Help

    <target name="usage">
        <echo message="  Execute 'ant a' for a."/>
        <echo message="  Execute 'ant b' for b."/>
        <echo message="  Execute 'ant -Dtest.filter=**/*SeleniumTest.class' for specific test cases."/>
        <echo message="  Execute 'ant -Dsome.property=xxx' for xxx."/>
    </target> 
    <target name="help" depends="usage"/> 
  1. 模块化构建脚本, 使用 <macrodef>

  2. 抽取可复用的 macrodef 或 target 到单独的脚本中, 并在其它脚本中 import 这个可复用的文件, 这样有助于分离关注点, 使脚本更易维护

借助第三方工具来弥补Ant的局限

  1. 缺乏异常处理机制, 任何 BuildException 都会终止 Ant 的执行, 而任何 Task 都可能出现异常, 这样有些清理操作就得不到执行. 解决方案:

    a. Ant-Contrib的 <trycatch>

    b. CruiseControl 的 AntPublisher. CruiseControl定义了构建结束后的操作, 参见 <<CruiseControl Enterprise 最佳实践 (1) : Publish with a Publisher>>

  2. 缺乏依赖管理机制

    前面已经提到, 借助 ivy 来管理, ivy 已经成为了 Ant 的子项目

其它一些常用的风格

  1. Define tasks, datatypes, and properties before targets.

  2. Order attributes logically and consistently.

  3. Separate words in target names with a hyphen.

  4. Separate words with a dot (.) character in property names.

  5. Include an "all" target that builds it all.

  6. Define reusable paths.

  7. Use explicit classpaths wherever possible.

  8. Provide visual and XML test results. Use <formatter type="brief" usefile="false"/> , <formatter type="xml"/> and <junitreport>.

  9. 用以连字符(-)开头的target名字来定义无法从命令行调用的"内部"target

其它一些常用的技巧

  1. 与用户交互, 这在持续集成环境中应该不多: <input>

  2. 并发执行: <parallel>, 通常用来执行在后台运行的任务

  3. 递归的property定义. Ant 缺省并不支持 property 的递归展开, 如 ${${os}.${prop}}. 但任何事情都可以用一个额外的中间层解决, 这里可以借用的机制是 <macrodef>

Here is the macro (along lines suggested by Peter Reilly with reference to http://ant.apache.org/faq.html#propertyvalue-as-name-for-property:

<!-- Allows you define a new property with a value of ${${a}.${b}} which can't be done by the Property task alone.  -->
<macrodef name="dymanic-property">
    <attribute name="name"/>
    <attribute name="prop"/>
    <attribute name="os"/>
    <sequential>
        <property name="@{name}" value="${@{os}.@{prop}}"/>
    </sequential>
</macrodef>

这样就可以动态的定义属性

<macro.compose-property name="some.property" prop="${component} os="${targetOS}"/>

<do-something-with property="${some.property}"/>

诡异的地方

  1. spawn, 不要使用spawn=true, 使用前面的<parallel>

    缺省spawn等于false, 这种情况下Ant退出时会把所有fork的进程都杀掉. 把spawn设置成true可以令进程寿命长于Ant, 从而可以不堵塞Ant的执行而用来运行后台程序. 但会带来很多不好的地方,比如不能即时在console看到信息等. 并且寿命长于 Ant 在某些情况下不是我们期待的, 比如在 CruiseControl 的环境中运行 Ant, 如果Ant fork的进程没有随着Ant退出而退出, 那么CruiseControl会认为Ant进程还没有结束, 从而一直在那里等待而不是执行后面的 Publishers.

    <parallel>的<daemon>来运行后台程序

  2. unix上的通配符 (Windows上没问题): On Unix-like systems, wildcards are understood by shell intepreters, not by individual binary executables as /usr/bin/ls or shell scripts.

    <target name="list">
        <exec executable="sh">
        <arg value="-c"/>
        <arg value="ls /path/to/some/xml/files/*.xml"/>
        </exec>
    </target>
  3. 几个没在 jdk 文档中说明的系统属性? -Duser.country=EN -Duser.language=US

参考


只有注册用户登录后才能发表评论。


网站导航: