手把手教你学会写 Gradle 插件

前言

很多业务同学在开发的时候,经常会看到公司里面架构组的同事,经常会提供一个插件供大家使用,这些插件有增加编译速度的,有一些字节码插桩的,或者封装了一些一件打包组件上传的功能,看上去还挺 6 的。今天就跟大家介绍一下,什么是 Gradle 插件?为什么要写 Gradle 插件?如何写一个 Gradle 插件?以及书写一个 Gradle 插件需要注意的点有哪些。希望能帮助到各位对插件感兴趣的同学。

什么是 Gradle 插件

首先,我们先解释一下什么是 Gradle 插件?在解释这个之前,我们先解释一下 Gradle,什么是 Gradle?

什么是 Gradle?

这个 Android 开发一直打交道的对象。 Gradle 是一个用于打包代码的工具,他会承担你的项目在打包的时候,项目类型是什么,项目涉及到的代码工程(也就是哪些代码要参与编译打包)是哪些,资源文件,依赖的三方库有哪些,最终的产物是什么。甚至在打包的过程当中,会进行一系统的编译优化等,最终生成对应的 AAR、JAR、APK 等文件。 举个例子:我们在日常使用 C/C++ 进行开发的时候,使用 Visual Studio 写完我们的代码之后,结合 makefile/cmake 来确定我们的源代码,哪些是需要参与打包,包括静态链接库、动态链接库,然后使用 MSVC/GCC/Clang 来进行对 C++ 的库进行编译、链接。很熟悉是不是,android 也可以使用 gradle 配合 jni/ndk 打包 C/C++ 代码为 so 库,这个 so 其实就是对应的 gradle 编译之后的产物。

一句话总结:Gradle 是多语言场景的自动化构建工具,定义构建流程规则。

什么是 Gradle 插件

我们使用 Android Studio 的时候,在项目的根目录下总是有一个 settings.gradle 和 build.gradle。这个是你创建 Gradle 工程的时候,自动生成的。方式就是

groovy 复制代码
gradle init --type

什么?你没创建过 Gradle 工程吗?那是因为你在使用 Android Studio 创建一个新的 application/library 的时候,Android Studio 就自动为我们创建了一个 Gradle 的工程。 而在每个 module 也都有一个 build.gradle,

groovy 复制代码
plugins {
  alias(libs.plugins.android.application)
  alias(libs.plugins.jetbrains.kotlin.android)
  alias(libs.plugins.ktlint)
  alias(libs.plugins.compose.compiler)
  id("androidx.navigation.safeargs")
  id("kotlin-parcelize")
  id("com.squareup.wire")
  id("translations")
  id("licenses")
}

我们可以看下,这里面有一个 libs.plugins.android.application ,这是一个 android 的插件,代表着这个 Gradle 项目是一个可运行的 android application 项目,如果是一个 library,他就是 libs.plugins.android.library,这些都是 Gradle 里面的插件。这个插件会告诉 Gradle 在进行

groovy 复制代码
./gradlew assemble 

的时候,打包的产物是 apk 还是 aar,因为这里设计到编译和打包的过程。所以,只要是涉及到工程的打包编译,那都可以在 Gradle 里面进行操作。我们可以在我们的 build.gradle 里面直接定义 Gradle 的任务,也可以将这个 Gradle 的任务进行封装。举个栗子,我们公司有自己的 maven 仓库,我们各个部门的同学经常需要将自己的打包编译的产物上传到公司的 maven 库里面进去,这个是不是很容易。我们可以在我们的工程里面使用 maven-publish 的插件,然后定义 publish 的任务即可,如下所示:

groovy 复制代码
plugins {
    id 'com.android.library'
    id 'kotlin-android'
    id("maven-publish")      // 引用 Maven 发布插件
}

