1. 概述
JVM 语言一直标榜“一次编写,到处运行”。但真正到部署阶段,各种问题就来了。如今的项目生态高度依赖第三方库,动辄几十个依赖项,全都得正确加载进 classpath,否则应用启动直接报错 ❌。
虽然可以把所有依赖 JAR 放到一个目录下分发,但更常见的需求是:打成一个独立可执行的 JAR(俗称 fat-jar 或 uber-jar),用户只需一条命令即可运行:
java -jar my-application.jar
我们之前已经介绍过 通过专用插件实现 fat-jar 的方式。本文将探讨其他几种方案,并且全部使用 Kotlin DSL 编写构建脚本,告别 Groovy。
2. 使用 Gradle 插件构建轻量级应用
严格来说,“自执行 JAR”这个说法有点误导性。如果你希望你的程序在 Windows、Linux、macOS 上都能开箱即用,最省事的方式是使用 Gradle 官方提供的 application
插件 ✅。
plugins {
application // 启用插件
kotlin("jvm") version "1.6.0"
}
application {
mainClass.set("com.example.MainKt") // 注意:Kotlin 文件名 Main.kt 对应 MainKt 类
}
只需要指定主类(包含 main(args: Array<String>)
方法的那个类),剩下的交给插件处理。
⚠️ 注意:application
插件会自动引入 distribution
插件,后者会生成两个压缩包:TAR 和 ZIP。内容包括:
- 项目自身的 JAR
- 所有依赖 JAR(放在 lib 目录)
- 启动脚本:Linux/macOS 用 Bash 脚本,Windows 用
.bat
文件
打包命令:
./gradlew distZip
输出路径为:build/distributions/your-project-1.0.1.zip
。解压后即可运行:
unzip your-project-1.0.1.zip
./your-project-1.0.1/bin/your-project-1.0.1 # Linux/macOS
your-project-1.0.1\bin\your-project-1.0.1.bat # Windows
这种方式的优点是结构清晰、易于维护;缺点也很明显:
- 需要目标机器安装 JRE ❌
- 用户必须有解压工具(zip/tar)❌
- 分发文件多,不够“干净”
所以,如果你追求极致简洁的交付物——单文件交付,那还得靠 fat-jar。
3. 构建轻量级应用的 Fat-JAR
如果项目不打算作为其他项目的依赖库,完全可以把所有依赖打包进一个 JAR 中,实现“单文件部署”。
我们可以手动添加一个自定义任务来生成 fat-jar,无需引入额外插件(如 ShadowJar),除非你真需要“重命名依赖包名”这种高级功能(也就是 shading)。
🔍 小知识:shading 是为了防止不同版本的同一依赖冲突,比如 A 依赖 log4j 1.x,B 依赖 log4j 2.x,通过重命名其中一个避免类冲突。但普通可执行应用基本不会被别人依赖,所以通常不需要 shading。
下面是核心配置:
tasks {
val fatJar = register<Jar>("fatJar") {
dependsOn.addAll(listOf("compileJava", "compileKotlin", "processResources"))
archiveClassifier.set("standalone")
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest {
attributes(mapOf("Main-Class" to application.mainClass))
}
val sourcesMain = sourceSets.main.get()
val contents = configurations.runtimeClasspath.get()
.map { if (it.isDirectory) it else zipTree(it) } +
sourcesMain.output
from(contents)
}
build {
dependsOn(fatJar) // 构建时自动触发 fatJar 任务
}
}
关键点说明:
✅ archiveClassifier.set("standalone")
→ 输出文件名为 xxx-standalone.jar
✅ manifest
中设置 Main-Class
,确保能通过 -jar
启动
✅ configurations.runtimeClasspath.get()
获取运行时依赖,包括本地编译结果和第三方库
✅ zipTree(it)
将每个依赖 JAR 解压后合并进最终包
✅ duplicatesStrategy = EXCLUDE
处理重复资源(比如多个 META-INF/services)
构建完成后,在 build/libs/
下会生成类似 myapp-1.0.1-standalone.jar
的文件,直接运行:
java -jar myapp-1.0.1-standalone.jar
💡 踩坑提示:某些框架(如 SLF4J)会在多个 JAR 中包含相同的 META-INF/services/org.slf4j.impl.StaticLoggerBinder
文件,不设置 duplicatesStrategy
会导致构建失败。
4. Spring Boot 应用的可执行 JAR
Spring Boot 应用天生就是可执行的 ❗。它不是简单地把所有依赖 unpack 再 pack 进去,而是采用“嵌套 JAR”机制:把依赖 JAR 直接原封不动地塞进 BOOT-INF/lib/
目录下,并通过自定义 ClassLoader 动态加载。
这意味着:
- 不破坏原有 JAR 结构 ✅
- 启动速度快(不用反复解压)✅
- 支持热部署、profile 切换等特性 ✅
创建一个 Spring Boot 项目非常简单,推荐使用 Spring Initializr,选择 Kotlin、Gradle、所需 Starter(如 Web、Data JPA 等),下载即可。
构建脚本示例:
plugins {
id("org.springframework.boot") version "2.7.5"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
kotlin("jvm") version "1.7.20"
kotlin("plugin.spring") version "1.7.20"
}
// 必须确保有一个带有 @SpringBootApplication 的主类
application {
mainClass.set("com.example.DemoApplication")
}
构建命令:
./gradlew build
输出位于 build/libs/demo-0.0.1-SNAPSHOT.jar
,可直接运行:
java -jar demo-0.0.1-SNAPSHOT.jar
Spring Boot 的这套机制已经成为事实标准,几乎成为微服务部署的默认选择。
5. 总结
方案 | 适用场景 | 是否推荐 |
---|---|---|
Application Plugin + Distribution | 内部工具、需脚本控制启动参数 | ⚠️ 一般 |
自定义 Fat-JAR | 小型独立服务、单文件交付需求强 | ✅ 推荐 |
Spring Boot 可执行 JAR | Web 服务、微服务、企业级应用 | ✅✅ 强烈推荐 |
无论哪种方式,Gradle 都提供了足够灵活的 API 来满足需求。选择哪种方案,取决于你的项目类型、部署环境以及对运维复杂度的容忍程度。
📌 示例代码已托管至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/kotlin-self-executable-jar