使用Gradle创建可运行的JAR文件

194

之前我是通过Eclipse的“导出”功能创建可运行的JAR文件,但现在我已经切换到了IntelliJ IDEA和Gradle用于构建自动化。

这里有一些文章建议使用“应用程序”插件,但这并没有完全达到我期望的结果(只是一个JAR文件,没有启动脚本或类似的东西)。

如何才能达到与Eclipse的“导出”对话框相同的结果?

12个回答

203

可执行的jar文件只是一个包含其清单中的Main-Class条目的jar文件。因此,您只需要配置jar任务以添加此条目到其清单中:

jar {
    manifest {
        attributes 'Main-Class': 'com.foo.bar.MainClass'
    }
}

你可能还需要在清单文件中添加classpath条目,但这将以同样的方式完成。
参见http://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html
如果您已经定义了应用程序上下文,可以重新使用该定义而不是复制它:
application {
  // Define the main class for the application.
  mainClass = 'com.foo.bar.MainClass'
}

jar {
  manifest {
    attributes 'Main-Class': application.mainClass
  }
}

6
这似乎就是我在寻找的东西;但是:我已经在 build.gradle 中声明了依赖关系,我真的需要手动添加类路径吗,还是可以重用我的依赖声明? - Hannes
你应该能够循环遍历“runtime”配置中的库,并将它们连接起来以创建Class-Path属性值。 - JB Nizet
4
“在'runtime'配置中”是什么意思?抱歉问一些愚蠢的问题,因为我对Gradle还很陌生... - Hannes
啊,谢谢 - 那我理解得没错,成功构建了JAR,现在我正在折腾创建类路径;-)非常感谢你的帮助! - Hannes
1
将清单属性添加到文件中,仅添加 chmod +x 属性是无法使 JAR 文件可执行的,绝对不同于 Eclipse 命令 导出 执行的操作。 - Dims
显示剩余3条评论

118

JB Nizet和Jorge_B的回答都是正确的。

简单来说,使用Gradle创建可执行的JAR只需要向manifest中添加适当的条目即可。然而,在实践中,由于常常需要包含在类路径中的依赖项,因此这种方法会变得比较棘手。

application plugin提供了一种替代方法;它不是创建一个可执行的JAR,而是提供:

  • run任务,以便轻松地直接从构建中运行应用程序
  • installDist任务生成了一个目录结构,包括已构建的JAR以及所有它所依赖的JAR和启动脚本,将它们组合成一个可运行的程序
  • distZipdistTar任务创建包含完整应用程序分发(启动脚本和JAR文件)的存档文件

第三种方法是创建所谓的“fat JAR”,它是一个可执行的JAR,不仅包含您组件的代码,还包括其所有依赖项。有几个不同的插件使用这种方法。我已经包含了我知道的一些链接;我相信还有更多。


刚试了一下 Shadow 和 One-Jar。我会坚持使用 One-Jar:它更简单易用。太棒了!谢谢。 - Jako
遗憾的是,One-Jar 无法与最新版本的 Gradle 兼容。例如,请参见 https://github.com/rholder/gradle-one-jar/issues/34 - pharsicle
1
这里提供一个一行代码的解决方案,通过修改jar任务来构建one-jar。我发现这是最方便的方法。请注意,在单个jar中添加“configurations.runtime”以捆绑运行时依赖项。 - Quazi Irfan
我发现“应用程序插件”非常适合我的需求,而且足够灵活。 它还允许将所有内容打包到zip文件中,并在该zip文件中包含/排除额外的文件。 此外,可以使用自定义任务添加另一个启动脚本以及附加入口点。 - kinORnirvana
我该如何执行生成的.tar/.zip文件? - Tobiq
提到的.tar/.zip文件不能直接执行。您需要先解压缩它们,然后运行其中捆绑的脚本。 - davidmc24

41

对我来说最简单的解决方案是使用gradle-shadow-plugin

除了应用插件外,需要做的只有:

配置jar任务以将您的主类放入清单中。

jar {
  manifest {
   attributes 'Main-Class': 'com.my.app.Main'
  }
}
运行 Gradle 任务。
./gradlew shadowJar

build/libs/获取app-version-all.jar,最后通过以下方式执行:

java -jar app-version-all.jar