afterEvaluate {
    // 定义发布任务
    publishing {
        publications {
            // 使用 create 方法,并指定类型
            create('release', MavenPublication) {
                // 注意:必须确保 `release` 变体存在
                from components.release // Groovy 中直接访问 components 的属性

                groupId = publishMavenGroupId
                artifactId = publishMavenArtifactId
                version = publishMavenVersion
            }
        }
        repositories {
            maven {
                url = uri(publishMavenUrl)
                credentials {
                    username = publishMavenUsername
                    password = publishMavenPassword
                }
                allowInsecureProtocol = true
            }
        }
    }
}

但是这样会有如下几个问题:

  • 每个开发人员都需要在自己的工程里面引用上面的插件,设置流程又臭又长,而且还不能很好的按照发布的规范
  • maven 库发布的配置会暴露出去,每个开发都能知道相应的发布地址,账号、密码,这样显然是很不安全的
  • 如果我们需要发布 snapshot 相关的一些产物,我们的 build.gradle 构建代码会越来越长

因此,我们就可以考虑,将上面的这部分 Gradle 配置进行封装,封装成一个插件来供大家使用。

再举个例子:当你需要使用 Jacoco (一个用于统计测试代码覆盖率)的插件,这个 Jacoco 插件需要配置覆盖率原始数据路径、classes/source 构建的目录用于生成覆盖率的高亮,如果你有很多个 module 你需要在 build.gradle 里面配置,并且定义获取 report 的 gradle task 任务,

groovy 复制代码
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'jacoco' // 应用 JaCoCo 插件
}

android {
    compileSdk 33
    
    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 21
        targetSdk 33
        versionCode 1
        versionName "1.0"
        
        // 启用 JaCoCo
        testCoverageEnabled true
    }
    
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    
    // 配置 JaCoCo 版本
    jacoco {
        toolVersion = "0.8.8"
    }
}

// 配置 JaCoCo 报告任务
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    
    // 指定要分析的执行数据文件
    executionData = fileTree(dir: "$buildDir", includes: [
        "jacoco/testDebugUnitTest.exec",
        "outputs/code_coverage/debugAndroidTest/connected/**/*.ec"
    ])
    
    // 指定源代码目录
    sourceDirectories = files([
        "$projectDir/src/main/java",
        "$projectDir/src/main/kotlin"
    ])
    
    // 指定类文件目录
    classDirectories = files([
        "$buildDir/intermediates/javac/debug/classes",
        "$buildDir/tmp/kotlin-classes/debug"
    ].filter { file -> file.exists() })
    
    // 排除不需要分析的包或类
    classDirectories = files(classDirectories.files.collect {
        fileTree(dir: it, exclude: [
            '**/R.class',
            '**/R$*.class',
            '**/BuildConfig.*',
            '**/Manifest*.*',
            '**/*Test*.*',
            'android/**/*.*'
        ])
    })
    
    // 生成 HTML 报告
    reports {
        html.enabled = true
        xml.enabled = true
        csv.enabled = false
        html.destination file("$buildDir/reports/jacoco/jacocoTestReport/html")
    }
    
    // 确保在运行此任务前先运行测试
    doFirst {
        new File("$buildDir/reports/jacoco/").mkdirs()
    }
}

