Skip to content

Spring Boot 自定义构建系统支持 🛠️

概述

当我们使用 Maven、Gradle 或 Ant 之外的构建工具时,可能需要开发自己的插件来支持 Spring Boot 应用的打包。Spring Boot 提供了 spring-boot-loader-tools 库,让我们可以直接使用其核心功能来创建可执行的 JAR 文件。

NOTE

Spring Boot 的 Maven 和 Gradle 插件都使用了 spring-boot-loader-tools 来生成 JAR 文件,这意味着我们可以直接使用这个库来实现自定义构建工具的支持。

为什么需要自定义构建系统支持? 🤔

在企业开发中,我们可能会遇到以下场景:

  • 使用公司内部的构建工具
  • 需要特殊的打包流程
  • 集成到现有的 CI/CD 流水线中
  • 支持特定的部署要求

传统的 JAR 文件无法直接运行,因为它们缺少启动类和依赖管理机制。Spring Boot 通过特殊的可执行 JAR 格式解决了这个问题。

核心组件解析

1. Repackager - 重新打包工具 📦

Repackager 是 Spring Boot 提供的核心工具类,用于将普通的 JAR 或 WAR 文件转换为可执行的归档文件。

kotlin
import org.springframework.boot.loader.tools.Repackager
import java.io.File

class SimpleRepackager {
    
    fun createExecutableJar() {
        // 指定源JAR文件
        val sourceJar = File("my-app-1.0.0.jar") 
        
        // 创建Repackager实例
        val repackager = Repackager(sourceJar) 
        
        // 配置选项
        repackager.setBackupSource(false) // 不备份原文件
        
        // 执行重新打包
        repackager.repackage { callback -> 
            // 这里处理依赖库
        }
    }
}
kotlin
import org.springframework.boot.loader.tools.Repackager
import org.springframework.boot.loader.tools.LayoutFactory
import java.io.File

class AdvancedRepackager {
    
    fun createCustomExecutableJar() {
        val sourceJar = File("my-app-1.0.0.jar")
        val repackager = Repackager(sourceJar)
        
        // 设置主类(可选,会自动检测)
        repackager.setMainClass("com.example.MyApplication") 
        
        // 设置布局类型
        repackager.setLayout(LayoutFactory.Layouts.JAR) 
        
        // 是否备份原文件
        repackager.setBackupSource(true) 
        
        // 指定输出文件
        val outputFile = File("my-app-executable.jar")
        repackager.repackage(outputFile) { callback ->
            // 处理依赖库
        }
    }
}

TIP

setBackupSource(false) 可以避免创建 .original 备份文件,在 CI/CD 环境中通常设置为 false

2. Libraries - 依赖管理接口 📚

Libraries 接口用于管理应用的依赖库。不同的构建系统需要实现自己的依赖收集逻辑。

kotlin
import org.springframework.boot.loader.tools.*
import java.io.File

class CustomLibrariesProvider {
    
    // 实现依赖收集逻辑
    fun collectLibraries(callback: LibraryCallback) {
        // 收集编译时依赖
        getCompileTimeDependencies().forEach { jarFile ->
            callback.library(Library(jarFile, LibraryScope.COMPILE)) 
        }
        
        // 收集运行时依赖
        getRuntimeDependencies().forEach { jarFile ->
            callback.library(Library(jarFile, LibraryScope.RUNTIME)) 
        }
        
        // 收集提供的依赖(通常不打包)
        getProvidedDependencies().forEach { jarFile ->
            callback.library(Library(jarFile, LibraryScope.PROVIDED)) 
        }
    }
    
    private fun getCompileTimeDependencies(): List<File> {
        // 构建系统特定的实现
        return listOf(
            File("lib/spring-boot-starter-web-2.7.0.jar"),
            File("lib/jackson-core-2.13.0.jar")
        )
    }
    
    private fun getRuntimeDependencies(): List<File> {
        return listOf(
            File("lib/mysql-connector-java-8.0.28.jar")
        )
    }
    
    private fun getProvidedDependencies(): List<File> {
        return listOf(
            File("lib/tomcat-embed-core-9.0.60.jar")
        )
    }
}

IMPORTANT

不同的 LibraryScope 决定了依赖库在最终 JAR 中的位置和加载方式:

  • COMPILE: 编译和运行时都需要
  • RUNTIME: 仅运行时需要
  • PROVIDED: 由运行环境提供,不打包

3. 主类自动检测 🔍

如果没有显式指定主类,Repackager 会使用 ASM 字节码分析工具自动查找包含 public static void main(String[] args) 方法的类。

kotlin
import org.springframework.boot.loader.tools.Repackager
import java.io.File

class MainClassDetection {
    
