前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

放弃 KotlinPoet 基于模版引擎生成 Dependency 的 Gradle Plugin

作者头像
bennyhuo
发布于 2022-12-01 03:00:59
发布于 2022-12-01 03:00:59
95000
代码可运行
举报
文章被收录于专栏:BennyhuoBennyhuo
运行总次数:0
代码可运行

背景

调研火山引擎的多仓开发插件时遇到一个很有趣的问题。

接入 mars-gradle-plugin

但是问题来了,官方文档是基于 groovy 写的,但是使用 kts 的开发者应该怎么写呢?

这样明显是行不通的,编译器会报错找不到 rootProject.veMarsExt 这个属性

翻源码 or 反编译

首先得找个这个插件的远程地址

但很不幸,只有二进制产物(问了字节的童鞋,没有上传源码) ,没有 sources.jar,没办法,只能 download 二进制产物然后通过 jadx 查看反编译后的代码了。

一、插件入口

二、InitSettingsAction

三、InitSettingsAction 的 run 方法

还好,调用链不长,逻辑也算清晰,很快就理清了脉络。核心:

  1. rootProject 创建了一个名为 veMarsExt 的 extension
  2. 读取根目录下dependency-lock.json,并解析为 deps:Map<String, String?>
  3. 最后把这个 deps 赋值给 veMarsExtdeps 属性

okk,到这里,就瞬间明白为啥 implementation rootProject.veMarsExt.deps.player_demogroovy 里能 work 了,原因就是 mars-gradle-plugin 已经给 rootProject 创建了一个名为 veMarsExtextension

kts 的正确写法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import com.bytedance.mars.veMarsExt

dependencies {
    implementation("androidx.appcompat:appcompat:1.4.2")
    implementation("com.google.android.material:material:1.6.1")
    // user
    val user: String by resolveDependencies()
    implementation(user)
}

/**
* 获取 veMarsExt 里的 deps
*/
fun resolveDependencies(): Map<String, String?> {
    val ext = rootProject.extensions["veMarsExt"] as? veMarsExt
        ?: return emptyMap()
    return ext.deps.toMap()
}

稍微麻烦了亿点点(毕竟 kotlin 没有 groovy 那么动态):

  1. 需要 import com.bytedance.mars.veMarsExt
  2. 定义一个 resolveDependencies 方法,用于解析 rootProject 下的 veMarsExt 里的 deps
  3. 通过 Map 的委托,获取到 key 对应的 value(第 7 行),即坐标依赖

思考

虽然理清了怎么在 build.gradle.kts 下使用 mars-gradle-plugin 解析坐标依赖,但还是很不友好,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
{
  "dependencies": [
    {
      "artifactId": "share",
      "groupId": "com.mars.lib",
      "version": "1.0.2"
    },
    {
      "artifactId": "comment",
      "groupId": "com.mars.lib2",
      "version": "1.0.2"
    },
    {
      "artifactId": "player",
      "groupId": "com.mars.lib2",
      "targets": [
        {
          "flavorName": "demo",
          "version": "1.0.2.demo"
        },
        {
          "flavorName": "full",
          "version": "1.0.2.full"
        }
      ]
    },
    {
      "artifactId": "lib-android",
      "groupId": "com.component.demo",
      "targets": [
        {
          "flavorName": "demo",
          "version": "0.0.30.demo-alpha.0"
        },
        {
          "flavorName": "full",
          "version": "0.0.30.full-alpha.0"
        }
      ]
    }
  ]
}

开发者声明了 depenendency-lock.json但他却不知道 veMarsExt#deps 里的 key 的生成规则是啥,看起来似乎是将 artifactId- 转为 _ (实际上还真是),**比如 artifactIdlib-android 生成的 deps 里对应的 key 应该为 lib_android**。

这就很麻烦,大部分开发者得像我一样去反编译插件的源码,才能确认 deps 的生成规则,最后才能正确的申明依赖,这也太离谱了吧!

所以有没有更友好一点的方式呢?

那,必须是有的

这里,我就先抛砖引玉,给出我思考出来的一种解法。