// 添加一个任务来检查代码覆盖率是否达到阈值
task checkCoverage(type: JacocoCoverageVerification, dependsOn: jacocoTestReport) {
    executionData = fileTree(dir: "$buildDir", includes: [
        "jacoco/testDebugUnitTest.exec",
        "outputs/code_coverage/debugAndroidTest/connected/**/*.ec"
    ])
    
    sourceDirectories = files([
        "$projectDir/src/main/java",
        "$projectDir/src/main/kotlin"
    ])
    
    classDirectories = files([
        "$buildDir/intermediates/javac/debug/classes",
        "$buildDir/tmp/kotlin-classes/debug"
    ].filter { file -> file.exists() })
    
    // 排除不需要分析的包或类
    classDirectories = files(classDirectories.files.collect {
        fileTree(dir: it, exclude: [
            '**/R.class',
            '**/R$*.class',
            '**/BuildConfig.*',
            '**/Manifest*.*',
            '**/*Test*.*',
            'android/**/*.*'
        ])
    })
    
    // 设置覆盖率阈值
    rules {
        rule {
            // 对整个项目设置规则
            element = 'BUNDLE'
            
            // 最低覆盖率要求
            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.7 // 70% 的代码覆盖率
            }
            
            limit {
                counter = 'BRANCH'
                value = 'COVEREDRATIO'
                minimum = 0.6 // 60% 的分支覆盖率
            }
        }
    }
    
    // 未达到阈值时失败构建
    violationRules {
        rule {
            limit {
                counter = 'LINE'
                value = 'MISSEDCOUNT'
                maximum = 0
            }
        }
    }
}    

看到没,如果你在每个工程里面去进行配置,1 来容易出错,2 来这个代码真的又臭又长,因此我们可以考虑将这些东西进行封装,封装之后的产物就是 Gradle 插件。

如何创建一个 Gradle 插件

初始化 Gradle 工程

shell 复制代码
# 首先初始化我们的工程
gradle init 

# 选择 Gradle 工程的类型
Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4] 3

# 选择插件实现的语言,大家可以按照自己的习惯选择
Select implementation language:
  1: Java
  2: Kotlin
  3: Groovy
Enter selection (default: Java) [1..3] 2

Project name (default: GradlePluginDemo): GPDDemoByLin  

# 这里是选择我们创建的 Gradle 的插件工程使用的 Gradle 的语言是什么
# 现在推崇的是使用 Kotlin,其实这个对我们会使用 Kotlin 的朋友是非常友好的
# 这里我随便先选择 Groovy,因为可能大家很多的工程也还都是 Groovy 的形式
Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 2

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] no


> Task :init
For more information, please refer to https://docs.gradle.org/8.11.1/userguide/custom_plugins.html in the Gradle documentation.

BUILD SUCCESSFUL in 1m 3s
1 actionable task: 1 executed

这里说几个注意点,如果我们希望我们的 Gradle 插件能被更多的工程所使用的话,建议大家使用的 Gradle 的版本是 7.0 左右的,这样能兼容 jdk 1.8 ,如果你选择版本过高的 Gradle 去编译你的插件,高版本的 Gradle 会不允许使用 jdk 1.8,会强制要求使用 11 或者 17 的 jdk,这样会导致有一些老 Android 工程在使用的时候,造成冲突,当然你也可以要求使用你插件的工程必须升级对应的 jdk 和 gradle 的版本。

初始化之后,我们的工程如下:

kotlin 复制代码
/*
 * This source file was generated by the Gradle 'init' task
 */
package org.example

import org.gradle.api.Project
import org.gradle.api.Plugin

/**
 * A simple 'hello world' plugin.
 */
class GPDDemoByLinPlugin: Plugin<Project> {
    // 在默认情况下,我们的插件通常是在 build.gradle 里面去使用的
    // 所以 gradle 里面使用的也是 Project
    // 但是如果你的插件是需要在 settings.gradle 里面使用的时候,
    // 你需要将 Project 改为 Settings
    override fun apply(project: Project) {
        // Register a task
        project.tasks.register("greeting") { task ->
            task.doLast {
                println("Hello from plugin 'org.example.greeting'")
            }
        }
    }
}

如果你的插件是在 settings.gradle 里面使用,请更改为如下:

kotlin 复制代码
/**
 * A simple 'hello world' plugin.
 */
class GPDDemoByLinSettingPlugin: Plugin<Settings> {
    override fun apply(settings: Settings) {
        // todo
    }
}

那么 settings 和 project 这两者有什么区别呢,这里我简单解释一下:

  1. 插件使用的位置不同,以 settings 为准的插件,只能应用到 settings.gradle 里面,以 project 为准的插件只能使用到 build.gradle 里面
  2. 由于使用位置的不同,你的插件在 gradle 执行的时候,执行的阶段也是不同的,settings 在准备阶段执行,project 发生在构建阶段

