如何使用Gradle创建包含实现依赖项的可执行fat JAR?

81

我在Gradle 4.6中有一个简单的项目,希望将其制作成可执行的JAR文件。我尝试了shadowgradle-fatjar-plugingradle-one-jarspring-boot-gradle-plugin插件,但它们都没有添加我的依赖项,这些依赖项被声明为implementation(没有使用compile)。对于gradle-one-jar插件,使用compile可以工作,但我想要使用implementation依赖项。


这个回答解决了你的问题吗?使用Gradle创建可运行的JAR - Mahozad
8个回答

127
您可以使用以下代码。
jar {
    manifest {
        attributes(
                'Main-Class': 'com.package.YourClass'
        )
    }
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
 }

请确保将com.package.YourClass替换为包含static void main( String args[] )方法的完全限定类名。

这将打包运行时依赖项。如果需要更多信息,请查看文档


1
“fat jar”的名称是什么,我应该在哪个目录下找到它? - Vijay Kumar
1
@VijayKumar 我的在 build/libs 目录下。 - Shayan Toqraee
7
您可以使用configurations.compileClasspath.filter{ it.exists() }.collect { it.isDirectory() ? it : zipTree(it) }。这样,如果您有不存在的类路径条目,它也不会卡住。我正在使用Kotlin,因此java源代码类路径条目没有任何内容,导致出现了一些问题。 - Alex Ives
当我使用这段代码时,所有库类都包含在uberjar中,但源目录中的类却没有被包含。 - user64141
1
男士,你节省了我很多时间。网络上有一个教程,但它有缺陷 :) - Gabor Somogyi
显示剩余10条评论

31

根据被接受的答案,我需要再添加一行代码:

task fatJar(type: Jar) {
  manifest {
    attributes 'Main-Class': 'com.yourpackage.Main'
  }
  archiveClassifier = "all"
  from {
    configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
    configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
  with jar
}

没有这个额外的行,它会忽略我的源文件,只添加依赖项:

configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }

对于较新的Gradle(7+),您可能会看到此错误:

Execution failed for task ':fatJar'.
> Entry [some entry here] is a duplicate but no duplicate handling strategy has been set. Please 
refer to https://docs.gradle.org/7.1/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:duplicatesStrategy
 for details.

如果发生这种情况,请在fatJar任务中添加一个duplicatesStrategy,例如duplicatesStrategy "exclude"

同样地,在 Gradle 7+ 中,您只需删除configuration.compile.collect行,因为它不再是此版本Gradle中的有效配置。


这个方法在Gradle 6.0.1中奏效了,而其他所有方法都没有。奇怪的是,在Gradle 4.x中似乎不需要第二行代码,但是一旦升级到Gradle 6或5,我的旧构建配置突然停止创建fat JARs。 - Kristopher Noronha
4
Gradle 6.5 报告以下警告:编译配置已经被弃用。在 Gradle 7.0 中将会发生错误。请改用 "compileClasspath" 来解决问题。将 "compile" 替换为 "compileClasspath" 可以解决这个问题。 - ocroquette
工作得非常完美,不确定你是如何找到这个的,但你正在做上帝的工作!! - j bel
@jbel 谢谢!我通过结合另一篇帖子的信息偶然发现了它。 - JohnP2
对我来说有点奇怪,尝试运行一个使用gradle wrapper设置的github项目,并且一直收到依赖项的"classdefnotfoundexception"错误。我检查了子模块中的gradle.build文件,并发现依赖项是使用"implementation"关键字导入的(而不是"compile")。一旦我为jar任务添加了"configuration.runtimeClasspath"行,问题就得到解决了。 - C Murphy

15

可以使用Gradle Kotlin DSL以类似的方式完成相同的任务:

val jar by tasks.getting(Jar::class) {
    manifest {
        attributes["Main-Class"] = "com.package.YourClass"
    }

    from(configurations
        .runtime
        // .get() // uncomment this on Gradle 6+
        // .files
        .map { if (it.isDirectory) it else zipTree(it) })
}

8

之前的回答现在有点过时了,这里有一个使用Gradle-7.4有效的方法:如何使用Gradle Kotlin脚本创建Fat JAR?

tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    val dependencies = configurations
        .runtimeClasspath
        .get()
        .map(::zipTree) // OR .map { zipTree(it) }
    from(dependencies)
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

6

在这里,我为 Kotlin DSL(build.gradle.kts)提供解决方案。
请注意,前三种方法修改了Gradle的现有Jar任务。

方法1:将库文件放置在结果JAR旁边

此方法不需要application或任何其他插件。

tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    manifest.attributes["Class-Path"] = configurations
        .runtimeClasspath
        .get()
        .joinToString(separator = " ") { file ->
            "libs/${file.name}"
        }
}

