Spring Boot 的默认构建工具集中在 Maven 和 Gradle,但在一些政务内网、金融专网或老旧的持续集成环境中,构建机只允许安装 Ant,且无法随意访问 Maven 中央仓库。此时完全可以借助 Ant 加 Apache Ivy 完成构建。这里所说的“整合 Ant”并不是让 Spring Boot 放弃现有的目录结构或依赖声明,而是通过 Ant 任务完成编译、资源拷贝、依赖解析和可执行 jar 打包,最终产物与 Maven 打出的 fat jar 保持一致。

一、Spring Boot 可执行 jar 与普通 jar 的区别
普通 Java 项目的 jar 包通常只包含业务类,依赖要么通过 Class-Path 指向外部 jar,要么完全依赖运行环境提供。而 Spring Boot 的可执行 jar 会把所有依赖都打进去,形成一个可以直接通过 java -jar 启动的 fat jar。它的入口类并不是我们写的 DemoApplication,而是 Spring Boot Loader 提供的 JarLauncher,真正的业务主类则通过 Start-Class 属性指定。
要理解 Ant 打包逻辑,必须先弄清楚 fat jar 的内部结构。一个典型的 Spring Boot 可执行 jar 展开后大致如下:
spring-boot-app.jar ├─ META-INF/MANIFEST.MF ├─ BOOT-INF/classes/ # 应用自己的 class 和资源 ├─ BOOT-INF/lib/ # 所有第三方依赖 jar └─ org/springframework/boot/loader/ # Spring Boot Loader 的 class
这里最关键的目录是 BOOT-INF/classes 和 BOOT-INF/lib。JarLauncher 启动后会创建 LaunchedURLClassLoader,从 BOOT-INF/classes 加载业务类,从 BOOT-INF/lib 加载嵌套 jar。因此 Ant 打包时必须同时满足三个条件:把应用 class 放到 BOOT-INF/classes、把依赖 jar 原样放入 BOOT-INF/lib、把 Spring Boot Loader 的 class 解包到 jar 根目录。
清单文件也需要专门配置,不能只写 Main-Class。一个可用的 MANIFEST.MF 内容如下:
Manifest-Version: 1.0 Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.example.demo.DemoApplication
其中 Main-Class 固定指向 JarLauncher,Start-Class 才是我们的业务入口类。如果只配置了 Main-Class 而没有 Start-Class,启动时会报错,因为加载器不知道要从哪个类开始运行。
二、基于 Ivy 解析依赖并编译 Spring Boot 代码
Ant 本身没有依赖管理能力,如果只靠手工下载 jar 并放进 lib 目录,维护起来会非常痛苦。推荐在 Ant 中集成 Apache Ivy,由 Ivy 负责解析第三方依赖及其传递依赖。Ivy 可以从 Maven 仓库下载 jar,也可以配置成本地仓库或共享盘,适合内网环境。
先在项目根目录创建 ivy.xml,声明 Spring Boot 相关依赖。这里要特别注意,spring-boot-loader 通常不会作为 spring-boot-starter-web 的传递依赖出现,需要单独声明。
<ivy-module version="2.0">
<info organisation="com.example" module="demo"/>
<dependencies>
<dependency org="org.springframework.boot" name="spring-boot-starter-web" rev="2.7.18"/>
<dependency org="org.springframework.boot" name="spring-boot-loader" rev="2.7.18"/>
</dependencies>
</ivy-module>
接着在 build.xml 中引入 Ivy 任务,并编写依赖解析与编译目标。下面是一个基础版本:
<project name="spring-boot-ant" default="package" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="src.dir" value="src/main/java"/>
<property name="res.dir" value="src/main/resources"/>
<property name="lib.dir" value="build/lib"/>
<property name="classes.dir" value="build/classes"/>
<property name="dist.dir" value="dist"/>
<target name="resolve" description="解析依赖">
<ivy:retrieve pattern="${lib.dir}/[artifact]-[revision].[ext]" sync="true"/>
</target>
<target name="compile" depends="resolve">
<mkdir dir="${classes.dir}"/>
<javac srcdir="${src.dir}" destdir="${classes.dir}" includeantruntime="false" encoding="UTF-8">
<classpath>
<fileset dir="${lib.dir}" includes="*.jar"/>
</classpath>
</javac>
<copy todir="${classes.dir}">
<fileset dir="${res.dir}"/>
</copy>
</target>
</project>
如果构建机完全离线,也可以不用 Ivy,改为把所有依赖 jar 提前放入一个固定目录,比如 D:\workspace\spring-boot-ant\lib,然后让 <javac> 直接使用该目录。这种方式虽然直接,但需要保证所有运行时级传递依赖都齐全,否则打包后启动时会缺类。Ivy 的优势在于能自动解决传递关系,因此只要网络条件允许,优先推荐 Ivy 方案。
三、生成可执行 Fat Jar 的核心任务
完成编译后,下一步是最关键的打包环节。Ant 的 <jar> 任务可以把多个目录合并到一个 jar 中,并通过 prefix 属性控制目标路径。我们需要把应用 class 放入 BOOT-INF/classes,把依赖 jar 放入 BOOT-INF/lib,同时把 spring-boot-loader 解包后的 class 放到 jar 根目录。
首先增加一个解包 Loader 的目标。假设 Ivy 已经把 spring-boot-loader-2.7.18.jar 下载到 build/lib,解包时不能把它继续留在 BOOT-INF/lib 中,否则 JarLauncher 无法作为主类被找到。
<target name="prepare-loader" depends="resolve">
<mkdir dir="${build.dir}/loader"/>
<unzip dest="${build.dir}/loader" src="${lib.dir}/spring-boot-loader-2.7.18.jar"/>
</target>
<target name="package" depends="compile, prepare-loader">
<mkdir dir="${dist.dir}"/>
<manifest file="${build.dir}/MANIFEST.MF">
<attribute name="Main-Class" value="org.springframework.boot.loader.JarLauncher"/>
<attribute name="Start-Class" value="com.example.demo.DemoApplication"/>
</manifest>
<jar destfile="${dist.dir}/demo-app.jar" manifest="${build.dir}/MANIFEST.MF">
<fileset dir="${classes.dir}" prefix="BOOT-INF/classes"/>
<fileset dir="${lib.dir}" prefix="BOOT-INF/lib">
<exclude name="spring-boot-loader-*.jar"/>
</fileset>
<fileset dir="${build.dir}/loader"/>
</jar>
</target>
这个目标的执行顺序很重要:resolve 先拉取依赖,compile 编译代码并拷贝资源,prepare-loader 解包 Loader,最后由 package 统一合并成 fat jar。打包完成后,可以继续添加一个运行目标,方便本地验证。
<target name="run" depends="package">
<java jar="${dist.dir}/demo-app.jar" fork="true"/>
</target>
执行时只需要输入 ant package 或 ant run。如果 ANT_HOME 和 JAVA_HOME 配置正常,打包任务会一次性完成。对于 Windows 用户,可以把命令写成:
cd D:\workspace\spring-boot-ant ant package java -jar dist\demo-app.jar
需要注意,这里的 spring-boot-loader 版本必须与 Spring Boot 应用版本保持一致。如果项目使用 Spring Boot 3.x,而 Loader 仍然使用 2.x,启动时可能抛出 NoClassDefFoundError 或 UnsupportedClassVersionError。
四、容易踩到的坑与验证方法
在实际操作中,最常见的问题是打包成功但启动时提示 ClassNotFoundException。这类问题多数不是 Ant 命令本身有误,而是依赖没有完整放进 BOOT-INF/lib。例如某些依赖声明为 provided 或 optional,Ivy 默认可能不会把它们带入运行时目录,但这些类在启动阶段又会被加载。因此建议在 ivy.xml 中统一使用默认的 default 配置,或者显式声明 runtime 范围的依赖。
另一个容易忽视的细节是 Start-Class 的配置。它必须是全限定类名,不能只写类名,也不能写成包路径。通常 Spring Boot 应用主类位于 com.example.demo 包下,例如 com.example.demo.DemoApplication。如果这里写错,JarLauncher 会找不到 main 方法,启动直接失败。
验证 fat jar 结构时,可以使用 JDK 自带的 jar tf 命令查看内容。在 Windows 上可以结合 findstr 过滤,在 Linux 或 macOS 上使用 grep。查看依赖是否完整:
jar tf dist\demo-app.jar | findstr BOOT-INF/lib
如果输出的列表中没有包含某个运行依赖,说明 resolve 阶段遗漏了依赖声明,或者 <fileset> 排除了不该排除的 jar。排查清楚后重新执行 ant clean package,一般就能得到可独立运行的可执行 jar。把目录结构、依赖解析和 Loader 解包这三件事做对之后,Ant 构建 Spring Boot 项目并不会比 Maven 或 Gradle 更难维护。
Spring BootAntIvy修改时间:2026-09-30 07:48:19