Skip to content

Spring Boot 可执行 JAR 的替代方案 🚀

前言:为什么需要替代方案?

在 Spring Boot 的世界里,可执行 JAR(Fat JAR) 是一个革命性的概念。它将应用程序及其所有依赖项打包成一个独立的 JAR 文件,让部署变得异常简单。然而,Spring Boot 默认的 Loader 机制并非万能,在某些特殊场景下可能会遇到限制。

NOTE

Spring Boot Loader 是 Spring Boot 用来加载嵌套 JAR 文件的核心机制,它让我们能够在一个 JAR 文件中包含其他 JAR 文件。

当你遇到以下情况时,可能需要考虑替代方案:

  • 需要更细粒度的类加载控制
  • 对启动性能有极致要求
  • 需要与特定的类加载器兼容
  • 遇到某些第三方库的兼容性问题

让我们深入了解这些强大的替代方案! 💪

1. Maven Shade Plugin:重新打包的艺术 🎨

核心原理

Maven Shade Plugin 采用了一种完全不同的策略:将所有依赖项解压并重新打包成一个扁平的 JAR 文件。这种方式消除了嵌套 JAR 的复杂性,创建了一个真正的"uber-jar"。

实际应用示例

xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.4.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <!-- 指定主类 -->
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.MyApplication</mainClass> 
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>
xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.4.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <!-- 处理包冲突 -->
                <relocations> 
                    <relocation>
                        <pattern>org.apache.commons</pattern>
                        <shadedPattern>shaded.org.apache.commons</shadedPattern>
                    </relocation>
                </relocations>
                
                <!-- 资源文件合并 -->
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.MyApplication</mainClass>
                    </transformer>
                    <!-- 合并 Spring 配置文件 -->
                    <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                        <resource>META-INF/spring.handlers</resource>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

Kotlin Spring Boot 应用示例

kotlin
// MyApplication.kt
@SpringBootApplication
class MyApplication

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

// 使用 Shade Plugin 后,这个应用将被打包成一个扁平的 JAR
// 所有依赖都会被直接包含在根目录下,而不是嵌套的 JAR 结构

TIP

Maven Shade Plugin 特别适合需要避免类加载器问题的场景,因为它创建的是标准的 JAR 结构。

2. JarClassLoader:轻量级的解决方案 ⚡

设计哲学

JarClassLoader 是一个专门设计用来从 JAR 文件中加载类的轻量级类加载器。它的核心思想是简单而高效,避免了复杂的嵌套结构处理。

实现示例

kotlin
// 自定义的 JarClassLoader 实现
class CustomJarClassLoader(
    private val jarFiles: List<String>,
    parent: ClassLoader? = null
) : URLClassLoader(
    jarFiles.map { File(it).toURI().toURL() }.toTypedArray(),
    parent
) {
    
    override fun findClass(name: String): Class<*> {
        return try {
            super.findClass(name) 
        } catch (e: ClassNotFoundException) {
            // 自定义类查找逻辑
            loadClassFromJars(name) 
        }
    }
    
    private fun loadClassFromJars(className: String): Class<*> {
        // 从指定的 JAR 文件中查找并加载类
        jarFiles.forEach { jarPath ->
            try {
                val jarFile = JarFile(jarPath)
                val entry = jarFile.getJarEntry("${className.replace('.', '/')}.class")
                if (entry != null) {
                    val inputStream = jarFile.getInputStream(entry)
                    val bytes = inputStream.readBytes()
                    return defineClass(className, bytes, 0, bytes.size) 
                }
            } catch (e: Exception) {
                // 继续尝试下一个 JAR 文件
            }
        }
        throw ClassNotFoundException("Class $className not found in any JAR file")
    }
}

// 使用示例
@SpringBootApplication
class MyApplication {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            // 使用自定义类加载器启动应用
            val classLoader = CustomJarClassLoader(
                listOf("lib/dependency1.jar", "lib/dependency2.jar") 
            )
            Thread.currentThread().contextClassLoader = classLoader
            
            runApplication<MyApplication>(*args)
        }
    }
}

WARNING

使用自定义类加载器时,需要特别注意类的可见性和安全性问题。

3. OneJar:一体化打包方案 📦

核心概念

OneJar 是一个专门的打包工具,它创建了一个特殊的 JAR 文件结构,其中包含一个自定义的类加载器来处理嵌套的依赖项。

配置示例

xml
<!-- Maven 配置 -->
<plugin>
    <groupId>org.dstovall</groupId>
    <artifactId>onejar-maven-plugin</artifactId>
    <version>1.4.4</version>
    <executions>
        <execution>
            <configuration>
                <mainClass>com.example.MyApplicationKt</mainClass> 
                <onejarVersion>0.97</onejarVersion>
            </configuration>
            <goals>
                <goal>one-jar</goal>
            </goals>
        </execution>
    </executions>
</plugin>
kotlin
// 应用入口点
@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    // OneJar 会自动处理类加载逻辑
    runApplication<MyApplication>(*args) 
}

IMPORTANT

OneJar 项目目前处于维护模式,建议在新项目中优先考虑其他更活跃的替代方案。

4. Gradle Shadow Plugin:Gradle 生态的明星 ⭐

为什么选择 Shadow Plugin?

Gradle Shadow Plugin 是 Gradle 生态系统中最受欢迎的"fat jar"解决方案。它提供了强大的功能和灵活的配置选项。

基础配置

kotlin
// build.gradle.kts
plugins {
    id("com.github.johnrengelman.shadow") version "8.1.1"
    kotlin("jvm")
    kotlin("plugin.spring")
    id("org.springframework.boot")
}

// Shadow 插件配置
shadowJar {
    archiveBaseName.set("my-application") 
    archiveClassifier.set("") // 移除 "-all" 后缀
    archiveVersion.set("")
    
    // 合并服务文件
    mergeServiceFiles() 
    
    // 排除不需要的文件
    exclude("META-INF/*.SF")
    exclude("META-INF/*.DSA")
    exclude("META-INF/*.RSA")
}

// 设置主类
application {
    mainClass.set("com.example.MyApplicationKt") 
}

高级配置示例

kotlin
shadowJar {
    // 解决包冲突问题
    relocate("org.apache.commons", "shadow.org.apache.commons") 
    relocate("com.google.common", "shadow.com.google.common")
    
    // 条件重定位
    relocate("org.slf4j", "shadow.org.slf4j") {
        exclude("org.slf4j.impl.*") 
    }
}
kotlin
shadowJar {
    // 排除特定依赖
    dependencies {
        exclude(dependency("org.springframework.boot:spring-boot-devtools")) 
        exclude(dependency("junit:junit"))
    }
    
    // 只包含特定依赖
    dependencies {
        include(dependency("com.fasterxml.jackson.core:.*")) 
    }
}

Kotlin Spring Boot 完整示例

kotlin
// MyApplication.kt
@SpringBootApplication
class MyApplication {
    
    @Bean
    fun commandLineRunner(): CommandLineRunner {
        return CommandLineRunner { args ->
            println("🚀 应用启动成功!使用 Shadow JAR 打包") 
            println("参数: ${args.joinToString(", ")}")
        }
    }
}

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

// 控制器示例
@RestController
class HelloController {
    
    @GetMapping("/hello")
    fun hello(): ResponseEntity<Map<String, String>> {
        return ResponseEntity.ok(
            mapOf(
                "message" to "Hello from Shadow JAR!", 
                "timestamp" to Instant.now().toString(),
                "packaging" to "Shadow Plugin"
            )
        )
    }
}

方案对比与选择指南 📊

特性Spring Boot LoaderMaven ShadeJarClassLoaderOneJarGradle Shadow
学习曲线简单中等复杂简单中等
性能良好优秀优秀良好优秀
包大小中等最小中等
兼容性最高中等中等
维护状态活跃活跃需自维护停止维护活跃

选择建议

选择指南

  • 默认选择:Spring Boot Loader(适合大多数场景)
  • 性能优先:Maven Shade Plugin 或 Gradle Shadow Plugin
  • 极简需求:自定义 JarClassLoader
  • Gradle 用户:Gradle Shadow Plugin
  • Maven 用户:Maven Shade Plugin

实际应用场景 🎯

场景一:微服务架构

kotlin
// 微服务应用,需要快速启动
@SpringBootApplication
@EnableEurekaClient
class UserServiceApplication

fun main(args: Array<String>) {
    // 使用 Shadow Plugin 减少启动时间
    runApplication<UserServiceApplication>(*args) 
}

// build.gradle.kts
shadowJar {
    // 优化微服务 JAR 大小
    minimize() 
    exclude("**/spring-boot-devtools-*.jar")
}

场景二:企业级应用

kotlin
// 企业应用,需要处理复杂的依赖关系
@SpringBootApplication
@EnableJpaRepositories
class EnterpriseApplication

fun main(args: Array<String>) {
    runApplication<EnterpriseApplication>(*args)
}
xml
<!-- Maven Shade 配置,处理企业级依赖冲突 -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <configuration>
        <relocations>
            <!-- 解决不同版本的库冲突 -->
            <relocation>
                <pattern>org.apache.commons.lang</pattern>
                <shadedPattern>shaded.commons.lang</shadedPattern>
            </relocation>
        </relocations>
    </configuration>
</plugin>

总结与最佳实践 ✅

核心要点

  1. 理解需求:根据具体的应用场景选择合适的打包方案
  2. 性能考虑:Shadow/Shade 插件通常提供更好的启动性能
  3. 兼容性优先:遇到类加载问题时,扁平化打包是最佳选择
  4. 维护成本:选择活跃维护的工具和插件

最佳实践

关键建议

  • 在项目早期就确定打包策略
  • 定期测试不同环境下的兼容性
  • 监控应用启动时间和内存使用
  • 保持构建配置的简洁性

通过理解这些替代方案,你可以根据项目的具体需求选择最适合的打包策略,确保应用在各种环境下都能稳定运行! 🎉