使用 Gradle 构建应用程序
application 插件使这非常容易。
// echo/build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.20'
id 'application'
}
group = 'mutual.aid'
version = '1.0'
application {
mainClass = 'mutual.aid.AppKt'
}
// echo/src/main/kotlin/mutual/aid/App.kt
package mutual.aid
fun main(args: Array<String>) {
val echo = args.firstOrNull() ?: "Is there an echo in here?"
println(echo)
}
现在可以构建和运行这个小应用程序
$ ./gradlew echo:run
Is there an echo in here?
如果我们想定制我们的信息
$ ./gradlew echo:run --args="'Nice weather today'"
Nice weather today
将应用程序转变为发行版
假设希望其他人实际运行应用程序,应该将其捆绑为一个发行版。让我们为此使用分发插件。
$ ./gradlew echo:installDist
$ echo/build/install/echo/bin/echo "hello world"
hello world
我们不必应用任何新插件,因为 application 插件已经负责应用 distribution 插件。后者添加了一个任务 installDist,它将分发安装到项目的 build 目录中。完整的分布如下所示:
我们可以看到它已经收集了我们运行时类路径中的所有 jar,包括我们刚刚构建的新 jar,echo-1.0.jar。除了这些 jars,我们还有两个 shell 脚本,一个用于 *nix,一个用于 Windows。这些脚本使用 Gradle 用于 gradlew[.bat] 的相同模板,因此它们应该非常健壮。
虽然只是一个“小”应用程序,但它仍然包含完整的 Kotlin 运行时,尽管它使用的很少。该 lib 目录的大小为 1.7M。但是我们真正想要的是我们的程序(echo-1.0.jar)和一个从命令行轻松调用它的脚而已。
在 Shadow 插件上分层
Shadow 插件确实提高了这一点,当尝试通过着色解决问题时,您现在至少有五个问题。
// echo/build.gradle
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.20'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
def shadowJar = tasks.named('shadowJar', ShadowJar) {
// the jar remains up to date even when changing excludes
// https://github.com/johnrengelman/shadow/issues/62
outputs.upToDateWhen { false }
group = 'Build'
description = 'Creates a fat jar'
archiveFileName = "$archivesBaseName-${version}-all.jar"
reproducibleFileOrder = true
from sourceSets.main.output
from project.configurations.runtimeClasspath
// Excluding these helps shrink our binary dramatically
exclude '**/*.kotlin_metadata'
exclude '**/*.kotlin_module'
exclude 'META-INF/maven/**'
// Doesn't work for Kotlin?
// https://github.com/johnrengelman/shadow/issues/688
//minimize()
}
我认为最有趣的部分是 from 和 exclude 语句。 from 告诉 shadow 要捆绑什么:我们的实际编译输出,加上运行时类路径。 exclude 语句对于缩小我们的fat jar很重要。
我们已经可以运行这个 fat jar 并验证它是否仍然有效(runShadow 任务是由 shadow 插件添加的,因为它与 application 插件集成):
$ ./gradlew echo:runShadow --args="hello world"
hello world
最后我们可以检查 fat jar 本身(当我们运行 runShadow 任务时,这个任务也会隐式运行):
$ ./gradlew echo:shadowJar
# Produces output at echo/build/libs/echo-1.0-all.jar
如果我们检查它的大小,我们会看到它是 1.5M:已经比原来的 1.7M 减少了大约 12%。
在 Proguard 上分层
// echo/build.gradle
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.internal.jvm.Jvm
import proguard.gradle.ProGuardTask
buildscript {
repositories {
mavenCentral()
}
dependencies {
// There is apparently no plugin
classpath 'com.guardsquare:proguard-gradle:7.1.0'
}
}
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.20'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
Proguard 没有捆绑为 Gradle 插件,而是作为 Gradle 任务,因此要将其添加到构建脚本的类路径中。现在我们可以访问 ProGuardTask 任务了。
// echo/build.gradle
def minify = tasks.register('minify', ProGuardTask) {
configuration rootProject.file('proguard.pro')
injars(shadowJar.flatMap { it.archiveFile })
outjars(layout.buildDirectory.file("libs/${project.name}-${version}-minified.jar"))
libraryjars(javaRuntime())
libraryjars(filter: '!**META-INF/versions/**.class', configurations.compileClasspath)
}
/**
* @return The JDK runtime, for use by Proguard.
*/
List<File> javaRuntime() {
Jvm jvm = Jvm.current()
FilenameFilter filter = { _, fileName -> fileName.endsWith(".jar") || fileName.endsWith(".jmod") }
return ['jmods' /* JDK 9+ */, 'bundle/Classes' /* mac */, 'jre/lib' /* linux */]
.collect { new File(jvm.javaHome, it) }
.findAll { it.exists() }
.collectMany { it.listFiles(filter) as List }
.toSorted()
.tap {
if (isEmpty()) {
throw new IllegalStateException("Could not find JDK ${jvm.javaVersion.majorVersion} runtime")
}
}
}
// proguard.pro
-dontobfuscate
-keep class mutual.aid.AppKt { *; }
由于 ProGuardTask 不是由插件注册和配置的,我们必须自己做。第一部分是告诉它我们的规则,非常简单;我们只想缩小,我们想保留我们的主类入口点。接下来,我们告诉它我们的 injars,这只是 shadowJar 任务的输出:这就是正在缩小的内容。 (重要的是,我使用的语法意味着任务依赖关系由 Gradle 确定,而不依赖于依赖项。) outjars 函数非常简单地告诉任务在哪里吐出缩小的 jar。最后,我们有 libraryjars,我认为它是编译我的应用程序所需的类路径。这些不会捆绑到输出中。其中最复杂的部分是 javaRuntime() 函数。
$ ./gradlew echo:minify
如果我们现在检查 echo/build/libs/echo-1.0-minified.jar,我们会发现它只有 12K.验证 fat jar 是否可运行类似,我们可以创建一个 JavaExec 任务并运行我们的缩小 jar:
tasks.register('runMin', JavaExec) {
classpath = files(minify)
}
$ ./gradlew echo:runMin --args="'hello world'"
hello world
我们仍然需要将这个缩小的应用程序捆绑为一个发行版并发布它。
制作发行版并发布
application 和 shadow 插件都注册了一个“zip”任务(分别是 distZip 和 shadowDistZip)。但是它们没有打包我们的缩小 jar。幸运的是,它们都是核心 Gradle 类型 Zip 的任务,易于配置。
// echo/build.gradle
def startShadowScripts = tasks.named('startShadowScripts', CreateStartScripts) {
classpath = files(minify)
}
def minifiedDistZip = tasks.register('minifiedDistZip', Zip) {
archiveClassifier = 'minified'
def zipRoot = "/${project.name}-${version}"
from(minify) {
into("$zipRoot/lib")
}
from(startShadowScripts) {
into("$zipRoot/bin")
}
}
我们要做的第一件事是选择 startShadowScripts 任务(这个任务的作用将很快解释)。我们不想让它使用默认的类路径(由 shadowJar 任务生成),而是希望它使用我们缩小的 jar 作为类路径。语法 classpath = files(minify) 类似于早期的 injars(shadowJar.flatMap { it.archiveFile }),因为它带有任务依赖信息。由于 minify 是一个 TaskProvider,files(minify) 不仅设置类路径,还将 minify 任务设置为 startShadowScripts 任务的依赖项。
接下来,我们创建自己的 Zip 任务 minifiedDistZip,并以类似于基本 distZip 任务的方式构建它。如果我们检查最终产品,则更容易理解:
$ ./gradlew echo:minifiedDistZip
$ unzip -l echo/build/distributions/echo-1.0-minified.zip
Archive: echo/build/distributions/echo-1.0-minified.zip
Length Date Time Name
-------------- ---------- ----- ----
0 07-11-2021 17:02 echo-1.0/
0 07-11-2021 17:02 echo-1.0/lib/
12513 07-11-2021 16:59 echo-1.0/lib/echo-1.0-minified.jar
0 07-11-2021 17:02 echo-1.0/bin/
5640 07-11-2021 17:02 echo-1.0/bin/echo
2152 07-11-2021 17:02 echo-1.0/bin/echo.bat
-------------- -------
20305 6 files
我们的存档包含我们的fat、minified的 jar,以及两个脚本,一个用于 *nix,一个用于 Windows。特定路径很重要,因为生成的 echo 和 echo.bat 脚本包含 CLASSPATH 属性:
CLASSPATH=$APP_HOME/lib/echo-1.0-minified.jar
发布minified
我们现在有了一个带有压缩 jar 的 zip 文件,而 zip 本身只有 15K,与 distZip 任务生成的原始 1.5M zip 相比有了巨大的改进。我们还有一件事要自动化,那就是发布这个档案。我们将添加 maven-publish 插件,然后对其进行配置:
// echo/build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.5.20'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
id 'maven-publish'
}
publishing {
publications {
minifiedDistribution(MavenPublication) {
artifact minifiedDistZip
}
}
}
这增加了一些发布任务,包括 publishMinifiedDistributionPublicationToMavenLocal。我们可以运行它并检查输出:
$ ./gradlew echo:publishMinifiedDistributionPublicationToMavenLocal
$ tree ~/.m2/repository/mutual/aid/echo/
~/.m2/repository/mutual/aid/echo/
├── 1.0
│ ├── echo-1.0-minified.zip
│ └── echo-1.0.pom
└── maven-metadata-local.xml
我们甚至获得了一个 pom 文件,因此我们可以通过引用其 Maven 坐标mutual.aid:echo:1.0 从存储库中解析此工件。