Skip to content

Spring Boot Gradle 插件:可执行归档文件打包详解 🚀

概述

在现代 Java 应用开发中,如何将应用程序及其所有依赖项打包成一个可独立运行的文件,是每个开发者都会遇到的重要问题。Spring Boot Gradle 插件提供了强大的可执行归档文件打包功能,让我们能够轻松创建 "Fat JAR" 或 "Fat WAR",实现 java -jar 一键启动应用。

NOTE

可执行归档文件(Executable Archives)是包含应用程序代码和所有依赖项的自包含文件,可以直接通过 java -jar 命令运行,无需额外配置类路径。

为什么需要可执行归档文件? 🤔

传统部署的痛点

在没有可执行归档文件之前,Java 应用部署面临诸多挑战:

kotlin
// 1. 需要手动管理类路径
java -cp "app.jar:lib/spring-core.jar:lib/spring-boot.jar:..." com.example.Application

// 2. 依赖项分散,容易丢失
├── app.jar
├── lib/
│   ├── spring-boot-starter-web-2.7.0.jar
│   ├── tomcat-embed-core-9.0.62.jar
│   └── ... (数十个依赖文件)

// 3. 部署复杂,环境配置繁琐
kotlin
// 一个命令搞定!
java -jar my-application.jar

// 单文件包含一切
my-application.jar (包含所有依赖)
├── BOOT-INF/
│   ├── classes/          # 应用代码
│   ├── lib/             # 所有依赖
│   └── classpath.idx    # 类路径索引
├── META-INF/
│   └── MANIFEST.MF      # 启动配置
└── org/springframework/boot/loader/  # Spring Boot 加载器

Spring Boot 的解决方案

Spring Boot 通过以下机制解决了传统部署的痛点:

可执行 JAR 文件打包 📦

基础配置

Spring Boot Gradle 插件会自动创建 bootJar 任务:

kotlin
// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.20"
    kotlin("plugin.spring") version "1.9.20"
}

// bootJar 任务自动创建,无需额外配置
// 运行 ./gradlew bootJar 即可生成可执行 JAR

实际应用示例

让我们创建一个简单的 Spring Boot 应用来演示:

kotlin
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class DemoApplication

@RestController
class HelloController {
    @GetMapping("/hello")
    fun hello(): String {
        return "Hello from executable JAR! 🎉"
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args) 
}
kotlin
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.20"
    kotlin("plugin.spring") version "1.9.20"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
}

// bootJar 任务会自动配置
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    archiveFileName.set("my-awesome-app.jar") 
}

构建和运行:

bash
# 构建可执行 JAR
./gradlew bootJar

# 运行应用
java -jar build/libs/my-awesome-app.jar

# 测试应用
curl http://localhost:8080/hello
# 输出: Hello from executable JAR! 🎉

TIP

bootJar 任务会自动包含在 assemblebuild 任务中,所以运行 ./gradlew build 也会生成可执行 JAR。

可执行 WAR 文件打包 🌐

为什么需要可执行 WAR?

WAR 文件传统上需要部署到 Servlet 容器(如 Tomcat)中运行。Spring Boot 的可执行 WAR 既可以独立运行,也可以部署到外部容器:

kotlin
// build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.20"
    kotlin("plugin.spring") version "1.9.20"
    war 
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") 
}

双重部署能力

kotlin
// 作为可执行文件运行
java -jar my-app.war

// 内嵌 Tomcat 启动
// 适合开发环境和容器化部署
kotlin
// 部署到外部 Tomcat
cp my-app.war /opt/tomcat/webapps/

// Tomcat 自动解压并部署
// 适合传统企业环境

配置示例

kotlin
// 应用主类需要继承 SpringBootServletInitializer
@SpringBootApplication
class DemoApplication : SpringBootServletInitializer() {
    
    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(DemoApplication::class.java) 
    }
}

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

IMPORTANT

使用 providedRuntime 而不是 compileOnly 配置 Tomcat 依赖,因为 compileOnly 的依赖不会出现在测试类路径中,会导致 Web 集成测试失败。

可执行和普通归档文件的并存 🔄

默认行为

