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


原始标题:Building a Self-Executable Jar With Gradle and Kotlin