1. 引言
众所周知, 在maven中我们会使用
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
...config
</plugin>
这样的配置来简单地将依赖进行外置以减小jar体积,那对于配置更灵活的gradle-kts,我们该怎么办呢?
2. 前置知识
1. 依赖外置的原理
众所周知,bootJar任务会打出一个fat-jar。该jar内会包含该应用程序的所有运行时依赖
。
如果我们不选择打包这些依赖,JVM会从运行时参数里寻找class-path,从中依次遍历加载jar包。
2. 如何指定class-path?
- 通过指定环境变量
export CLASSPATH=/path/to/classes:/path/to/lib.jar
- 使用通配符
java -cp "/path/to/libs/*" MyMainClass
- 在MANIFEST.MF里指定
Manifest-Version: 1.0
Class-Path: lib/dependency1.jar lib/dependency2.jar
Main-Class: com.example.Main
在这里我以第三种方式进行演示。因为第三种方式可以在控制台里以一条命令运行。
3. 详细设计
首先我们需要定义两个任务:
// 清除现有的lib目录
tasks.register<Delete>("clearJar") {
delete(layout.buildDirectory.dir("libs/lib"))
}
// 清除现有的res目录
tasks.register<Delete>("clearRes") {
delete(layout.buildDirectory.dir("libs/res"))
}
因为我们生成jar的时候,会将所有的依赖全部打包进某个文件夹里。
假如你删除了某个依赖,如果不及时清理,最终的无用依赖包将会越来越多。
有删除就有创建,我们再定义两个任务:
// 将依赖包复制到lib目录
tasks.register<Copy>("copyJar") {
dependsOn("clearJar")
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("libs/lib"))
}
// 将资源文件复制到res目录
tasks.register<Copy>("copyRes") {
dependsOn("clearRes")
from("src/main/resources")
into(layout.buildDirectory.dir("libs/res"))
}
此时复制操作和删除操作都已准备完毕,现在需要修改bootJar
方法了:
tasks.bootJar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// 排除所有的jar
exclude("*.jar")
exclude("assets/**")
exclude("static/**")
exclude("application.properties")
// lib目录的清除和复制任务
dependsOn("clearJar", "clearRes")
dependsOn("copyJar", "copyRes")
// 指定依赖包的路径
manifest {
attributes(
"Manifest-Version" to "1.0",
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") {
"lib/${it.name}"
} + " res/"
)
}
}
4. 完整代码
关键代码展示完毕,实际上面的代码不能直接用,还有多多少少一些小坑,如果要使用的话建议复制下面的代码片段:
// 清除现有的lib目录
tasks.register<Delete>("clearJar") {
delete(layout.buildDirectory.dir("libs/lib"))
}
// 清除现有的res目录
tasks.register<Delete>("clearRes") {
delete(layout.buildDirectory.dir("libs/res"))
}
// 将依赖包复制到lib目录
tasks.register<Copy>("copyJar") {
dependsOn("clearJar")
from(configurations.runtimeClasspath)
into(layout.buildDirectory.dir("libs/lib"))
}
// 将资源文件复制到res目录
tasks.register<Copy>("copyRes") {
dependsOn("clearRes")
from("src/main/resources")
into(layout.buildDirectory.dir("libs/res"))
}
tasks.jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.bootJar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// 排除所有的jar
exclude("*.jar")
exclude("assets/**")
exclude("static/**")
exclude("application.properties")
// lib目录的清除和复制任务
dependsOn("clearJar", "clearRes")
dependsOn("copyJar", "copyRes")
// 指定依赖包的路径
manifest {
attributes(
"Manifest-Version" to "1.0",
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") {
"lib/${it.name}"
} + " res/"
)
}
}
// 确保processResources任务不会将资源文件复制到classes目录
tasks.processResources {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
5. 扩展——增量更新
既然把依赖进行外置,那一定也可以以停机——上传增量包——开机
来减少上传jar所带来的带宽占用。
这里的代码没什么好说的,我全贴出来以供展示:
tasks.register("doPatchForLibHash") {
inputs.files(layout.buildDirectory.file("build.hash"), fileTree("${layout.buildDirectory.get()}/libs"))
doLast {
//获取旧的md5值分组
val oldFileMap = layout.buildDirectory.file("build.hash").get().asFile.bufferedReader()
.lineSequence().chunked(2).associate {
it[0] to it[1]
}
//遍历新的libs
layout.buildDirectory.file("libs").get().asFile.walkTopDown().forEach { newFile ->
//获取旧的MD5,key为新文件的相对路径
val oldMD5 =
oldFileMap[newFile.relativeTo(layout.buildDirectory.dir("libs").get().asFile).path] ?: return@forEach
//比对,一样则删除
if (oldMD5 == newFile.md5Digest().toHexString()) {
newFile.delete()
}
}
fun deleteEmptyDirectories(directory: File): Boolean {
if (!directory.isDirectory) {
return false
}
var success = true
directory.listFiles()?.forEach { file ->
if (file.isDirectory) {
success = deleteEmptyDirectories(file) && success
}
}
if (success && directory.listFiles()?.isEmpty() == true) {
if (!directory.delete()) {
success = false
}
}
return success
}
deleteEmptyDirectories(layout.buildDirectory.file("libs").get().asFile)
}
}
tasks.register("generateLibHash") {
inputs.files(fileTree("${layout.buildDirectory.get()}/libs"))
outputs.file(layout.buildDirectory.file("libs/build.hash"))
doLast {
val out = layout.buildDirectory.dir("libs/build.hash").get().asFile
out.delete()
out.parentFile.mkdirs()
out.createNewFile()
with(layout.buildDirectory.dir("libs").get().asFile) {
walkTopDown().filter { it.isDirectory.not() }.forEach {
val md5 = it.md5Digest().toHexString()
val path = it.relativeTo(this).path
out.appendText(
"""
$path
$md5
""".trimIndent()
)
}
}
}
}
fun File.md5Digest(): ByteArray {
val md = MessageDigest.getInstance("MD5")
val fis = FileInputStream(this)
val buffer = ByteArray(1024)
var numRead: Int
while (fis.read(buffer).also { numRead = it } > 0) {
md.update(buffer, 0, numRead)
}
fis.close()
return md.digest()
}
需要在 tasks.bootJar {}
的 DSL 里添加:
finalizedBy("generateLibHash")
即可在bootJar后生成lib.hash。
增量包的获取需要先获取旧的lib.hash,放置在build/build.hash
中。
然后运行gradle任务即可获得增量包。