简介:面向 Java 项目构建自动化场景的 Ant build.xml 模板,适合需要了解或快速搭建经典 Ant 构建流程的初中级开发者与构建工具学习者。压缩包仅含 1 个 xml 文件,大小约 1KB,核心即一份可直接参考的 build.xml 配置模板,围绕项目声明、属性设置、任务定义、目标组织与依赖关系展开,并体现 javac 编译、目录创建、jar 打包等常见构建环节。其价值在于提供清晰的结构骨架,便于读者对照理解 target 的 depends 顺序、属性复用方式和默认目标执行逻辑,也可作为现有 Java 工程改造或教学演示的配置参照。目前已有 1014 人学习,兼具源码参考与工具模板属性,适合用来梳理 Ant 构建脚本的编写思路并迁移到自己的项目。
1. 接手老项目那天:一个 build.xml 模板顶掉了半页 README
上个月接手一个 2013 年的老服务,代码能跑,依赖全在 lib 目录里躺着,构建入口只有根目录一个 build.xml。想改一行代码验证,得先回忆javac -cp "lib/*" -d build src/...这串东西,漏一个 jar 就是 NoClassDefFoundError。这份 build.xml 模板要解决的就是这类问题:它把编译、复制资源、打 jar、跑测试、生成版本号这些手工步骤,用 project / property / target 三层结构固化成一条ant dist就走完的流水线。适合两类人,一类是手里攥着没有构建工具的老 Java 源码、必须让它重新跑起来的维护者,另一类是厌倦了 Maven 黑盒、想看清楚「构建脚本每一步到底在干什么」的新手。下面从我实际改过的那份模板讲起,拆它的骨架、跑法和需要动手改的地方。
2. 骨架拆解:project / property / target 三层怎么搭才不打架
Ant 的构建文件里真正起作用的元素只有三种:project 定边界,property 存参数,target 存动作。听起来简单,但模板能不能长期维护,全看这三层有没有分清楚。我见过太多 build.xml 把路径写死在 target 里,换台机器就得全文搜索替换;也见过把几十个 target 用 depends 串成一张网,改一个编译参数要顺着依赖链看半天。一份合格的模板应该在「改配置」和「改流程」之间划一道清晰的线。
2.1 目录约定:src、lib、build、dist 各放什么
Ant 不像 Maven 那样强制目录结构,它只认你在 srcdir 里写什么。这意味着模板本身要承担「约定」的职责,否则每个人拉下来都会按自己的习惯摆文件。我一般固定成下面这套,跟大多数老 Java 工程的布局能对上:
demo-service/ ├── src/main/java/ # 主源码,按包名分层 ├── src/main/resources/ # 配置、SQL、模板文件,编译时要复制进 classes ├── src/test/java/ # 单元测试源码 ├── lib/ # 第三方 jar,全部平铺或按目录分,不嵌套太深 ├── build.properties # 团队公共配置,进版本库 ├── build.local.properties # 本机路径覆盖,写进 .gitignore ├── build/ # 编译产物,随时可删 └── dist/ # 最终发布包,只放要交付的东西| 目录 | 内容 | 是否入版本库 | 谁负责生成 |
|---|---|---|---|
| src/main/java | 业务源码 | 是 | 人写 |
| src/main/resources | 非 Java 资源 | 是 | 人写 |
| lib | 第三方 jar | 是(老项目常见做法) | 人工放入 |
| build | classes、测试报告 | 否 | compile / test |
| dist | 可发布 jar 或 war | 否 | dist |
把 build 和 dist 彻底分开是有原因的。build 里放的是中间态,包括一堆 .class、测试 XML 报告、临时复制的资源;dist 里只放上线要用的东西,别人拿到 dist 目录就知道该传哪几个文件。模板里所有${build.dir}和${dist.dir}的引用都指向这两个不同位置,clean 时一起删,谁也不会误删源码。
提示:lib 目录提交进版本库虽然不符合现代依赖管理习惯,但对离线环境和老项目是最省事的选择。真要改造,先保证
ant dist还能跑通,再考虑抽依赖。
2.2 property 分层:把环境相关的和项目相关的拆开
这是整个模板里最值钱的一段。Ant 的 property 有一个必须记住的特性:一旦被设置,后续再赋值不会覆盖。也就是说谁先设谁生效。很多「我明明改了配置却不生效」的怪事,根源都在这。所以加载顺序必须设计过:
<?xml version="1.0" encoding="UTF-8"?> <project name="demo-service" default="dist" basedir="."> <!-- 1. 本机覆盖文件放最前,开发者自己的 JDK 路径、私服地址写这里 --> <property file="build.local.properties"/> <!-- 2. 团队公共配置其次,进版本库,所有人共享 --> <property file="build.properties"/> <!-- 3. 环境变量,访问时加 env. 前缀,例如 ${env.JAVA_HOME} --> <property environment="env"/> <!-- 4. 兜底默认值必须放最后,前面已经设过的同名属性不会被它覆盖 --> <property name="src.dir" value="src/main/java"/> <property name="res.dir" value="src/main/resources"/> <property name="test.dir" value="src/test/java"/> <property name="lib.dir" value="lib"/> <property name="build.dir" value="build"/> <property name="classes.dir" value="${build.dir}/classes"/> <property name="test.classes" value="${build.dir}/test-classes"/> <property name="dist.dir" value="dist"/> <property name="jar.name" value="${ant.project.name}.jar"/> <property name="javac.encoding" value="UTF-8"/> <property name="javac.source" value="1.8"/> <property name="javac.target" value="1.8"/> </project>几个隐含信息值得说清楚。${ant.project.name}是 Ant 内置属性,直接等于 project 的 name 属性,用它拼 jar 名可以省掉一处硬编码。basedir="."表示所有相对路径都以 build.xml 所在目录为基准,这样从任何工作目录执行ant -f /path/to/build.xml都不会错位。命令行传参-Djavac.source=11的优先级最高,因为 Ant 在解析构建文件之前就把 -D 参数塞进属性表了,模板里的默认值自然覆盖不了它——这也是调试时最方便的开关。
如果确实需要在构建过程中强制改某个属性,Ant 1.8 之后可以给<property>加override="true",但这属于例外手段。模板设计上更推荐把可变项全部外置到 build.properties,让 build.xml 里只剩下${}引用。
2.3 target 依赖链:从 clean 到 dist 的一条主线
target 的组织原则是「一个 target 只干一件事,名字能当动词读」。模板主线上六个 target 足够覆盖日常:clean 清场,init 建目录打时间戳,compile 编主源码,test 跑测试,jar 打包,dist 收口。依赖关系写成一条链而不是一张网:
<!-- 时间戳:给 jar 的版本号和日志用,格式自己定 --> <target name="init"> <tstamp> <format property="build.stamp" pattern="yyyyMMdd.HHmm"/> </tstamp> <mkdir dir="${classes.dir}"/> <mkdir dir="${dist.dir}"/> <echo level="info">构建开始,时间戳 ${build.stamp}</echo> </target> <target name="clean" description="删除 build 和 dist 目录"> <delete dir="${build.dir}"/> <delete dir="${dist.dir}"/> </target> <target name="compile" depends="init" description="编译主源码到 build/classes"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}" encoding="${javac.encoding}" source="${javac.source}" target="${javac.target}" includeantruntime="false" debug="true" debuglevel="lines,vars,source"> <classpath> <fileset dir="${lib.dir}" includes="**/*.jar"/> </classpath> </javac> </target> <target name="dist" depends="clean,compile,jar" description="清理后重新编译并打包"/>depends="clean,compile,jar"是逗号分隔的顺序列表,Ant 会按声明顺序解析依赖,并且保证同一个 target 在一次构建会话里只执行一次。所以ant dist的实际执行顺序是 clean → init → compile → jar → dist 本身,init 虽然被 compile 依赖,但不会因为 dist 也依赖 compile 而跑两遍。这个「幂等执行」特性是 Ant 用起来比裸 shell 脚本舒服的核心原因。
description属性别省。ant -p会把所有带描述的 target 列成一张表,新同事拿到模板第一件事就是敲这个命令,比翻 README 快。没写 description 的 target 不会出现在列表里,等于隐身。
3. 跑通第一次构建:JDK、ANT_HOME 与 ant -p 的完整验证链
拿到一份 build.xml,第一步永远不是直接ant dist,而是确认环境。我见过太多人卡在「命令找不到」和「编译报版本不匹配」,最后发现是 JAVA_HOME 指到了 JRE 上,或者机器上装了三个 JDK 但 PATH 里那个是 17。把验证拆成三步,每步都有明确的可观察结果,出问题能立刻定位到哪一层。
3.1 环境准备:JAVA_HOME 指到 JRE 上是最常见的翻车点
Ant 本身是个 Java 程序,它启动要用一个 JVM,编译时又要调 javac,所以 JAVA_HOME 必须指向 JDK 根目录而不是 jre 子目录。Windows 上装 JDK 时经常被塞一个 jre 目录在旁边,环境变量指错了,ant -version照样能出版本号,一到 compile 就报Unable to find a javac compiler。
# 第一步:确认 javac 可用,这条不通后面全白搭 javac -version # 第二步:设置环境变量,路径按自己机器实际情况改 export JAVA_HOME=/opt/jdk1.8.0_202 export ANT_HOME=/opt/apache-ant-1.10.14 export PATH=$JAVA_HOME/bin:$ANT_HOME/bin:$PATH # 第三步:Ant 版本和诊断信息一起看 ant -version ant -diagnostics | head -40ant -diagnostics的输出比-version有用得多。重点看三块:java.home是不是指向 JDK,ant.home是不是你设的那个目录,以及下面 task 定义列表里 junit、junitreport、scp 这些可选任务有没有被加载。Ant 的核心任务(javac、jar、copy、delete)随发行包自带,但 junit 相关任务需要 ANT_HOME/lib 下存在对应的 ant-junit.jar,同时你的 classpath 里要能找 junit 本身的 jar。诊断信息里这几项是空的,就说明测试 target 一定跑不起来,早点补比跑到一半报错强。
注意:Ant 1.10 要求 Java 8 及以上,Ant 1.9.x 才支持更老的 JDK。老项目如果锁死在 JDK 6,模板里所有
source/target参数都要跟着降,不然 javac 直接拒绝。
3.2 最小可跑模板:二十行跑起一个 hello.jar
在啃完整模板之前,先用一份能独立跑通的最小构建文件确认环境链路。把它存成 hello-ant/build.xml,配一个 src/com/example/Main.java,就能验证从编译到打包的完整闭环:
<?xml version="1.0" encoding="UTF-8"?> <project name="hello-ant" default="dist" basedir="."> <property name="src.dir" value="src"/> <property name="classes.dir" value="build/classes"/> <property name="dist.dir" value="dist"/> <target name="clean"> <delete dir="build"/> <delete dir="dist"/> </target> <target name="compile"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}" encoding="UTF-8" source="1.8" target="1.8" includeantruntime="false" debug="true"/> </target> <target name="dist" depends="clean,compile"> <mkdir dir="${dist.dir}"/> <jar destfile="${dist.dir}/hello.jar" basedir="${classes.dir}"> <manifest> <attribute name="Main-Class" value="com.example.Main"/> <attribute name="Built-By" value="${user.name}"/> </manifest> </jar> </target> </project>includeantruntime="false"这行看着不起眼,却是模板里必须写死的一项。它控制 Ant 是否把自己的 ant.jar 加进编译 classpath。默认值是 true,会让编译产物隐式依赖 Ant 运行时;一旦你把 jar 拿到没有 Ant 的机器上跑,就可能冒出莫名其妙的NoClassDefFoundError: org/apache/tools/ant/...。显式关掉,顺带消掉那条烦人的构建警告。
执行前先敲ant -p,看到 clean、compile、dist 三个带描述的 target 列出来,说明文件解析没问题。再敲ant -v dist,-v会把每个任务的完整参数和实际执行的命令行打出来,包括 javac 最终用的那串 classpath——这一屏日志是后面排查问题的原始素材,别嫌它长。
3.3 产物验证:jar tf 和 java -jar 两步确认
构建成功不等于包能用。Ant 的 BUILD SUCCESSFUL 只代表每个任务没抛异常,不代表 jar 里该有的东西都在。模板交付前我固定跑三条命令:
# 看 jar 里到底打了哪些文件,重点确认 resources 下的配置进去了没 jar tf dist/hello.jar | head -30 # 单独把 MANIFEST 抽出来,检查 Main-Class 和 Class-Path 的写法 unzip -p dist/hello.jar META-INF/MANIFEST.MF # 真正启动一次,能跑起来才算打包成功 java -jar dist/hello.jarjar tf的输出里如果只有 .class 文件、没有 resources 目录下的 xml 或 properties,说明 jar 任务只包含了 classes.dir,资源复制那一步漏了。unzip -p看 MANIFEST 是排查启动类找不到的最快手段,Main-Class 少一个包名、Class-Path 里多一个尾部空格,都会让java -jar直接报错。第三条命令看着多余,实际上能捞出一批「编译没问题、运行环境缺配置」的问题,尤其在依赖 lib 目录的情况下。
4. 核心 target 逐段讲:javac、jar、junit 三个任务的关键参数
模板跑通之后,改动最频繁的就是这三个任务。javac 决定编不编得过,jar 决定包能不能跑,junit 决定改完敢不敢发。每个任务的参数都有几个必须理解到位的,光靠复制粘贴,遇到报错就只能猜。
4.1 javac:srcdir、destdir 与 includeantruntime
先看一个带过滤和增量控制的完整写法:
<target name="compile" depends="init" description="编译主源码"> <mkdir dir="${classes.dir}"/> <javac srcdir="${src.dir}" destdir="${classes.dir}" encoding="${javac.encoding}" source="${javac.source}" target="${javac.target}" includeantruntime="false" fork="true" memoryMaximumSize="512m" debug="true" debuglevel="lines,vars,source" failonerror="true"> <classpath> <fileset dir="${lib.dir}" includes="**/*.jar"/> </classpath> <!-- 排除掉源码目录里遗留的临时文件和包信息 --> <exclude name="**/package-info.java"/> <compilerarg value="-Xlint:unchecked"/> </javac> <!-- 资源文件不进 javac,靠 copy 手动搬进 classes,否则 jar 里会缺配置 --> <copy todir="${classes.dir}" filtering="false" overwrite="true"> <fileset dir="${res.dir}" includes="**/*"/> </copy> </target>参数逐个说。srcdir和destdir是源码根和输出根,包结构会被原样保留,com/example/Main.java编出来就是com/example/Main.class。encoding必须是源码的真实编码,模板里统一 UTF-8,避免 Windows 机器上默认 GBK 导致中文注释报 unmappable character。source和target控制字节码版本,两个要一起改,只改一个会出现「高版本语法配低版本字节码」的诡异组合。fork="true"让 javac 跑在独立进程里,配memoryMaximumSize才能让大项目编译不 OOM;不开 fork 的话内存参数会被忽略。failonerror="true"是默认值,写出来是为了表明意图——编译错就该中断,不要让它带着残缺的 classes 继续打包。
<copy>那段容易被忽略。javac 只处理 .java,resources 下的 xml、properties、ftl 不会自动进 classes 目录,不复制的话 jar 里就没有它们,程序启动读配置直接 NPE。filtering="true"可以开启占位符替换(把文件里的@VERSION@换成${build.stamp}),需要配合<filterset>用;默认关掉更安全,免得配置文件里的@被误伤。
关于增量编译要有个清醒认识:Ant 的 javac 靠源文件和 class 文件的时间戳比较来决定编不编,遇到 Git 切分支把 mtime 改乱、或者系统时钟回拨,就会出现「明明改了代码却没重新编译」的玄学现象。我的习惯是发布前一律ant clean dist,本地开发才用增量图快。
4.2 jar 与 manifest:Class-Path 生成是模板里最容易写错的一段
依赖放在 lib 目录的项目,jar 的 MANIFEST 里必须声明 Class-Path,否则java -jar找不到第三方类。手写这串路径既容易漏又容易因为换机器而失效,正确做法是用 pathconvert 从 Ant 的 path 结构自动生成:
<!-- 声明一次 classpath 引用,编译、测试、打包三处共用 --> <path id="runtime.classpath"> <fileset dir="${lib.dir}" includes="**/*.jar"/> </path> <!-- 把绝对路径列表转成 MANIFEST 需要的相对文件名列表 --> <pathconvert property="manifest.classpath" refid="runtime.classpath"> <mapper> <mapper type="flatten"/> </mapper> <map from="${basedir}${file.separator}" to=""/> </pathconvert> <target name="jar" depends="compile" description="打成可执行 jar"> <jar destfile="${dist.dir}/${jar.name}" basedir="${classes.dir}"> <manifest> <attribute name="Main-Class" value="${main.class}"/> <attribute name="Implementation-Version" value="${build.stamp}"/> <attribute name="Class-Path" value="${manifest.classpath}"/> </manifest> <!-- 资源已经复制进 classes,这里不用重复包含,避免路径叠加 --> </jar> </target><pathconvert>干的事是把path里的绝对路径列表,通过flatten映射只保留文件名,再用<map>把 basedir 前缀削掉,得到lib/a.jar lib/b.jar这种形式。MANIFEST 的 Class-Path 规范要求用空格分隔、路径相对于 jar 自身位置、不能用 Windows 的反斜杠或分号。这三点全踩过:用;拼出来的在 Linux 上直接失效,写成绝对路径的换台机器就崩,末尾多一个空格会让 JVM 去找一个名字带空格的空文件。
Main-Class取自${main.class},把它放进 build.properties 而不是写死在 build.xml 里,多模块项目每个模块填自己的入口类就行。Implementation-Version塞构建时间戳,线上出问题捞 jar 出来看一眼就知道是哪个版本,比翻发布记录快得多。
4.3 junit 与 junitreport:让测试结果从日志里解放出来
只在控制台打测试结果,出了问题没人愿意翻那几百行日志。Ant 的 junit 任务可以输出 XML,再用 junitreport 转成 HTML,产物直接放进构建目录:
<target name="test" depends="compile" description="跑单元测试并生成 HTML 报告"> <!-- 先探测 junit 在不在 classpath 里,缺了就给明确提示而不是报一堆 NoClassDefFoundError --> <available classname="org.junit.Test" property="junit.present"> <classpath> <fileset dir="${lib.dir}" includes="**/*.jar"/> </classpath> </available> <fail unless="junit.present" message="lib 目录下缺少 junit jar,测试无法执行"/> <mkdir dir="${build.dir}/test-report"/> <junit printsummary="yes" fork="yes" haltonfailure="no" dir="${basedir}"> <classpath> <pathelement location="${classes.dir}"/> <path refid="runtime.classpath"/> </classpath> <formatter type="xml"/> <batchtest todir="${build.dir}/test-report"> <fileset dir="${classes.dir}"> <include name="**/*Test.class"/> <exclude name="**/*Abstract*"/> </fileset> </batchtest> </junit> <junitreport todir="${build.dir}/test-report"> <fileset dir="${build.dir}/test-report" includes="TEST-*.xml"/> <report format="frames" todir="${build.dir}/test-report/html"/> </junitreport> </target>三个参数决定行为。fork="yes"让测试跑在独立 JVM 里,避免测试代码修改系统属性污染 Ant 进程,也让System.exit的测试不会把整个构建带走。haltonfailure="no"表示某个测试失败也继续往下跑,这样一次构建能看到全部失败项,而不是修一个跑一次。printsummary="yes"在控制台打一行汇总,方便 CI 日志里直接抓。<batchtest>按文件名模式扫 class,**/*Test.class是约定,所以测试类命名必须统一以 Test 结尾;<exclude>排除抽象基类,这类类没有可执行的测试方法,被扫到会报 initializationError。
<junitreport>依赖 XSLT 引擎,JDK 自带的内置实现一般够用;如果报告目录是空的,先确认TEST-*.xml真的生成了,再看 Ant 是不是找不到 XSLT 处理器。
5. 避坑与排查:五条踩过的报错,从现象追到根因
构建脚本的报错往往离根因隔了好几层,日志里那句红字只是最后倒下的那块骨牌。下面五条是我在模板上真踩过的,按「现象 — 原因 — 解决」记下来,遇到同类问题能省掉半天。
5.1 编译期三条:编码、ant.jar 污染、增量编译失效
现象一:编译报unmappable character for encoding GBK,但源码在 IDE 里看得好好的,中文注释也没乱。
原因:<javac>没写encoding属性时,用的是 JVM 的默认字符集。同一份 UTF-8 源码,在 Windows 开发机上按 GBK 解析就炸,在 Linux 构建机上又正常,于是变成「我这儿是好的」经典局面。
解决:模板里把encoding="UTF-8"硬编码进 javac 任务,同时统一源码文件编码和<project>声明里的 XML 编码。三处不一致就会出现同一种文件在不同环节表现不同的问题。
现象二:ant -v里看到的 javac 命令行带着一长串 Ant 自己的 jar,构建日志里还有一句warning: 'includeantruntime' was not set。
原因:includeantruntime默认 true,Ant 会把 ant.jar 及其依赖塞进编译 classpath。编译能过,但产物隐式依赖 Ant 运行时,换个环境就可能NoClassDefFoundError: org/apache/tools/ant/BuildException。
解决:所有 javac 任务显式写includeantruntime="false",需要 Ant API 的项目单独用<path>引入指定 jar,不要靠默认行为兜底。
现象三:改了一行代码,ant compile显示 BUILD SUCCESSFUL,但跑起来行为还是旧的。
原因:Ant 的增量编译靠源文件与 class 文件的 mtime 比较。Git 切分支会把文件时间重置、容器里挂载的卷时区错乱、系统时钟被 NTP 回拨,都会让 Ant 认为「class 比源码新」,直接跳过编译。
解决:发布构建一律走ant clean dist,不给增量判断留机会;本地开发如果确信改了却没重编,删掉对应 class 再跑。模板里把 dist 的 depends 写成clean,compile,jar,就是为了强制这条路径。
5.2 运行期两条:Class-Path 失效与 jar 内容缺失
现象四:java -jar app.jar报ClassNotFoundException: com.mysql.cj.jdbc.Driver,但 lib 目录里明明有这个 jar,开发时用 IDE 跑也没问题。
原因:MANIFEST 里的 Class-Path 写成了绝对路径,或者用${basedir}直接拼接导致路径带上前缀;也可能是分隔符用了分号。MANIFEST 规范只认相对 jar 位置的路径、空格分隔、正斜杠。
解决:用<pathconvert>配flatten和<map>生成,生成后一定要unzip -p app.jar META-INF/MANIFEST.MF肉眼确认一遍,那一行展开长什么样,决定了运行时能不能找到依赖。发布时 jar 和 lib 目录的相对位置必须保持和构建时一致。
现象五:程序启动报配置文件找不到,jar tf一看,resources 下的 xml 一个都没进去。
原因:javac 只处理 .java,模板里漏了把${res.dir}复制到${classes.dir}的 copy 任务,或者 copy 的includes写成了**/*.java,把资源排除掉了。
解决:在 compile 之后紧跟着做资源复制,includes="**/*"全量搬,需要过滤的个别文件再用<exclude>挑出来。验证方式固定成一条jar tf dist/app.jar | grep -c "\.xml$",数量对不上就说明复制环节有问题。
6. 二次开发:把通用模板改成自己项目的构建脚本
模板给的是骨架,落到自己项目上,改动应该尽量收敛在配置层,而不是散落到每个 target 里。我的做法是把「项目相关的值」全部塞进 build.properties,build.xml 只保留流程;多人协作时 build.local.properties 承担个人差异,进 .gitignore,谁也不用为别人的 JDK 路径改文件。
改动和验证可以按下面这张表走,每一步都有明确的观察点:
| 要改的东西 | 改哪里 | 怎么验证 |
|---|---|---|
| 源码/资源/测试目录 | build.properties 里的 src.dir、res.dir、test.dir | ant -p能列出 target,ant compile无报错 |
| JDK 版本 | javac.source / javac.target | javap -v看 class 文件的 major version |
| 入口类 | main.class | unzip -p dist/*.jar META-INF/MANIFEST.MF |
| 第三方依赖 | 往 lib 里丢 jar,或改 runtime.classpath 的 fileset | ant -v compile看实际 classpath |
| 多模块打包 | 抽公共片段 + macrodef | 每个模块单独ant -f module/build.xml dist |
多模块项目最值得做的一件事是把重复动作抽成<macrodef>,比复制粘贴强得多,也比<antcall>少一层开销——antcall 会新开一个项目上下文,属性传递容易出岔子:
<!-- 定义一个打包宏,模块名和入口类当参数传,避免每个模块抄一遍 jar 任务 --> <macrodef name="package-module"> <attribute name="module"/> <attribute name="main-class" default=""/> <sequential> <jar destfile="${dist.dir}/@{module}.jar" basedir="${build.dir}/@{module}/classes"> <manifest> <attribute name="Implementation-Version" value="${build.stamp}"/> <attribute name="Main-Class" value="@{main-class}"/> <attribute name="Class-Path" value="${manifest.classpath}"/> </manifest> </jar> <echo level="info">已打包 @{module}.jar</echo> </sequential> </macrodef> <target name="dist-all" depends="clean,compile"> <package-module module="api" main-class="com.example.api.Bootstrap"/> <package-module module="worker" main-class="com.example.worker.Bootstrap"/> </target>宏里的@{module}是 macrodef 的参数占位符,和${}的属性引用是两套机制,别混用;<attribute>的default让可选参数有兜底值。这套写法在四五个模块的项目里省下来的维护成本很可观。
如果后续打算整体迁到 Maven 或 Gradle,不用一次性推翻。先在 Ant 里用<exec executable="mvn">把新模块的构建桥接进来,老模块继续走 target,两条路并行跑一段时间,等哪边的构建日志没人看了再删。这个过渡期通常会拖上几个月,急不得。
最后说个血泪教训。有次为了赶一个补丁包,临时在 build.xml 里改了 jar 的 destfile 路径,打完包忘了改回来,第二天整条流水线的产物路径全错,排查时所有人都在看代码,没人怀疑构建脚本。从那以后我每次动完 build.xml 或 build.properties,都强制先跑一遍ant clean dist -v,把完整日志存到本地,再和上一版 diff 一下产物清单,确认只变了该变的东西才提交。构建脚本是项目的黑匣子,它出问题时你连后悔药都没地方买。希望帮到你。
本文还有配套的精品资源,点击获取