一种更为优雅的方案

Gradle 插件 + kotlinPoet

最先想到的一种简单且不失风度的解决方案就是这个了,与火山引擎的 mars-gradle-plugin 不同的是,**这个方案的插件需要在 buildSrc 的 build.gradle(.kts) 被 apply**,然后:

  1. 还是从 dependency-lock.json 里读取依赖信息
  2. 通过 kotlinPoetbuildSrckotlin 目录下生成 Dependency.kt

kotlinPoet 进行元编程之前,我期望生成的 Dependency.kt 能满足以下条件:

  • Dependency 是一个单例
  • Dependency 有多个 enum class,这些 enum class 根据产物的 groupId 生成(相同 groupId 的枚举值在同一个 enum class 内)
  • Dependency 内代码缩进正常,well fortmatted
  • 避免生成的 enum class 名和 kotlin 的保留关键字冲突

基于上述的期望,Dependency.kt 可能长这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
object Dependency {

    enum class androidx_lifecycle {
        `lifecycle_extensions` {
            override val gav: String
                get() = "androidx.lifecycle:lifecycle-extensions:2.2.0"
        },
        `lifecycle_viewmodel_ktx` {
            override val gav: String
                get() = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
        },
        `lifecycle_livedata_ktx` {
            override val gav: String
                get() = "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
        },
        `lifecycle_runtime_ktx` {
            override val gav: String
                get() = "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
        };

        abstract val gav: String
    }

    enum class androidx_fragment {
        `fragment` {
            override val gav: String
                get() = "androidx.fragment:fragment:1.2.4"
        },
        `fragment_ktx` {
            override val gav: String
                get() = "androidx.fragment:fragment-ktx:1.2.4"
        };

        abstract val gav: String
    }
}

好像有亿点点复杂,用 kotlinPoet 写出来的代码可能不太好维护。

就这样,暂时没有想到更好的方案(主要我这人有代码洁癖),就先战略性放弃了。

转机

在讲这个方案之前,故事还得从盘古开天说起。 不至于,不至于。

其实就是有一天,突然翻到森哥的一篇是时候放弃 JavaPoet/KotlinPoet 了 ,内心 OS:你让我放弃就放弃啊,我不管,KotlinPoet 天下第一...

但看到文章里有这么一段话:

哎,妈鸭,真香

Gradle 插件 + 模版引擎

模版引擎

  • mustache

模版代码

放置于 gradle pluginresource 目录:

xxx.kt.mustache 为文件名,内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package {{packageName}}

/*
* AUTO-GENERATED, DO NOT MODIFY THIS FILE!
*/
@Suppress("ClassName", "RemoveRedundantBackticks", "EnumEntryName", "SpellCheckingInspection")
object {{implementationClass}} {
    {{#deps}}

    enum class {{groupId}} {
        {{#artifacts}}
        `{{artifactId}}` {
            override val gav: String
                get() = "{{gav}}"
        }{{separator}}
        {{/artifacts}}

        abstract val gav: String
    }
    {{/deps}}
}

Mustache 是一个 logic-less(轻逻辑)模板解析引擎,稍微学习下语法就可以写出 Dependency.kt 对应的模版代码

动态生成 Dependency.kt

接下来,就是如何实现插件的问题了,思路大致如下:

  1. find kotlinSourceSet dir

找到 buildSrc KotlinSourceSet 所在的文件目录,如图:

  1. set Dependency generate dir(optional)

如果想要生成的 Dependency.ktpackage,可以从 Extension 读取 packageName,然后:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 val generatedDir = ktDir.resolve(packageName.replace(".", "/"))
  1. register GenDependency task

注册一个名为 GenDependency 的 task

  1. hook KotlinCompile Task(已废弃)

将 GenDependency task 挂在 KotlinCompile Task 的前,这样生成的 Dependency.kt 源码就会被编译了

之前的思路是把 Dependency.kt 生成到 buildSrcbuild/generated 下的一个子目录里,这就需要:

  • 将这个子目录添加到 kotlinSourceSet
  • 将 GenDependency 这个 task 挂 KotlinCompile task 前

现在的方案不需要了,故此说明。

模版引擎生成代码

为了美观&容易理解,仅贴出最核心的源码实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
abstract class GenerateDependencyTask : DefaultTask() {

    // dependency-lock.json 文件
    @get:InputFile
    abstract val inputFile: RegularFileProperty

    // Dependency.kt 输出目录
    @get:OutputDirectory
    abstract val outputDirectory: DirectoryProperty

    // 包名,例如:info.hellovass
    @get:Input
    abstract val packageName: Property<String>

    // 模板引擎:Mustache
    private val engine: TemplateEngine by lazy(::MustacheEngine)

    @TaskAction
    fun run() {
        
        val dependencies = inputFile.asFile.get().deserialize<Dependencies>()

        val outputDir = outputDirectory.asFile.get()

        val fileWriter = outputDir.resolve("Dependency.kt").writer()

        val packageName = packageName.get()

        fileWriter.use { writer -> engine.render(
                template = "template/Deps.kt.mustache",
                model = DependencyModel(
                    packageName = packageName,
                    implementationClass = "Dependency",
                    deps = toDeps(dependencies.dependencies)
                ),
                writer = writer
            )
        }
    }
}

模版引擎生成 Dependency.kt 的代码主要参考了森哥这个 example 的思路。其实,思路也很简单,还记得上面贴的 Dependency.kt.mustache 代码嘛,这里再贴一次:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package {{packageName}}

/*
* AUTO-GENERATED, DO NOT MODIFY THIS FILE!
*/
@Suppress("ClassName", "RemoveRedundantBackticks", "EnumEntryName", "SpellCheckingInspection")
object {{implementationClass}} {
    {{#deps}}

    enum class {{groupId}} {
        {{#artifacts}}
        `{{artifactId}}` {
            override val gav: String
                get() = "{{gav}}"
        }{{separator}}
        {{/artifacts}}

        abstract val gav: String
    }
    {{/deps}}
}

企业级理解:

  • 模版代码准备好坑位(mustache 各种占位语法)
  • 插件准备好数据(DependencyModel)填坑

使用

  1. buildSrc 的 build.gradle(.kts) apply 这个插件
  2. dependency-lock.json 放置到根目录下
  3. sync 一把,即可在 buildSrc 生成 Dependency.kt

添加依赖

build.gradle.kts

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import info.hellovass.Dependency

dependencies {
    implmentation(Dependency.androidx_legacy.legacy_support_v4.gav)
} 

build.gradle

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import info.hellovass.Dependency

dependencies {
    implementation( Dependency.androidx_legacy.legacy_support_v4.gav)
} 

kts 引用 Dependency 里的 enum class 倒是很方便,但是在 groovy 就没那么简单了,直接这么写是会报错的!

需要这样:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import info.hellovass.Dependency

dependencies {
    implementation( Dependency.androidx_legacy.@legacy_support_v4.gav)
} 

这个小技巧是从 https://touk.pl/blog/2018/05/28/testing-kotlin-with-spock-part-2-enum-with-instance-method/ 学来的,你学废了吗?

参考

  • https://www.volcengine.com/docs/6436/110098
  • 是时候放弃 JavaPoet/KotlinPoet 了 | Johnson Lee
  • https://touk.pl/blog/2018/05/28/testing-kotlin-with-spock-part-2-enum-with-instance-method/
  • https://square.github.io/kotlinpoet/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-09-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Kotlin 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Android 重构 | 持续优化统一管理 Gradle
从最初的创建 config.gradle 到现在的 basic_depend.gradle,虽说今天更比昨天强,但是依然不是很满意。
贺biubiu
2020/09/24
1.9K0
Android 重构 | 持续优化统一管理 Gradle
【错误记录】Android Studio 编译报错 ( The minCompileSdk (33) specified in a dependency‘s AAR metadata META-I )
问题是 androidx.core:core:1.9.0. 依赖库中 要求的 minCompileSdk 为 33 , 本应用的编译版本为 32 , 这里设置为 33 即可解决 , 但是这样设置会造成其它问题 ;
韩曙亮
2023/03/30
4.1K0
【错误记录】Android Studio 编译报错 ( The minCompileSdk (33) specified in a dependency‘s AAR metadata META-I )
【错误记录】Android Studio 编译报错 ( kotlin library {0} was compiled with a newer kotlin compiler and not b )
其中涉及到 org.jetbrains.kotlin:kotlin-stdlib:1.7.10 依赖库 ,
韩曙亮
2023/03/30
2.2K0
【错误记录】Android Studio 编译报错 ( kotlin library {0} was compiled with a newer kotlin compiler and not b )
将构建配置从 Groovy 迁移到 KTS
作为Android开发习惯了面向对象编程,习惯了IDEA提供的各种辅助开发快捷功能。
静默加载
2021/06/28
3.8K0
将构建配置从 Groovy 迁移到 KTS
【Android Gradle 插件】Android 依赖管理 ⑤ ( Gradle 依赖优化 | 命令行查看依赖模块 | 依赖冲突问题 | 依赖传递冲突 | 分库冲突 | 依赖分组不同导致冲突 )
① 依赖库版本选择 : 在 build.gradle 构建脚本中 , 如果设置了多个版本的依赖库 , Gradle 构建时会默认选择最高版本的依赖库 ;
韩曙亮
2023/03/30
3.1K0
如何为 Gradle 的 KTS 脚本添加扩展?
现在我们的 Gradle 脚本都迁移到 KTS 了。接下来我们要考虑的问题是,能不能添加一些好用的扩展,方面后续脚本的编写?
bennyhuo
2021/05/14
2.4K0
Android经典面试题之Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用 Kotlin 实现 MVVM(Model-View-ViewModel)模式是开发 Android 应用程序的一种常见架构方式。MVVM 模式将应用程序的 UI 逻辑和业务逻辑分离,使用 LiveData、ViewModel 和 DataBinding 可以使代码更加模块化和可维护。以下是实现 MVVM 模式的步骤和示例:
AntDream
2024/07/22
6470
Android经典面试题之Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. ) 2
在之前遇到过类似问题 【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. The binary ) , 报错的依赖库不同 , 本篇博客再次分析一遍 ;
韩曙亮
2023/03/30
4.5K0
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. ) 2
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. The binary )
报错的模块是 e: C:/Users/octop/.gradle/caches/modules-2/files-2.1/org.jetbrains.kotlin/kotlin-stdlib-common/1.7.10/bac80c520d0a9e3f3673bc2658c6ed02ef45a76a/kotlin-stdlib-common-1.7.10.jar!/META-INF/kotlin-stdlib-common.kotlin_module 模块 , 就是 kotlin-stdlib-common-1.7.10.jar 依赖库 ;
韩曙亮
2023/03/30
9.9K0
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. The binary )
用 Kotlin 脚本(KTS)重塑 Android 工程效能:2000 字终极实践指南
对比 Groovy 的动态类型缺陷,KTS 的静态类型系统能在 编译期拦截 90%+ 的配置错误:
龙小雨
2025/05/15
1070
Android Jetpack - Room
Room 持久化库提供了一个基于 SQLite 的抽象层,以便在利用 SQLite 的全部功能的同时实现更强大的数据库访问
SkyRiN
2019/08/08
2K0
​前端开发者的 Kotlin 之旅:理解 Gradle关键文件与目录
在深入了解具体文件之前,让我们先建立前端构建工具与 Gradle 之间的概念映射:
骑猪耍太极
2025/04/19
1220
​前端开发者的 Kotlin 之旅:理解 Gradle关键文件与目录
Android—Gradle教程(九)完结篇
到目前为止,Gradle基础以及Kotlin基础讲解完毕。因此,在本篇里,将会以Gradle的构建优化以及如何从Groovy迁移到KTS进行详解!
全栈程序员站长
2022/09/07
3.5K0
Android—Gradle教程(九)完结篇
一个C#开发者用Java搭建Android框架的心路历程
Java的框架文章太难写了,因为他引用了太多框架,而没一个框架都有很繁琐的配置,把每个框架都写一遍,就等于写书了;所以本文只能大体上介绍,但大体上介绍,感觉读起来又没有生气,总之非常难写。
Kiba518
2021/11/02
7500
哪怕不学Gradle,这些开发常见操作,你也值得掌握
新建一个 gradle 文件,命名为 xxx.gradle ,复制上述 model 里的配置,放到你的项目中,可以自定义修改一些通用内容,在其他model 中依赖即可,如下所示:
Petterp
2022/01/18
6460
哪怕不学Gradle,这些开发常见操作,你也值得掌握
从精准化测试看ASM在Android中的强势插入-Gradle插件
Gradle Plugin是我们在编译期修改代码的重要武器,也是我们精准化测试的核心组成部分。
用户1907613
2021/07/19
1.2K0
从精准化测试看ASM在Android中的强势插入-Gradle插件
Gradle Kotlin DSL指南
Gradle的Kotlin DSL提供了一种替代传统Groovy DSL的语法,它在受支持的ide中增强了编辑体验,具有更好的内容辅助、重构、文档等功能。本章详细介绍了主要的Kotlin DSL结构,以及如何使用它与Gradle API进行交互。
从大数据到人工智能
2022/01/19
10.9K0
Gradle Kotlin DSL指南
是时候更新手里的武器了—Jetpack架构组件简析
最近两年,MVVM的呼声越来越高,说实话,在经历了MVP的臃肿,MVP的繁琐,我有点怕了。但是这次Google官方带来的一系列为MVVM架构设计的武器—Jetpack,真的让我惊喜到了。
码上积木
2020/09/27
3K0
Android Jetpack架构组件(二)之Lifecycle
一直以来,解藕都是软件开发永恒的话题。在Android开发中,解藕很大程度上表现为系统组件的生命周期与普通组件之间的解藕,因为普通组件在使用过程中需要依赖系统组件的的生命周期。
xiangzhihong
2020/12/21
1.4K0
前端开发者的 Kotlin 之旅:初试Gradle 构建系统
为了学习 Gradle,我们需要一个实际的项目作为载体。Kotlin Multiplatform 项目是一个很好的选择,原因如下:
骑猪耍太极
2025/04/15
1730
前端开发者的 Kotlin 之旅:初试Gradle 构建系统
推荐阅读
Android 重构 | 持续优化统一管理 Gradle
1.9K0
【错误记录】Android Studio 编译报错 ( The minCompileSdk (33) specified in a dependency‘s AAR metadata META-I )
4.1K0
【错误记录】Android Studio 编译报错 ( kotlin library {0} was compiled with a newer kotlin compiler and not b )
2.2K0
将构建配置从 Groovy 迁移到 KTS
3.8K0
【Android Gradle 插件】Android 依赖管理 ⑤ ( Gradle 依赖优化 | 命令行查看依赖模块 | 依赖冲突问题 | 依赖传递冲突 | 分库冲突 | 依赖分组不同导致冲突 )
3.1K0
如何为 Gradle 的 KTS 脚本添加扩展?
2.4K0
Android经典面试题之Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
6470
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. ) 2
4.5K0
【错误记录】Android Studio 编译报错 ( Module was compiled with an incompatible version of Kotlin. The binary )
9.9K0
用 Kotlin 脚本(KTS)重塑 Android 工程效能:2000 字终极实践指南
1070
Android Jetpack - Room
2K0
​前端开发者的 Kotlin 之旅:理解 Gradle关键文件与目录
1220
Android—Gradle教程(九)完结篇
3.5K0
一个C#开发者用Java搭建Android框架的心路历程
7500
哪怕不学Gradle,这些开发常见操作,你也值得掌握
6460
从精准化测试看ASM在Android中的强势插入-Gradle插件
1.2K0
Gradle Kotlin DSL指南
10.9K0
是时候更新手里的武器了—Jetpack架构组件简析
3K0
Android Jetpack架构组件(二)之Lifecycle
1.4K0
前端开发者的 Kotlin 之旅:初试Gradle 构建系统
1730
相关推荐
Android 重构 | 持续优化统一管理 Gradle
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档