    fun demonstrateMainClassDetection() {
        val sourceJar = File("my-app.jar")
        val repackager = Repackager(sourceJar)
        
        // 方式1:自动检测主类
        try {
            repackager.repackage { /* 依赖处理 */ }
            println("✅ 主类自动检测成功")
        } catch (e: IllegalStateException) {
            println("❌ 发现多个主类候选或未找到主类") 
        }
        
        // 方式2:显式指定主类(推荐)
        repackager.setMainClass("com.example.Application") 
        repackager.repackage { /* 依赖处理 */ }
    }
}

WARNING

当项目中存在多个包含 main 方法的类时,自动检测会失败。建议在生产环境中总是显式指定主类。

完整实现示例 🚀

让我们创建一个完整的自定义构建工具实现:

完整的自定义构建工具实现
kotlin
import org.springframework.boot.loader.tools.*
import java.io.File
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths

/**
 * 自定义Spring Boot构建工具
 * 演示如何使用spring-boot-loader-tools创建可执行JAR
 */
class CustomSpringBootBuilder {
    
    private val projectRoot: File
    private val buildDir: File
    private val dependencies: MutableList<File> = mutableListOf()
    
    constructor(projectPath: String) {
        this.projectRoot = File(projectPath)
        this.buildDir = File(projectRoot, "build")
        
        // 确保构建目录存在
        if (!buildDir.exists()) {
            buildDir.mkdirs()
        }
    }
    
    /**
     * 主要的构建方法
     */
    @Throws(IOException::class)
    fun build(): File {
        println("🚀 开始构建Spring Boot应用...")
        
        // 1. 编译源代码(这里假设已经完成)
        val sourceJar = createSourceJar()
        
        // 2. 收集依赖
        collectDependencies()
        
        // 3. 创建可执行JAR
        val executableJar = createExecutableJar(sourceJar)
        
        println("✅ 构建完成: ${executableJar.absolutePath}")
        return executableJar
    }
    
    /**
     * 创建源代码JAR(模拟)
     */
    private fun createSourceJar(): File {
        val sourceJar = File(buildDir, "app-sources.jar")
        
        // 这里应该是实际的编译和打包逻辑
        // 为了演示,我们假设已经存在
        if (!sourceJar.exists()) {
            // 创建一个空的JAR文件作为示例
            sourceJar.createNewFile()
        }
        
        return sourceJar
    }
    
    /**
     * 收集项目依赖
     */
    private fun collectDependencies() {
        println("📦 收集项目依赖...")
        
        // 这里应该是构建系统特定的依赖解析逻辑
        // 例如:解析build.gradle.kts、pom.xml等
        
        val libDir = File(projectRoot, "lib")
        if (libDir.exists()) {
            libDir.listFiles { file -> file.extension == "jar" }
                ?.forEach { dependencies.add(it) }
        }
        
        println("📚 找到 ${dependencies.size} 个依赖库")
    }
    
    /**
     * 创建可执行JAR
     */
    @Throws(IOException::class)
    private fun createExecutableJar(sourceJar: File): File {
        val outputJar = File(buildDir, "app-executable.jar")
        
        // 创建Repackager实例
        val repackager = Repackager(sourceJar) 
        
        // 配置重新打包选项
        repackager.setBackupSource(false) // 不创建备份文件
        repackager.setMainClass("com.example.Application") // 设置主类
        
        // 执行重新打包
        repackager.repackage(outputJar) { callback ->
            processLibraries(callback) 
        } 
        
        return outputJar
    }
    
    /**
     * 处理依赖库
     */
    @Throws(IOException::class)
    private fun processLibraries(callback: LibraryCallback) {
        println("🔧 处理依赖库...")
        
        dependencies.forEach { jarFile ->
            when {
                isSpringBootStarter(jarFile) -> {
                    // Spring Boot starter依赖
                    callback.library(Library(jarFile, LibraryScope.COMPILE)) 
                }
                isRuntimeDependency(jarFile) -> {
                    // 运行时依赖
                    callback.library(Library(jarFile, LibraryScope.RUNTIME)) 
                }
                isProvidedDependency(jarFile) -> {
                    // 提供的依赖(如Tomcat)
                    callback.library(Library(jarFile, LibraryScope.PROVIDED)) 
                }
                else -> {
                    // 默认为编译时依赖
                    callback.library(Library(jarFile, LibraryScope.COMPILE)) 
                }
            }
        }
    }
    
    /**
     * 判断是否为Spring Boot Starter
     */
    private fun isSpringBootStarter(jarFile: File): Boolean {
        return jarFile.name.contains("spring-boot-starter")
    }
    
    /**
     * 判断是否为运行时依赖
     */
    private fun isRuntimeDependency(jarFile: File): Boolean {
        val runtimeLibs = listOf("mysql-connector", "postgresql", "h2")
        return runtimeLibs.any { jarFile.name.contains(it) }
    }
    
