Gradle系列2——如何自定以Gradle 插件
声明:本篇以及系列Gradle
相关帖子均是基于gradle-7.3.3
版本
1.简介gradle plugin
每一个gradle release版本分3中类型,以7.3.3版本为例:
在wrapper>gradle-wrapper.properties内:
以
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
声明使用的gradle版本。
- 7.3.3-doc,类型只有文档
- 7.3.3-bin,只有源代码
- 7.3.3-all ,l既有文档又有源代码
1.1 什么是gradle plugin
gradle plugin是应用在AndroidStudio编译脚本中的插件,是一种用于扩展和定制Gradle构建系统功能的机制。
其中打包了可重复利用的编译逻辑,这些逻辑可以是一些便捷的功能便于我们快速实现编译逻辑,也可以包含特定的逻辑实现特定的功能,包括执行测试,打包,生成文档等等,
gradle plugin提供了构建灵活性,可以分享给不同的人,并在不同的项目中应用;
r如“com.android.application”,“com.android.library”,
1.2 编写语言
DSL(Domain Specific Language)领域特定语言,它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。
gradle5.0之后开始支持Kotlin
Groovy 是Apache旗下的一种强大的、可选类型的和动态的语言,具有静态类型和静态编译功能,用于Java平台,旨在通过简洁、熟悉和易于学习的语法提高开发人员的生产力。它与任何Java程序顺利集成,并立即为您的应用程序提供强大的功能,包括脚本功能、领域特定语言创作、运行时和编译时元编程以及函数编程。
并且,Groovy可以与Java语言无缝衔接,可以在Groovy中直接写Java代码,也可以在Java中直接调用Groovy脚本,非常丝滑,学习成本很低。
7.0之后,project 下的build.gradle 发生改变,默认只有plugin配置,其它的逻辑挪到setting.gradle
2. gradle 生命周期
initialization(初始化)阶段
在这个阶段,Gradle 会读取settings.gradle。这个文件内包含插件管理,插件仓库管理,项目依赖仓库管理,最重要的是要读取module,gradle会为每一个module创建一个project。
Configuration (配置)阶段
在这个阶段,Gradle 会读取包含的所有构建脚本,应用插件,使用DSL配置构建,并在最后注册Task。
Execution (执行)阶段
Gradle会根据Task的依赖关系按序执行每一个Task。
2.1 监听gradle生命周期
在setting.gradle内:
println("this is settings")
gradle.addBuildListener(object : BuildListener {
override fun beforeSettings(settings: Settings) {
super.beforeSettings(settings)
println("beforeSettings")
}
override fun settingsEvaluated(settings: Settings) {
println("settingsEvaluated")
}
override fun projectsLoaded(gradle: Gradle) {
println("projectsLoaded:rootProject.name:" + gradle.rootProject.name + ",projectNum:" + gradle.rootProject.allprojects.size)
}
override fun projectsEvaluated(gradle: Gradle) {
println("projectsEvaluated:rootProject.name:" + gradle.rootProject.name + ",projectNum:" + gradle.rootProject.allprojects.size)
}
override fun buildFinished(result: BuildResult) {
println("buildFinished")
}
})
在app module里添加
println("in main module")
android {
println("android config")
//...
}
点击同步,看build输出:
this is settings
settingsEvaluated
projectsLoaded:rootProject.name:BoosterApply,projectNum:3
> Configure project :app
-----------------gradle-rocket start-----------
[SignatureConfigImpl]: start apply signature config >>>
isApplication:true
taskName:[]
configProguardFile buildType:debug
configProguardFile buildType:release
[VersionConfigImpl]:start apply common-version config >>>
javaVersion:11,each:11
[DependenciesConfigImpl]:start apply dependencies configs >>>
[OtherConfigImpl]:start apply other config >>>
-----------------gradle-rocket end -----------
booster config
in main module
android config
> Configure project :lib-support
-----------------gradle-rocket start-----------
[SignatureConfigImpl]: start apply signature config >>>
isApplication:false
project is not app,Booster won`t apply signature
configProguardFile buildType:debug
configProguardFile buildType:release
[VersionConfigImpl]:start apply common-version config >>>
javaVersion:11,each:11
[DependenciesConfigImpl]:start apply dependencies configs >>>
[OtherConfigImpl]:start apply other config >>>
-----------------gradle-rocket end -----------
projectsEvaluated:rootProject.name:BoosterApply,projectNum:3
> Task :prepareKotlinBuildScriptModel UP-TO-DATE
buildFinished
BUILD SUCCESSFUL in 2s
从输出可以看出,上述明显分为3个阶段:
第一阶段读取Settings,然后加载project,这个时候出现3个project(两个子module+rootProject);
第二阶段就是读取module里的build.gradle,配置对应的project。可以看到,在上述先后配置两个module project。"gradle-rocket"是一个插件。
第三阶段,开始执行task,最后buildFinish(此处我只是同步项目,不是打包,但是都是一样的)。
我们的插件就是要在configure阶段做我们的事情。
3.gradle plugin实现
3.1 集成方式
例如我在项目编译完成之后提示编译完成:
在module build.gradle内
class GreetingPlugin:Plugin<Project>{
override fun apply(target: Project) {
println("this is my greetingPlugin !")
//eg:添加一个依赖
target.dependencies.add("api","com.google.code.gson:gson:2.10")
//eg:配置完成输入一段日志
target.afterEvaluate{
println("Greeting plugin ,afterEvaluate")
}
}
}
apply<GreetingPlugin>()
在这个plugin中,做了两件事:添加一个依赖,配置完成输出一段日志。
输出结果:
this is settings
settingsEvaluated
projectsLoaded:rootProject.name:BoosterApply,projectNum:3
> Configure project :app
this is my greetingPlugin !
Greeting plugin ,afterEvaluate
> Configure project :lib-support
projectsEvaluated:rootProject.name:BoosterApply,projectNum:3
> Task :prepareKotlinBuildScriptModel UP-TO-DATE
buildFinished
BUILD SUCCESSFUL in 4s
可以看到输出了我们插件的日志。
然后我们使用gradle app:dependencies
命令查看依赖是否添加成功:
+--- project :app (*)
+--- androidx.test:runner:1.5.2
+--- com.android.support.test.espresso:espresso-core:3.0.2 -> androidx.test.espresso:espresso-core:3.1.0-alpha3
+--- androidx.test.ext:junit:1.1.3
+--- androidx.appcompat:appcompat:1.4.1
...
+--- com.google.code.gson:gson:2.10
+--- androidx.databinding:viewbinding:7.1.2
| \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
+--- com.alibaba:fastjson:2.0.31
...
\--- androidx.lifecycle:lifecycle-common:{strictly 2.4.0} -> 2.4.0 (c)
可以看到打印的依赖树(删减部分)中有在插件中添加的gson。
这种实现插件的方式简单快捷,但会导致项目的配置文件臃肿,不易阅读维护,同时也不方便项目之间共享
3.2 buildSrc
复杂的构建逻辑适合封装在单独的插件中,而且自定义任务和插件的实现也不应该放在build.gradle中。这些逻辑应该自成一体,独立存放,所以buildSrc应用而生,buildSrc就非常的方便了,它在单个项目中使用非常的方便。
buildSrc是一个特殊的included module,无需显式的声明在setting.gradle中;一个项目只能存在一个且必须存在根目录中,gradle会自动识别编译这个目录;buildSrc的执行顺序优先于childProject。
接下来就是实现:
3.2.1 初始化buildSrc
-
创建buildSrc/main 目录
-
创建 buildSrc/build.gradle.kts(当前dsl我是用的是kotlin,也可以使用groovy),添加以下内容:
plugins{
// `kotlin-dsl`
// `groovy`
`java-gradle-plugin`
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
然后同步之后就可以看到:
注意
:Gradle6.4以前需要依赖gradleApi(),之后的版本默认提供支持
3.2.2 创建开发目录以及plugin
在上一步应用的plugins中,有3中方案:
- groovy
- kotlin-dsl
- java-library
三种plugin对应我们插件的3种实现语言,选择某一种语言,就要在main下创建对应的目录。
eg:我们当前选择java语言,那么使用java-library,随后就需要在创建buildSrc/main/src/java/
目录
加入测试类:
public class JavaPlugin implements Plugin<Project> {
@Override
public void apply(Project target) {
System.out.println("hello from javaPlugin ");
}
}
eg:如果使用kotlin语言,即kotlin-dsl,就需要创建buildSrc/main/src/kotlin/
目录
加入测试类:
class GreetingPlugin :Plugin<Project> {
override fun apply(target: Project) {
println("hello from kotlin plugin !")
}
}
3.2.3 绑定plugin
-
首先需要确定你的插件id,插件id:eg:com.example.plugin
-
创建
buildSrc/main/resources/META-INF/gradle-plugins/
目录 -
在该目录创建以你的id命名的文件:com.example.plugin.properties,
内容为:implementation-class=JavaPlugin
此处JavaPlugin对应你的插件的类名称
3.2.4 使用你的插件
在其它module build.gradle内:
plugins {
id("com.android.library")
//---应用我的插件
id("com.example.plugin")
}
android {
namespace = "com.avatr.support"
compileSdk = 31
defaultConfig {
targetSdk = 31
minSdk = 28
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
dependencies {
//使用我的插件内定义的三方库
implementation(Libs.appCompat)
}
到这一步,我们插件已经很强大了,可以给项目的不同module使用,不同module都可以复用相同的逻辑,减少了很多不必要的配置
但是!还是不符合我们团队化使用,不能够在项目&&团队之间共享。
插入一个版本三方库管理方案:
便捷的版本管理方案
相信大家都碰到一个场景,一个项目里有多个module,module需要进行sdk版本管理,三方库管理,java版本等版本管理,每次创建一个新的module都需要花点时间配置一下。
通常的方案是项目根目录创建一个config.gradle,在ext里面统一声明这些内容,然后在build.gradle apply from config.gradle ,然后就可以引用ext里的内容。
在7.0以后,官方推出了一个版本管理方案,Version Catalogs
,是一种新的依赖管理方式,其中一种是通过.toml文件定义所有依赖项和版本信息。这个方法的一个优点是能够集中管理所有依赖的版本,减少版本冲突的可能性。步骤如下
- 在项目的根目录下创建一个名为dependencies.toml的.toml文件,定义依赖项。
# dependencies.toml
[dependencies]
appCompat = "com.android.support:appcompat-v7:28.0.0"
firebaseCore = "com.google.firebase:firebase-core:20.0.0"
# 添加更多依赖...
- 在主项目的settings.gradle.kts文件中,指定Version Catalogs的位置:
// settings.gradle.kts
dependencyResolutionManagement {
// 指定Version Catalogs的位置
versionCatalogs {
create("dependencies") {
from("${rootProject.projectDir}/dependencies.toml")
}
}
}
- 在module的build.gradle.kts文件中引用Version Catalogs,并使用其中的依赖项:
// build.gradle.kts
dependencies {
// 使用Version Catalogs中的依赖项
implementation dependencies.appCompat
implementation dependencies.firebaseCore
// 添加更多依赖...
}
这种方案优点非常明显,使用简单,非常适用于大型多module项目,轻松管理依赖,减少版本冲突,并且可以做到检测升级;小小的缺点就是需要一点小小的学习成本
我们的buildSrc也可以实现三方库版本管理,而且跟为方便。
以我们java语言为例
在buildSrc/src/main/java加入一个依赖管理类
public interface Libs {
String appCompat = "androidx.appcompat:appcompat-resources:1.6.1";
}
gradle给的支持非常强大,你在编辑后面的库信息时,gradle可以自动提示;
- 使用插件的三方库
在module的build.gradle
dependencies {
implementation(Libs.appCompat)
}
这两步,非常简单由直观!
3.3 单独的plugin项目
第二种方案相较于第一种方案,已经灵活便捷很多了,但是这种方案仍然有弊端,它并不是一个独立的个体,而是依附在项目里,依赖项目整体的编译,这种方案适用于单个项目多个module的情况。
我们的目标是最大化的通用性,不仅要实现module之间,更是要在项目和团队之间共享,因此第二种方案还是不够,所以我们介绍第三种这种方案,也是官方推荐的方案,也是大家都采用的方案。
在单独的gradle plugin项目专注于plugin开发,不仅可以进行以上两种方案的实现,更是可以将插件单独发布(虽然第二种方案也可以),但是它具有独立性,专一性。
这一段不进行详细的讲解,详细的讲解将在实战片段讲解。
4.gradle plugin
4.1 初始化项目
初始化的方式可以用手动创建空项目,然后创建名为plugin
的module,然后进行后续配置,但是Gradle 为gradle plugin开发提供了更为便捷的方案:
在你的workSpace打开终端:
4.2 明确需求
- sdk、java版本配置
通常每一个module创建都会有相同的android sdk版本,java版本配置。
- 公共的库依赖
例如单元测试,appCompat,网络等等
- 特性配置
例如viewBinding等等。
4.3 实现sdk版本配置
抽象一个config类:
public interface IRocketConfig {
/**
*
* @param project 当前module
* @param rootExtension 当前module应用的plugin:com.android.library/com.android.application
*/
void applyConfig(Project project, IRootExtension rootExtension);
}
sdk版本配置实现类
public class VersionConfigImpl implements IRocketConfig {
private static final String TAG = "[VersionConfigImpl]:";
@Override
public void applyConfig(Project project, IRootExtension extension, BoosterPlatformConfig platformConfig) {
System.out.println(TAG + "start apply common-version config >>>");
//设置sdk版本号
extension.setCompileSdkVersion(33);
extension.getDefaultConfig().setMinSdkVersion(28);
extension.getDefaultConfig().setTargetSdkVersion(33);
extension.getDefaultConfig().testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner");
//java 版本
extension.getCompileOptions().setSourceCompatibility(JavaVersion.VERSION_11);
extension.getCompileOptions().setTargetCompatibility(JavaVersion.VERSION_11);
}
}
4.4 实现依赖配置
- 实现一个三方库管理类
public interface RocketLibs {
//------------------------------------------Base-------------------------------------------
String androidTestRunner = "androidx.test:runner:" + LibsVersion.androidTestRunner;
String jUnit = "junit:junit:" + LibsVersion.jUnit;
String extjUnit = "androidx.test.ext:junit:" + LibsVersion.extjUnit;
//------------------------------------------View-------------------------------------------
String recyclerView = "androidx.recyclerview:recyclerview:" + LibsVersion.recyclerView;
String appCompat = "androidx.appcompat:appcompat:" + LibsVersion.appCompat;
}
public interface LibsVersion {
String androidTestRunner = "1.5.2";
String espressoCore = "3.0.2";
String jUnit = "4.12";
String recyclerView = "1.3.0";
String appCompat = "1.4.1";
}
- 实现三方库配置类:
public class DependenciesConfigImpl implements IRocketConfig Config {
private static final String TAG = "[DependenciesConfigImpl]:";
@Override
public void applyConfig(Project project, IRootExtension rootExtension) {
System.out.println(TAG + "start apply dependencies configs >>>");
DependencyHandler dependencies = project.getDependencies();
dependencies.add("androidTestImplementation", RocketrLibs.androidTestRunner);
dependencies.add("androidTestImplementation", RocketLibs.espressoCore);
dependencies.add("androidTestImplementation", RocketLibs.extjUnit);
dependencies.add("testImplementation", BoosterLibs.jUnit);
// dependencies.add("api", RocketLibs.commonUtilsAndroid);
dependencies.add("api", RocketLibs.appCompat);
// dependencies.add("api", RocketLibs.okHttp3);
// dependencies.add("api", RocketLibs.recyclerView);
}
}
4.5 特性配置
public class FeatureConfigImpl implements IRocketConfig {
private static final String TAG = "[OtherConfigImpl]:";
@Override
public void applyConfig(Project project, IRootExtension extension, BoosterPlatformConfig config) {
System.out.println(TAG + "start apply other config >>> ");
//使用ViewBinding
if (config != null && config.featureConfig != null) {
extension.getViewBinding().setEnabled(true);
}
}
}
4.6 应用配置
在Plugin内应用上述的实现类
@Override
public void apply(Project project) {
//找到BaseExtension,主要确定module是Application还是Library
final RootExtensionDetector rootExtensionDetector = new RootExtensionDetector(project);
final IRootExtension rootExtension = rootExtensionDetector.getRootExtension();
//签名配置
IRocketConfig signatureConfig = new SignatureConfigImpl();
signatureConfig.applyConfig(project, rootExtension, mAllPlatformConfig);
//sdk版本配置
IRocketConfig versionConfig = new VersionConfigImpl();
versionConfig.applyConfig(project, rootExtension, mAllPlatformConfig);
//依赖配置
IRocketConfig dependenciesConfig = new DependenciesConfigImpl();
dependenciesConfig.applyConfig(project, rootExtension, mAllPlatformConfig);
//特性配置
IRocketConfig otherConfig = new FeatureConfigImpl();
otherConfig.applyConfig(project, rootExtension, mAllPlatformConfig);
//flavor配置
IRocketConfig flavorConfig = new FlavorConfigImpl();
flavorConfig.applyConfig(project, rootExtension, mAllPlatformConfig);
}
以上是应用全部实现我们插件需求的配置类,不再一一展开
4.7 发布插件
我之前的一篇帖子讲过maven-publish 如何发布项目到maven
在plugin的builde.gradle内
//公司仓库地址
val mavenBaseUrl = "http://maven.example.company"
//发布到本地
val isPublish2Local = true;
//绑定plugin
gradlePlugin {
// kotlin版本
val gradleBooster by plugins.creating {
id = "gradle-booster"
implementationClass = "com.example.RoecktPlugin"
}
}
//注册发布任务
publishing {
repositories {
//仓库配置
maven {
url = uri(
//发布到本地
if (isPublish2Local) {
"../../WilsonMaven/Dependency"
} else {
//发布到远程仓库
"$mavenBaseUrl /repository/cockpit-snapshots/"
}
)
//发布到公司maven需要配置密码
if (!isPublish2Local) {
credentials {
username = avatrMavenUserName
password = avatrMavenUserPW
}
}
}
}
//发布版本信息
publications {
create<MavenPublication>("booster") {
groupId = "com.example.company"
//插件id,和上面绑定的插件一致
artifactId = "gradle-rocket"
//插件版本号
version = "0.0.0"
from(components["java"])
}
}
}
然后同步之后,右侧gradle task里面会出现“publish”的task,双击运行这个task,提示发布成功可以查看
查看配置的目录下:
5 使用插件
- 其它应用内根目录build.gradle(或setting.gradle)引入插件仓库
allprojects {
//给项目配置
repositories {
google()
mavenCentral()
maven {
allowInsecureProtocol = true
url = "../WilsonMaven/Dependency"
}
}
}
buildscript {
//给gradle配置插件仓库
repositories {
maven {
allowInsecureProtocol = true
url = "../WilsonMaven/Dependency"
}
}
dependencies {
classpath("com.android.tools.build:gradle:7.1.2")
//引入插件
classpath group: "com.example.company", name: "gradle-rocket", version: "0.0.0"
}
}
classpath group: "com.example.company", name: "gradle-rocket", version: "0.0.0"
就是引入了我们的插件,可以看出来一个完整的插件包括group
,name
(对应插件id),version
和插件配置的内容是一一对应的。
- 在项目的app mudle 应用插件
import com.example.company.libs.RocketLibs
plugins {
id("com.android.application")
//应用插件
id("gradle-rocket")
}
android {
namespace = "com.example.company.app"
defaultConfig {
applicationId = "com.example.company.app"
versionCode = 1
versionName = "1.0"
}
dependencies {
//使用插件内的三方库
implementation(RocketLibs.fastJson)
}
}
- library级别module配置:
plugins {
id("com.android.library")
//---应用我的插件
id("gradle-rocket")
}
android {
namespace = "com.example.support"
}
是不是十分简洁!!!
我们的插件内已经帮我们做了sdk版本配置,java版本配置,签名混淆配置等等我们需要一一去配置的东西
到此,做为一个平台化的gradle插件,已经实现了很多便捷的功能,但是各位大佬仍然可以展开你的想想实现更多的配置,上面我还提到了签名配置
渠道包配置
,由于代码量比较大,我并未贴代码,各位可以自行实现,如果有需要可以找我要代码。
扩展:可以看到上述实例代码,很多配置都是写死的,假如不同的团队有不同的配置怎么办呢,是不是可以本地配置一个json文件,然后插件初始化的时候读取进来,根据文件里的配置来配置项目呢,嘿嘿嘿
下一篇讲如何自定义扩展
参考: