用kotlin來開發一個cli工具 | 沒用的技能+1
theme: smartblue
腳手架
腳手架是為了保證各施工過程順利進行而搭設的工作平台
而在程序開發過程中,每個工程或者説公司也都需要一個腳手架工具。通過腳手架命令行的形式簡化開發流程,避免發生一些人為的相對低級的問題,所以這個也就是為什麼叫做腳手架的原因吧。
而由於每個公司的代碼規範都不同,一般情況下會主動讓開發同學進行工程方面的cv操作,就是成本高並且容易出錯。這也就是為什麼我們打算寫一些這樣的工具的原因。
在一般情況下,更多的程序猿會選擇用python去寫,因為腳本語言的靈活性,但是對於一個辣雞安卓來説會增加額外的學習成本,所以這就取決於有沒有天賦了,能不能對一門陌生的語言快速上手了。
這次文章會介紹的是用kotlin去構建一個二進制文件,通過這個來完成腳手架cli工具的建設。
開搞
demo 工程地址TheNext
一開始的啟發在於有時候使用一些第三方工具的時候會提供一個jar包,然後只要輸入java -jar xxx.jar
就可以使用這個jar包中的Main函數了。
因為是一個jar包,所以裏面的內容肯定也都是用jvm內的幾種語言來進行編寫的,那麼這就讓我們這種老年選手看到了一絲絲的希望。
開發調試
先建立了一個java工程,然後構建了一個main函數,之後開始進行代碼編寫。但是如果每次都需要先打包之後在通過java -jar
來執行的話非常不便利開發並且debug。而且模擬入參也灰常的噁心,你也知道的程序猿都是懶人嗎。
所以我們就借用了unittest
的能力,對於入參進行mock進行簡單的調試功能了。
【參考地址](http://github.com/Leifzhang/TheNext/blob/main/impact/src/test/kotlin/com/kronos/mebium/test/Sample.kt)
``` class Sample {
@Test
fun help() {
Next.main(
arrayOf(
"--help"
)
)
}
@Test
fun testAndroidModule() {
val file = File("")
val moduleName = "strike-freedom"
val groupName = "com.kronos.common"
Next.main(
arrayOf(
"module", "android",
"-file", file.absolutePath,
"-name", moduleName,
"-group", groupName
)
)
}
@Test
fun testAndroidApplication() {
val file = File("../app/")
val projectName = "freedom"
Next.main(
arrayOf(
"project", "android",
"-name", projectName,
"-file", file.absolutePath
)
)
}
} ```
此處我們將Main函數通過unittest來進行模擬,這樣就可以方便我們在開發階段快速調試腳手架的能力了。
每個方法塊都可以認為是一個運行的入口,通過這個來模擬出程序所需要的入參。從而一邊完成了測試代碼的編寫,一邊完成了調試入口。
jcommander
這是一個讓我們可以更像模像樣的寫一個cli的入參解析工具,即使參數順序是錯亂的,我們仍然能解析出我們想要的數據結構,讓我們的工程看起來更正規一點。而且這個庫也被很多開源項目所使用,基本算的上是千錘百煉了,比如美團的walle
。
jcommander值得你一個star的
``` @Parameters(commandDescription = "args 參數") class CommandEntity {
@Parameter(
names = ["-file", "-f"],
required = true,
converter = FileConverter::class,
description = "生成目標文件路徑"
)
lateinit var file: File
@Parameter(
names = ["-name"], required = true,
description = "文件名"
)
lateinit var name: String
@Parameter(names = ["-group", "-bundle", "-g", "-b"], description = "唯一標識符")
var group: String? = null
}
```
```
override fun handle(args: Array
實例demo如上,我也是參考了官方demo寫的。通過JCommander
將args解析成對應的數據實體結構。
Main 函數聲明
我們要在build.gradle
內的jar的task中,聲明當前jar的main
函數,作為命令行工具的入口。否則打出來的jar包就會報沒有main函數的異常。
jar {
exclude("**/module-info.class")
/* from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}*/
manifest {
attributes 'Main-Class': 'com.kronos.mebium.Next'
}
}
其中from的含義就是將一個jar包把所有的依賴都打到一起,從而形成一個fatjar
,而後續因為使用了gradle提供的application
插件,所以這行被我註釋了。
壓縮模板
我們這個腳手架最核心的就是把一部分工程模板壓縮成一個zip資源文件,打包帶入jar產物中。然後呢我這個人又比較懶,希望每次執行打包的時候都進行一次模板的壓縮替換,所以這裏我通過一部分gradle task
來進行執行了。
```
abstract class ZipTask extends DefaultTask {
@InputDirectory
Provider
@OutputFile
Provider<File> outputFile = project.objects.property(File)
@TaskAction
def doAction() {
def outputFile = outputFile.get()
createFileSafety(outputFile)
compress(library.get(), outputFile)
}
static File compress(final File srcDir, final File zipFile) {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))
srcDir.eachFileRecurse({
zos.putNextEntry(new ZipEntry(it.path - srcDir.path + (it.directory ? "/" : "")))
if (it.file) {
zos << it.bytes
}
zos.closeEntry()
})
zos.close()
return zipFile
}
private static File createFileSafety(File file) {
if (file.exists()) {
file.delete()
}
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs()
}
return file
}
} ```
首先定義出一個task,然後定義好輸入輸出,輸入的是一個文件夾,輸出的則是一個zip的壓縮文件,輸入輸出的地址由外部來聲明。
```
def moduleTask = project.tasks.register("zipAndroidLib", ZipTask.class) { it.library.set(file("../library")) it.outputFile.set(file("./src/main/resources/zip/android/android.zip")) }
def projectTask = project.tasks.register("zipAndroidProject", ZipTask.class) { it.library.set(file("../project")) it.outputFile.set(file("./src/main/resources/zip/android/project.zip")) }
afterEvaluate { project.tasks.findByName("compileJava").dependsOn(moduleTask) project.tasks.findByName("compileJava").dependsOn(projectTask) } ```
然後直接聲明處兩個task,之後把compileJava
依賴到這兩個task上去,這樣就可以保證每次compileJava
,這兩個task都會被執行到了。編譯緩存我就不説了,大家自行領悟吧。
java resource 讀取方式
javaClass.classLoader.getResourceAsStream(name)
就可以了。
放飛自我
接下來我們就可以在命令行工具內放飛自我,開始很簡單的通過unittest來進行代碼的編寫和調試了。
我們就可以通過自己熟悉的kotlin或者java來編寫一個簡單的cli工具,從而來進一步的做到基於工程定製化的一些方便的腳手架工具了。
生成最終產物
這裏我們使用了 gradle提供的application plugin
,這個插件可以將java jar包裝成一個可執行文件的zip的壓縮包。格式如下圖所示:
而這個的生成指令就是,通過./gradlew impact:assembleDist 任務生成對應的二進制壓縮包。
這樣的好處就是我們可以省略掉java -jar xxxxx.jar
的繁瑣操作,通過可執行文件直接達到我們寫一個cli的便利。
結尾
工程內的代碼還是比較簡單的,有興趣的就自己讀一下,只是一個demo而已。
還是那句因為菜,不想去學一門新語言。如果萬一哪怕我的py在強那麼一點點,我也考慮用py來寫了,哈哈哈哈哈。
- 為什麼要選擇VersionCatalog來做依賴管理?
- 用kotlin來開發一個cli工具 | 沒用的技能 1
- 我想變成光 | 2022 安卓年終總結
- Gradle Enterprise 牛逼 | 還債了
- kotlin compose 升級的苦澀 | 一地雞毛
- 偽造出一個假的系統View | Gradle Task
- Jetpack Compose 導致的編譯劣化 | KCP 簡介
- Android Gradle 同步優化
- Gradle Build Cache 引發的編譯問題 | Gradle Task 緩存
- 有趣的Viewbinding委託
- 閒話安卓MonoRepo
- 嗶哩嗶哩在Hilt組件化的使用 | 技術探索
- 註解排列組合出啟動任務 | ksp
- 我們升級適配完AGP 7.0 | 雜談
- Android 啟動優化雜談 | 另闢蹊徑
- Android 基礎架構組面試題 | 面試
- Aar和源碼切換插件Plus | 一個沒采納的廢案
- 現在準備好吿別Transform了嗎? | 擁抱AGP7.0
- Tree Api ClassScanner = 識別三方隱私權限調用 | Android Lint
- Lambda引發的慘案 | Transform進階教程