    /**
     * 判断是否为提供的依赖
     */
    private fun isProvidedDependency(jarFile: File): Boolean {
        val providedLibs = listOf("tomcat-embed", "jetty", "undertow")
        return providedLibs.any { jarFile.name.contains(it) }
    }
    
    /**
     * 验证生成的JAR文件
     */
    fun validateExecutableJar(jarFile: File): Boolean {
        if (!jarFile.exists()) {
            println("❌ JAR文件不存在")
            return false
        }
        
        // 检查文件大小
        val size = jarFile.length()
        if (size == 0L) {
            println("❌ JAR文件为空")
            return false
        }
        
        println("✅ 可执行JAR验证通过 (大小: ${size / 1024}KB)")
        return true
    }
}

/**
 * 使用示例
 */
fun main() {
    try {
        val builder = CustomSpringBootBuilder("/path/to/project")
        val executableJar = builder.build()
        
        if (builder.validateExecutableJar(executableJar)) {
            println("🎉 Spring Boot应用构建成功!")
            println("💡 运行命令: java -jar ${executableJar.name}")
        }
        
    } catch (e: IOException) {
        println("❌ 构建失败: ${e.message}")
        e.printStackTrace()
    }
}

实际应用场景 🌟

1. CI/CD 集成

kotlin
class CiCdIntegration {
    
    fun buildForDeployment(
        sourceJar: File,
        environment: String,
        version: String
    ): File {
        val repackager = Repackager(sourceJar)
        
        // 根据环境设置不同的配置
        when (environment) {
            "production" -> {
                repackager.setBackupSource(false) 
                // 生产环境优化
            }
            "staging" -> {
                repackager.setBackupSource(true) 
                // 保留调试信息
            }
        }
        
        val outputJar = File("app-${environment}-${version}.jar")
        repackager.repackage(outputJar) { callback ->
            // 环境特定的依赖处理
            processEnvironmentSpecificLibraries(callback, environment)
        }
        
        return outputJar
    }
    
    private fun processEnvironmentSpecificLibraries(
        callback: LibraryCallback,
        environment: String
    ) {
        // 根据环境包含不同的依赖
        when (environment) {
            "production" -> {
                // 生产环境排除调试工具
                // 只包含必要的依赖
            }
            "development" -> {
                // 开发环境包含调试工具
                // 包含开发时依赖
            }
        }
    }
}

2. 多模块项目支持

kotlin
class MultiModuleBuilder {
    
    fun buildMultiModuleApp(modules: List<File>): File {
        // 合并多个模块的JAR
        val combinedJar = combineModules(modules)
        
        val repackager = Repackager(combinedJar)
        repackager.setMainClass("com.example.MultiModuleApplication") 
        
        val executableJar = File("multi-module-app.jar")
        repackager.repackage(executableJar) { callback ->
            // 处理所有模块的依赖
            modules.forEach { module ->
                processModuleDependencies(module, callback)
            }
        }
        
        return executableJar
    }
    
    private fun combineModules(modules: List<File>): File {
        // 实现模块合并逻辑
        return File("combined-modules.jar")
    }
    
    private fun processModuleDependencies(
        module: File,
        callback: LibraryCallback
    ) {
        // 处理单个模块的依赖
    }
}

最佳实践与注意事项 ⚡

性能优化建议

  1. 并行处理: 在处理大量依赖时,考虑使用并行流来提高性能
  2. 缓存机制: 对已处理的依赖进行缓存,避免重复处理
  3. 增量构建: 只重新打包发生变化的部分

常见陷阱

  1. 依赖冲突: 确保正确处理不同版本的同一依赖
  2. 主类检测失败: 在有多个主类时显式指定主类
  3. 内存使用: 处理大型项目时注意内存管理

安全考虑

  1. 文件权限: 确保生成的JAR文件具有正确的执行权限
  2. 路径遍历: 验证所有文件路径,防止路径遍历攻击
  3. 依赖验证: 验证依赖库的完整性和来源

总结 📝

Spring Boot 的自定义构建系统支持为我们提供了强大的灵活性,让我们能够:

  1. 🔧 集成任何构建工具: 通过 spring-boot-loader-tools
  2. 📦 自定义打包流程: 根据特定需求调整打包策略
  3. 🚀 优化部署流程: 创建适合不同环境的可执行JAR
  4. 🔍 智能依赖管理: 自动处理复杂的依赖关系

通过理解和掌握这些核心概念,我们可以构建出更加灵活和强大的Spring Boot应用构建流程,满足企业级开发的各种需求。

NOTE

记住,spring-boot-loader-tools 是Spring Boot官方提供的核心工具,Maven和Gradle插件都是基于它构建的。理解它的工作原理,就能够创建出适合任何构建环境的解决方案。