2
这里是一个完整的 build.gradle 示例。链接:https://gist.github.com/TurekBot/4a3cd406cb52dfd8bfb8022b982f0f6c。 - Brad Turek
当我进行构建时,我得到了BUILD SUCCESSFUL的提示,但是当我尝试使用java -jar build/libs/core-all-1.0.jar运行jar文件时,我遇到了以下错误:Error: Could not find or load main class scanners.exchange.Main Caused by: java.lang.ClassNotFoundException: scanners.exchange.Main 你知道我该如何解决吗? - Luka Lopusina
@LukaLopusina 您指定的类不在您的JAR文件中。如果您正在使用Kotlin,则需要说'com.my.app.MainKt'。没有更多信息,我无法为您提供更多帮助。 - byxor
当前插件Shadow v5.+仅兼容Gradle 5.0+和Java 7+。 - Zon

38

正如其他人所指出的,为了使一个jar文件可以执行,应用程序的入口点必须在清单文件中的Main-Class属性中设置。如果依赖的类文件不在同一目录下,则需要在清单文件的Class-Path条目中进行设置。

我尝试过各种插件组合和其他方法,以简单的任务创建可执行的jar文件,并以某种方式包含依赖项。所有插件似乎都有缺陷,但最终我得到了我想要的结果。没有神秘的脚本,没有污染构建目录的数百个不同的小文件,一个相当干净的构建脚本文件,最重要的是:不会将数百万个外来第三方类文件合并到我的jar存档中。

以下内容是从这里复制粘贴而来,供您参考。

[如何]创建一个分发zip文件,在子目录/lib中带有依赖的jar并将所有依赖项添加到清单文件的Class-Path条目中:

apply plugin: 'java'
apply plugin: 'java-library-distribution'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.apache.commons:commons-lang3:3.3.2'
}

// Task "distZip" added by plugin "java-library-distribution":
distZip.shouldRunAfter(build)

jar {
    // Keep jar clean:
    exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.MF'

    manifest {
        attributes 'Main-Class': 'com.somepackage.MainClass',
                   'Class-Path': configurations.runtime.files.collect { "lib/$it.name" }.join(' ')
    }
    // How-to add class path:
    //     https://dev59.com/m2Eh5IYBdhLWcg3wNxPM
    //     https://gist.github.com/simon04/6865179
}

可以在 这里 找到托管的结果。

结果可以在 build/distributions 中找到,并且解压后的内容如下:

lib/commons-lang3-3.3.2.jar
MyJarFile.jar

MyJarFile.jar#META-INF/MANIFEST.mf 的内容如下:

Manifest-Version: 1.0
Main-Class: com.somepackage.MainClass
Class-Path: lib/commons-lang3-3.3.2.jar


当前应用程序的jar包也将位于分发的“lib”目录中。您必须手动将其移动到顶级目录或更改此行: 'Class-Path': configurations.runtime.files.collect { "lib/$it.name" }.join(' ') } 为: 'Class-Path': configurations.runtime.files.collect { "$it.name" }.join(' ') } - Marc Nuri
1
@MarcNuri 你确定吗?我尝试使用这种方法来构建我的应用程序,但是当前的应用程序 jar 文件并不在生成的 zip/tar 文件的 lib 目录中,而是在 lib 的父目录中,就像这个答案所建议的那样。这个解决方案对我来说似乎完美地解决了问题。 - thejonwithnoh
1
@thejonwithnoh 对不起,你是对的。我没有看到使用“java-library-distribution”插件的解决方案。在我的情况下,我只是使用“application”插件,它与主要区别在于所有的jar文件(包括应用程序jar)都位于“lib”目录中。因此,将“lib/$it.name”更改为“$it.name”就可以完成工作。 - Marc Nuri
似乎在Gradle 7.x中,configurations.runtime.files.collect被替换为configurations.runtimeClasspath.files.collect - comfreak

14
这是为了Kotlin DSL(build.gradle.kts)。
方法1(不需要application或其他插件)
tasks.jar {
    manifest.attributes["Main-Class"] = "com.example.MyMainClass"
    // OR another notation
    // manifest {
    //     attributes["Main-Class"] = "com.example.MyMainClass"
    // }
}

