使用Gradle和Kotlin构建自执行JAR文件

60

我写了一个简单的 Kotlin 源代码文件和 Gradle 脚本文件来开始我的项目。但是我不知道如何将主函数添加到清单文件中,以便 JAR 文件可以自行执行。

这是我的 build.gradle 脚本:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.9.66'
    }
}
apply plugin: "kotlin"
repositories {
    mavenCentral()
}
dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:0.9.66'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.loloof64.kotlin.exps.ExpsPackage'
    }
}

这是我的com.loloof64.kotlin.exps.Multideclarations.kt文件:

package com.loloof64.kotlin.exps

class Person(val firstName: String, val lastName: String) {
    fun component1(): String {
        return firstName
    }
    fun component2(): String {
        return lastName
    }
}

fun main(args: Array < String > ) {
    val(first, last) = Person("Laurent", "Bernabé")
    println("First name : $first - Last name : $last")
}

我从终端启动JAR文件(java -jar MYJar.jar)时,会看到以下堆栈跟踪信息,说明缺少Kotlin反射库的类,而它们确实没有被添加到JAR中。似乎我在最终的JAR文件中缺少kotlin-compiler构件的类,以及kotlin-stdlib源代码,但我不知道如何调整Gradle构建。

$> java -jar build/libs/kotlin_exps.jar 

Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/reflect/jvm/internal/InternalPackage
    at com.loloof64.kotlin.exps.ExpsPackage.<clinit>(MultiDeclarations.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.reflect.jvm.internal.InternalPackage
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

我正在使用 Kotlin 0.9.66 和 Gradle 2.1。


请注意,Kotlin 为顶层函数(例如 main)生成的类名是在同一文件中定义的包加上附加了 KT 的文件名。在您的情况下,这将是 com.loloof64.kotlin.exps.MultideclarationsKT。您可以通过在文件顶部添加 @file:JvmName("OtherName") 来更改此行为,以为文件中的所有顶层函数定义一个新的类名。 - Jayson Minard
你需要添加两个字符 Kt 而不是 KT... 即大写字母 K 和小写字母 t。这是一个容易犯的错误,但会导致 jar 执行失败。我自己有时也会因为打字不完美而遇到这个问题。 - J.E.Tkaczyk
1
请查看这个相当全面的答案 - Mahozad
8个回答

38

添加插件application,然后将mainClassName设置为

mainClassName = '[your_namespace].[your_arctifact]Kt'

例如,假设您将以下代码放在名为main.kt的文件中:

package net.mydomain.kotlinlearn

import kotlin
import java.util.ArrayList

fun main(args: Array<String>) {

    println("Hello!")

}

你的build.gradle应该是这样的:

apply plugin: 'kotlin'
apply plugin: 'application'

mainClassName = "net.mydomain.kotlinlearn.MainKt"

实际上,Kotlin正在构建一个类来封装与文件同名的主函数 - 以Title Case形式命名。


1
你能否将你的答案更新为Kotlin为顶级函数创建类的新命名约定?在他的示例中,现在应该是:com.loloof64.kotlin.exps.MultideclarationsKT作为类名。这适用于Kotlin 1.0 beta及以上版本。 - Jayson Minard
更新至 Kotlin 1.0 Beta 及更新版本。感谢 Jayson。 - Guildenstern70
2
这不应该是被接受的答案;它是正确的,但不是完整的图片:需要“收集”Kotlin运行时文件以制作完整的可执行jar文件,如@loloof64的示例所示。 - Chris Hatton
这个已经不起作用了。如果你修复它,我会撤销我的踩票。 - Adam Arold
@AdamArold 我测试了1.1.51的版本,它能够正常工作。你所提到的是哪个版本?谢谢。 - Guildenstern70
我正在使用 1.1.4-3 - Adam Arold

29

我找到了解决方法(感谢MkYong网站)。

  1. Gradle脚本需要将kotlin-compiler作为依赖项。
  2. Gradle脚本需要一种方法来收集所有kotlin文件并将它们放入jar包中。

因此,我使用了以下Gradle脚本:

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
       classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.1-2'
    }
}