Spring Boot 插件默认会同时生成两种类型的归档文件:

bash
build/libs/
├── my-app.jar          # 可执行 JAR (bootJar 任务)
└── my-app-plain.jar    # 普通 JAR (jar 任务)

自定义分类器

如果你希望可执行归档文件使用分类器,而普通归档文件不使用:

kotlin
tasks.named<BootJar>("bootJar") {
    archiveClassifier.set("boot") 
}

tasks.named<Jar>("jar") {
    archiveClassifier.set("") 
}

// 结果:
// ├── my-app.jar          # 普通 JAR
// └── my-app-boot.jar     # 可执行 JAR
kotlin
tasks.named<Jar>("jar") {
    enabled = false
}

// 只生成可执行 JAR
// ├── my-app.jar          # 可执行 JAR

WARNING

在创建原生镜像时不要禁用 jar 任务,详见 Spring Boot 官方文档 #33238。

高级配置选项 ⚙️

主类配置

Spring Boot 提供多种方式配置应用主类:

kotlin
tasks.named<BootJar>("bootJar") {
    mainClass.set("com.example.MyApplication") 
}
kotlin
springBoot {
    mainClass.set("com.example.MyApplication") 
}
kotlin
plugins {
    application
}

application {
    mainClass.set("com.example.MyApplication") 
}
kotlin
tasks.named<BootJar>("bootJar") {
    manifest {
        attributes("Start-Class" to "com.example.MyApplication") 
    }
}

NOTE

如果主类是用 Kotlin 编写的,需要使用生成的 Java 类名。例如,ExampleApplication 类会生成 ExampleApplicationKt 类。如果使用了 @JvmName 注解,则使用注解指定的名称。

开发依赖包含

默认情况下,developmentOnly 配置中的依赖不会包含在可执行归档文件中:

kotlin
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    developmentOnly("org.springframework.boot:spring-boot-devtools") 
    // 默认不会包含在 bootJar 中
}

// 如果需要包含开发依赖:
tasks.named<BootWar>("bootWar") {
    classpath(configurations["developmentOnly"]) 
}

需要解压的库配置

某些库在嵌套 JAR 中无法正常工作,需要解压到临时目录:

kotlin
tasks.named<BootJar>("bootJar") {
    requiresUnpack("**/jruby-complete-*.jar") 
    
    // 或使用闭包进行更精细的控制
    requiresUnpack { element ->
        element.name.contains("problematic-library") 
    }
}

完全可执行归档文件 🖥️

什么是完全可执行?

完全可执行归档文件在 JAR 文件前添加了 Shell 脚本,使其在 Unix-like 系统上可以像普通可执行文件一样运行:

kotlin
tasks.named<BootJar>("bootJar") {
    launchScript() 
}

使用示例

bash
# 构建完全可执行 JAR
./gradlew bootJar

# 直接运行(无需 java -jar)
./build/libs/my-app.jar

# 安装为系统服务
sudo ln -s /path/to/my-app.jar /etc/init.d/my-app
sudo service my-app start

自定义启动脚本

kotlin
tasks.named<BootJar>("bootJar") {
    launchScript {
        properties(mapOf(
            "logFilename" to "my-app.log",
            "pidFilename" to "my-app.pid",
            "useStartStopDaemon" to "false"
        )) 
    }
}
kotlin
tasks.named<BootJar>("bootJar") {
    launchScript {
        script = file("src/main/scripts/custom-launch.script") 
    }
}

CAUTION

完全可执行归档文件可能与某些工具不兼容。例如,jar -xf 可能无法正确提取完全可执行的 JAR 文件。建议仅在直接执行时使用此功能。

分层 JAR 打包 🏗️

什么是分层 JAR?

分层 JAR 将应用内容按变更频率分组,优化 Docker 镜像构建的缓存效率:

默认分层配置

Spring Boot 默认定义了四个层:

kotlin
// 默认分层(按变更频率从低到高排序)
val defaultLayers = listOf(
    "dependencies",           // 稳定的第三方依赖
    "spring-boot-loader",    // Spring Boot 加载器
    "snapshot-dependencies", // 快照版本依赖  
    "application"           // 应用代码和资源
)