具体更多的细节请同学们还是去自行查阅 gradle 的官方资料

我们以封装一个 maven 配置插件为例

我们来封装一个 支持自动发布 Maven 包的 Gradle 插件,目标是:

插件目标功能:

  • 自动配置 maven-publish

  • 内部封装:

    • 发布地址(release 和 snapshot)
    • 认证信息(用户名密码)
  • 由使用者配置:

    • groupId
    • artifactId
    • version
    • release 还是 snapshot(支持自动切换发布地址)

插件实现代码(Kotlin DSL)

MavenPublisherPlugin.kt

kotlin 复制代码
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication

// 我们在这里定义一个扩展类,供使用方在使用的时候能进行配置
open class MavenPublishExtension {
    var groupId: String = ""
    var artifactId: String = ""
    var version: String = ""
    var isRelease: Boolean = true
}

class MavenPublisherPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // 获取扩展类对象
        val ext = project.extensions.create("mavenPublishConfig", MavenPublishExtension::class.java)

        project.afterEvaluate {
            // 应用 maven-publish 插件
            project.plugins.apply("maven-publish")
            // 根据配置来判断是 release 还是 snapshot
            val repoUrl = if (ext.isRelease) {
                "https://your.release.repo/repository/maven-releases/"
            } else {
                "https://your.snapshot.repo/repository/maven-snapshots/"
            }
            
            // 配置对应的 maven 库的信息
            // 通过在这里配置之后,使用我们插件库的同学就无须知道 maven 库的隐私信息了
            project.extensions.configure(PublishingExtension::class.java) { publishing ->
                publishing.repositories { repos ->
                    repos.maven { repo ->
                        repo.url = project.uri(repoUrl)
                        repo.credentials {
                            it.username = "your-username"
                            it.password = "your-password"
                        }
                    }
                }
                // 定义发布任务
                publishing.publications { pubs ->
                    pubs.create("mavenJava", MavenPublication::class.java) { pub ->
                        pub.groupId = ext.groupId
                        pub.artifactId = ext.artifactId
                        pub.version = ext.version

                        if (project.plugins.hasPlugin("java")) {
                            pub.from(project.components.getByName("java"))
                        } else if (project.plugins.hasPlugin("com.android.library")) {
                            pub.from(project.components.getByName("release"))
                        }
                    }
                }
            }
        }
    }
}

插件声明文件

路径:src/main/resources/META-INF/gradle-plugins/com.xxx.mavenpublisher.properties

properties 复制代码
implementation-class=com.example.mavenpublisher.MavenPublisherPlugin

使用方式(在目标项目中)

kotlin 复制代码
plugins {
    id("com.example.mavenpublisher")
}

mavenPublishConfig {
    groupId = "com.your.group"
    artifactId = "your-lib-name"
    version = "1.0.0-SNAPSHOT"
    isRelease = false // false = 发布到 snapshot 仓库
}

然后运行:

bash 复制代码
./gradlew publish

通过上述的方式,我们就能成功将我们的 maven 信息封装成一个 gradle 插件。针对上面另外一个 Jacoco 的例子,我们简单的赘述一下,在配置的时候我们可以对上面的任务进行封装,然后通过遍历工程的形式对所有的 module 进行获取,然后在内部直接设置默认的 classes / source 的路径。 希望看到这里,能对你们开发 Gradle 插件有一点启发。

相关推荐
叽哥42 分钟前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走1 小时前
创建自定义语音录制View
android·前端
用户2018792831671 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831671 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker3 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong3 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil4 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌11 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端
书弋江山12 小时前
flutter 跨平台编码库 protobuf 工具使用
android·flutter
来来走走15 小时前
Flutter开发 webview_flutter的基本使用
android·flutter