apply plugin: "kotlin"

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib:1.0.1-2'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.loloof64.kotlin.exps.MultideclarationsKT'
    }

    // NEW LINE HERE !!!
    from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
}

3
对我而言,即使没有编译器依赖,只要收集了JAR文件所需的文件就可以运行。 - Stefan Paul Noack
可以确认编译器依赖不是必需的,只收集文件就足够了。 - Czyzby
执行任务后,jar 包在哪里? - Franmcod
Jar文件的放置位置取决于构建jar的方法。如果您执行gradle jar任务,它将放置在build/libs目录中。该构建目录与build.gradle文件位于同一级别。如果您在IntelliJ IDE中将jar映射构建为工件并使用IntelliJ构建工件,则它将放置在out/artifacts目录中,该目录与IntelliJ项目处于相同级别。 - J.E.Tkaczyk
具体来说,我发现在将这个jar_block添加到build.gradle文件后,无论是使用IntelliJ的构件机制还是通过gradle任务机制构建jar文件,都可以成功创建jar。 - J.E.Tkaczyk

19

在构建Artifact时,以上所有解决方案都无效。

IDE版本为IntelliJ IDEA 2019.1.1.

若要解决此问题,请执行以下操作:

步骤

步骤1 - 创建Artifact

  1. 进入 File -> Project Structure -> Artifacts

  2. 点击 + -> JAR -> From modules with dependencies

enter image description here

  1. 选择您程序的Main Class

enter image description here

步骤2 - 更改MANIFEST路径

  1. Directory for META-INF/MANIFEST.MF的值更改为您的项目根目录。

    例如,从/your/project/directory/src/main/kotlin更改为/your/project/directory

enter image description here

  1. 点击 OK,然后点击 ApplyOK

步骤3 - 构建Artifact

  1. 最后,前往Build -> Build Artifacts -> [你的打包名称] -> Build

生成的JAR文件可以在out/artifact/[你的打包名称]目录中找到。(y)


2
非常感谢!经过几次尝试,这是唯一有效的方法! - Augusto Carmo
1
Kotlin是一门非常酷的语言,但它的构建系统实在是糟糕透顶(到处都是神奇的配方)。感谢您的帮助! - undefined

3
这是针对 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要求我们使用相对URL来设置Class-Path属性。因此,我们不能使用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插件(创建一个fat或uber 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文档以获取有关配置插件的更多信息。
第四种方法:创建新任务(而不是修改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(用于 .kts 构建脚本的 Kotlin 版本为 1.4.31)
请参阅官方 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"));
    

2
感谢您的帮助,在添加jar部分后,它对我有用。
jar {
    manifest {
        attributes 'Main-Class': 'com.photofiles.Application'
    }

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

不需要应用程序插件。


3
主类名需要以 Kt 结尾。因此,上面应该是 ApplicationKt - J.E.Tkaczyk

2

如果你像我一样比较傻:

不要将你的IntelliJ运行配置创建为一个应用程序。IntelliJ会认为它是Java,但它永远不会工作。相反,请使用“Kotlin”条目。


2

如果使用GradleKotlinDSL,那么我的重复的问题有一个答案:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
    kotlin("jvm") version "1.2.51"
    id("com.github.johnrengelman.shadow") version "2.0.4"
}

group = "xxx.yyy"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(kotlin("stdlib-jdk8"))
}

tasks.withType<KotlinCompile> {
    kotlinOptions.jvmTarget = "1.8"
}

tasks.withType<ShadowJar> {

    manifest.attributes.apply {
        put("Implementation-Title", "Gradle Jar File Example")
        //put("Implementation-Version" version)
        put("Main-Class", "HelloKotlinWorld.App")
    }

我认为这是最简单的解决方案。噢,也许你只是使用 Kotlin 本身而不是 DSL


1

对于使用 Kotlin MP 插件的任何人,这是您的代码

jvm{
    jvmJar {
        manifest{
            attributes 'Main-Class':'Class path here'
        }
    }

}

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