自定义分层配置

完整的分层配置示例
kotlin
tasks.named<BootJar>("bootJar") {
    layered {
        application {
            // Spring Boot 加载器单独分层
            intoLayer("spring-boot-loader") {
                include("org/springframework/boot/loader/**")
            }
            // 其余应用内容
            intoLayer("application")
        }
        
        dependencies {
            // 项目依赖放入应用层
            intoLayer("application") {
                includeProjectDependencies()
            }
            // 快照依赖单独分层
            intoLayer("snapshot-dependencies") {
                include("*:*:*SNAPSHOT")
            }
            // 稳定依赖
            intoLayer("dependencies")
        }
        
        // 定义层的顺序(重要!)
        layerOrder.set(listOf(
            "dependencies", 
            "spring-boot-loader", 
            "snapshot-dependencies", 
            "application"
        ))
    }
}

Docker 优化示例

使用分层 JAR 优化 Docker 镜像构建:

dockerfile
FROM openjdk:17-jdk-slim

# 提取分层内容
COPY build/libs/my-app.jar app.jar
RUN java -Djarmode=tools -jar app.jar extract --layers --destination extracted

# 按层复制(利用 Docker 层缓存)
COPY extracted/dependencies/ ./           # [!code highlight]
COPY extracted/spring-boot-loader/ ./     # [!code highlight]  
COPY extracted/snapshot-dependencies/ ./  # [!code highlight]
COPY extracted/application/ ./            # [!code highlight]

ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
bash
# 首次构建
Step 4/8 : COPY extracted/dependencies/ ./
 ---> Using cache                          # ✅ 缓存命中
Step 5/8 : COPY extracted/spring-boot-loader/ ./  
 ---> Using cache                          # ✅ 缓存命中
Step 6/8 : COPY extracted/snapshot-dependencies/ ./
 ---> Using cache                          # ✅ 缓存命中  
Step 7/8 : COPY extracted/application/ ./
 ---> 2f8a9b5c1234                        # 🔄 仅应用层重新构建

禁用分层功能

kotlin
tasks.named<BootJar>("bootJar") {
    layered {
        enabled.set(false) 
    }
    
    // 或者排除工具 JAR
    includeTools.set(false) 
}

使用 PropertiesLauncher 🔧

PropertiesLauncher 提供了更灵活的启动配置选项:

kotlin
tasks.named<BootWar>("bootWar") {
    manifest {
        attributes("Main-Class" to "org.springframework.boot.loader.launch.PropertiesLauncher") 
    }
}

通过 loader.properties 文件可以配置额外的类路径、系统属性等:

properties
# loader.properties
loader.path=lib/,file:///opt/app/lib/
loader.main=com.example.MyApplication

最佳实践总结 ✅

1. 选择合适的打包方式

选择指南

  • JAR 打包:适用于微服务、容器化部署
  • WAR 打包:需要部署到传统 Servlet 容器时使用
  • 分层 JAR:Docker 环境下推荐使用
  • 完全可执行:需要在 Linux 服务器上作为服务运行时使用

2. 依赖管理最佳实践

kotlin
dependencies {
    // 运行时依赖
    implementation("org.springframework.boot:spring-boot-starter-web")
    
    // 仅编译时需要的依赖
    compileOnly("org.springframework.boot:spring-boot-configuration-processor")
    
    // 开发时依赖(不会打包到生产环境)
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    
    // WAR 部署时由容器提供的依赖
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
}

3. 构建配置优化

kotlin
tasks.named<BootJar>("bootJar") {
    // 明确指定主类,避免自动检测的不确定性
    mainClass.set("com.example.MyApplication") 
    
    // 使用有意义的文件名
    archiveFileName.set("${project.name}-${project.version}.jar")
    
    // 启用分层以优化 Docker 构建
    layered {
        enabled.set(true)
    }
}

通过掌握 Spring Boot Gradle 插件的可执行归档文件打包功能,我们可以轻松创建适合不同部署场景的应用程序包,大大简化了 Java 应用的分发和部署过程。无论是开发环境的快速启动,还是生产环境的容器化部署,Spring Boot 都为我们提供了强大而灵活的解决方案。