注意Java要求我们在Class-Path属性中使用相对URL。因此,我们不能使用Gradle依赖项的绝对路径(这也容易更改并且无法在其他系统上使用)。如果要使用绝对路径,也许this workaround可以解决问题。
请使用以下命令创建JAR文件:
./gradlew jar

默认情况下,结果JAR将在build/libs/目录中创建。
创建JAR文件后,将库JAR文件复制到结果JAR所在的libs/子目录中。确保库JAR文件名中不含空格(它们的文件名应与任务中指定的${file.name}变量匹配)。
方法2:将库嵌入结果JAR(fat或uber JAR)
这种方法也不需要任何Gradle插件。
tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    val dependencies = configurations
        .runtimeClasspath
        .get()
        .map(::zipTree) // OR .map { zipTree(it) }
    from(dependencies)
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

创建JAR文件的方法与之前相同。
第三种方法:使用Shadow插件(创建一个大而全的JAR文件)。
plugins {
    id("com.github.johnrengelman.shadow") version "6.0.0"
}
// Shadow task depends on Jar task, so these configs are reflected for Shadow as well
tasks.jar {
    manifest.attributes["Main-Class"] = "org.example.MainKt"
}

使用此命令创建JAR文件:

./gradlew shadowJar

请参阅Shadow文档以获取有关配置插件的更多信息。

方法4:创建新任务(而不是修改Jar任务)

tasks.create("MyFatJar", Jar::class) {
    group = "my tasks" // OR, for example, "build"
    description = "Creates a self-contained fat JAR of the application that can be run."
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    val dependencies = configurations
        .runtimeClasspath
        .get()
        .map(::zipTree)
    from(dependencies)
    with(tasks.jar.get())
}

运行创建的JAR文件

java -jar my-artifact.jar

以上解决方案已经测试过,使用以下版本:

  • Java 17
  • Gradle 7.1(使用 Kotlin 1.4.31 来编写 .kts 构建脚本)

请查看官方 Gradle 文档以创建 uber (fat) JARs

有关清单的更多信息,请参阅 Oracle Java 文档:使用清单文件

有关 tasks.create()tasks.register() 之间的区别,请参见 此帖子

请注意,您的资源文件将自动包含在JAR文件中(假设它们放置在/src/main/resources/目录或任何在构建文件中设置为资源根目录的自定义目录中)。要在应用程序中访问资源文件,请使用以下代码(请注意名称开头的/):
  • Kotlin
    val vegetables = MyClass::class.java.getResource("/vegetables.txt").readText()
    // Alternative ways:
    // val vegetables = object{}.javaClass.getResource("/vegetables.txt").readText()
    // val vegetables = MyClass::class.java.getResourceAsStream("/vegetables.txt").reader().readText()
    // val vegetables = object{}.javaClass.getResourceAsStream("/vegetables.txt").reader().readText()
    
  • Java
    var stream = MyClass.class.getResource("/vegetables.txt").openStream();
    // OR var stream = MyClass.class.getResourceAsStream("/vegetables.txt");
    
    var reader = new BufferedReader(new InputStreamReader(stream));
    var vegetables = reader.lines().collect(Collectors.joining("\n"));
    

我希望能够在更新的Gradle版本上使方法1)运行起来。保留辅助JAR文件而不将所有内容压平是很好的。 - Sridhar Sarnobat

4

from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }

这行代码对我来说是至关重要的。


这里怎么去除重复项? - pavel_orekhov
如何在这里删除重复项? - undefined

1
mainClassName = 'Main'
sourceSets {
    main {
        java {
            srcDirs 'src/main/java', 'src/main/resources'
        }
    }
}
jar{
    manifest {
        attributes(
                "Main-Class": "$mainClassName",

        )

    }
    from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) 
         }
    }
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    dependsOn ('dependencies')
}

它在jdk 1.8下运行。 - garawaa garawaa

1
Kotlin 1.3.72和JVM插件,Gradle 6.5.1
所有这些平台的语法都在快速变化。
tasks {
    compileKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    compileTestKotlin {
        kotlinOptions.jvmTarget = "1.8"
    }
    val main = sourceSets.main.get()
    //TODO
    register<Jar>("buildFatJar") {
        group = "app-backend"
        dependsOn(build)
        // shouldRunAfter(parent!!.tasks["prepCopyJsBundleToKtor"]) -> This is for incorporating KotlinJS gradle subproject resulting js file.
        manifest {
            attributes["Main-Class"] = "com.app.app.BackendAppKt"
        }

        from(configurations.compileClasspath.get().files.map { if (it.isDirectory) it else zipTree(it) })
        with(jar.get() as CopySpec)
        archiveBaseName.set("${project.name}-fat")
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接