如果你使用任何外部库,请使用以下代码。将库的JAR文件复制到放置结果JAR文件的目录的libs子目录中。确保你的库JAR文件的文件名中不包含空格。
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依赖项的绝对路径(这也容易被更改,并且在其他系统上不可用)。如果您想使用绝对路径,也许这个解决方法会起作用。
使用以下命令创建JAR文件:
./gradlew jar

默认情况下,结果 JAR 文件将会在 build/libs/ 目录中创建。
方法二:将库文件(如果有的话)嵌入到结果 JAR 文件中(也称为 fat 或 uber 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
}

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

使用以下命令创建JAR文件:
./gradlew shadowJar

查看Shadow文档以获取有关配置插件的更多信息。

运行创建的JAR文件

java -jar my-artifact.jar

以上解决方案已经进行了以下测试:

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

请参阅官方Gradle文档以创建uber(fat)JAR文件

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

请注意,您的资源文件将自动包含在JAR文件中(假设它们放置在 /src/main/resources/ 目录或在构建文件中设置为资源根目录的任何自定义目录中)。要在应用程序中访问资源文件,请使用以下代码(注意名称开头的/):

Kotlin
val vegetables = MyClass::class.java.getResource("/vegetables.txt").readText()
// 其他方式:
// 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();
// 或者 var stream = MyClass.class.getResourceAsStream("/vegetables.txt");
var reader = new BufferedReader(new InputStreamReader(stream));
var vegetables = reader.lines().collect(Collectors.joining("\n"));

此外,参见此帖子,以创建一个新任务而不是修改现有的Jar任务。 - Mahozad

12
您可以使用SpringBoot插件:
plugins {
  id "org.springframework.boot" version "2.2.2.RELEASE"
}

创建jar文件
gradle assemble

然后运行它

java -jar build/libs/*.jar

注意:您的项目不需要是SpringBoot项目才能使用此插件。


5

谢谢Konstantin,它像魔法一样工作,并带有一些细微差别。由于某种原因,在jar清单的一部分指定主类并没有完全生效,而是需要使用mainClassName属性。以下是包含所有要使其工作的内容的build.gradle片段:

plugins {
  id 'java' 
  id 'com.github.johnrengelman.shadow' version '1.2.2'
}
...
...
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
...
...
mainClassName = 'com.acme.myapp.MyClassMain'
...
...
...
shadowJar {
    baseName = 'myapp'
}

在运行gradle shadowJar之后,您会在构建文件夹中得到myapp-{version}-all.jar,可以通过java -jar myapp-{version}-all.jar命令运行。

5

据我所知,installApp 不会创建 META-INF/MANIFEST.MF 文件。我做错了什么吗? - advait
1
Application Plugin的任务列表中,我没有看到installApp任务。你是不是想说installDist呢? - Quazi Irfan
2
是的,在Gradle 3.0中,installApp被重命名为installDist。这里是发布说明 - Quazi Irfan

3

您可以在模块设置(或项目结构)中定义jar工件。

  • 右键单击模块 > 打开模块设置 > 工件 > + > JAR > 从具有依赖关系的模块。
  • 设置主类。

制作jar文件只需从“构建”菜单中单击“构建工件...”。作为奖励,您可以将所有依赖项打包到一个单独的jar文件中。

在IntelliJ IDEA 14 Ultimate上测试过。


2

我检查了很多链接来找到解决方案,最终执行以下步骤使其正常工作。我正在使用Gradle 2.9。

请在您的build.gradle文件中进行以下更改:

  1. Mention plugin:

    apply plugin: 'eu.appsatori.fatjar'
    
  2. Provide the Buildscript:

    buildscript {
    repositories {
        jcenter()
    }
    
    dependencies {
        classpath "eu.appsatori:gradle-fatjar-plugin:0.3"
    }
    }
    
  3. Provide the Main Class:

    fatJar {
      classifier 'fat'
      manifest {
        attributes 'Main-Class': 'my.project.core.MyMainClass'
      }
      exclude 'META-INF/*.DSA', 'META-INF/*.RSA', 'META-INF/*.SF'
    }
    
  4. Create the fatjar:

    ./gradlew clean fatjar
    
  5. Run the fatjar from /build/libs/ :

    java -jar MyFatJar.jar
    

1
从2019年开始:这个建议在这里不起作用。在fatjar中没有包含任何依赖项。 - carl

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