Leo's Studio.

Gradle插件-基础篇

字数统计: 2.4k阅读时长: 9 min
2018/05/16

前言

本文是 Gradle 系列的第三篇,前两篇都是关于 Gradle 多项目构建,有兴趣的同学可以去翻看下。Gradle 系列作者会一直更新下去,这些知识大部分都来自于 Gradle 用户手册,但我并不想写成翻译类型的文章,从最基础的知识开始深入,因为这样前期枯燥的理论知识会让人感到厌倦,所以从用户最常用的知识入手,再穿插必要的基础知识,最终达到知识的融会贯通。因为作者从事 Android 开发,所以会更多提及 Android 中关于 Gradle 的知识。

博客中的源码地址

Gradle插件

Gradle 是非常强大的构建工具,所有的知识都围绕着 project 和 task 这两个知识点。project 在前面的文章中我们已经简单介绍了,task 现在是穿插着讲,后续会考虑单独写成文章。而我们今天讲的 plugin 对于大部分的 Gradle 使用者来说,可能是一个熟悉又陌生的知识点,熟悉是因为用的太频繁了,比如 Android 项目中,我们都会使用以下配置:

1
2
3
4
5
6
7
8
9
apply plugin: 'com.android.application'

android {
compileSdkVersion xxx
buildToolsVersion xxx
defaultConfig {

}
}

但另外一方面,我们对 plugin 的实现又似懂非懂,使用 Android Application Plugin 后,android {} 配置块是如何生成的,又有哪些配置可以用。现在有很多自定义的 Gradle Plugin,比如使用 AOP 实现的代码插桩,打点等等功能的库,那么这些库又是如何通过 Plugin 功能实现的。

接下来,我们将基于 Gradle 用户手册 中关于 Plugin 的知识,从 Plugin 的理论知识到自定义实现 Plugin 写一系列文章。

友情提示:如果想深入理解 Gradle 知识,最好还是从 Gradle 官方文档入手,比如 Gradle 用户手册API 文档 等等,如果只是想快速了解,那么可以继续往下读。

设计

Gradle Plugin 的设计应该符合以下准则:

架构

  • 可复用的逻辑应该写成二进制形式

    Gradle Plugin 有两种形式:脚本插件和二进制插件。脚本插件就是普通的构建脚本,虽然脚本插件也可以组织构建逻辑,但是它不能进行测试,也不能定义可复用的类型,难于长期维护。

  • 考虑性能影响

    虽然在设计 Gradle Plugin 时,可以实现你能想到的任何逻辑,但是应该考虑对构建时间的影响。

  • 约定大于配置

    Gradle Plugin 的设计应该秉承约定大于配置的准则,尽量减少用户陷入繁琐的配置中,又提供配置入口给具有自定义需求的用户。比如 Android Application Plugin 的 sourceSets 配置,如果不进行配置,会使用默认的目录,可以使用 ./gradlew :app:sourceSets 打印默认使用的目录。

  • 功能与约定

    为了让用户能够更灵活地使用你设计的 Plugin,你应该提供更为灵活的使用方式,其中一个方式就是,将实现功能与通用配置分离开来。比如: Java Base PluginJava Plugin

    • Java Base Plugin:提供了 sourceSets 配置属性,用于定义源文件的目录,但它并不会被用于任何构建任务
    • Java Plugin:则是在 Java Base Plugin 提供的 sourceSets 配置上读取源文件,同时定义了具体的构建任务

    如果用户想实现更深层次的定制,则可以继承于 Java Base Plugin 实现自定义构建任务。

    在实现自定义 Plugin 可以这样实现:

    假设 BasePlugin 实现了通用配置:

    1
    2
    3
    4
    5
    public class BasePlugin implements Plugin<Project> {
    public void apply(Project project) {

    }
    }

    MyPlugin 则是基于 BasePlugin 实现了特定功能:

    1
    2
    3
    4
    5
    public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
    proejct.getPlugins().apply(BasePlugin.class);
    }
    }

技术实现

  • 倾向于使用静态类型语言来实现

    Gradle Plugin 的实现语言可以是任何 JVM 语言(即编译产物能在 JVM 上运行),可以是 Java、Kotlin、Groovy 等等。

    但建议使用 Java 或 Kotlin 等静态类型语言来实现,以降低出现不兼容的可能。

  • 使用 Gradle 公开 API 实现

    当实现 Gradle Plugin,需要指定使用的 Gradle API 版本,例如以下配置:

    1
    2
    3
    dependencies {
    compile gradleAPi()
    }

    但因为 Gradle 的历史原因,公开和内部的 API 并没有分离开来,为了确保与各个 Gradle 版本的兼容性,请尽量使用公开的 API

    能在 DSL 文档DOC 文档 中找到的,即为公开的 API

  • 最大程度地减少外部库的依赖

    当我们在编写 Plugin 时,可能会习惯性地去引入某个功能齐全的外部库,比如:Guave,但是这同时也会带来一些问题,可以使用 buildEnvironment task 查看构建环境依赖关系,包括 Plugin:

    1
    2
    3
    4
    classpath
    +--- com.android.tools.build:gradle:2.3.3
    | \--- com.android.tools.build:gradle-core:2.3.3
    | +--- com.android.tools.build:builder:2.3.3

    因为 Gradle Plugin 并没有独立的类加载器运行环境,所以这些依赖可能会与其他 Plugin 的依赖发生冲突。

例子

上面是自定义 Gradle Plugin 的一些规范和注意事项,下面我们通过一个比较简单的自定义 Plugin 来实践下。

这个 Plugin 的功能很简单,创建一个 task,获取配置的属性,再打印出来。

上面我们说到,Plugin 可以分为脚本插件和二进制插件两种,脚本插件就是 Gradle 文件,而二进制文件的可以像普通依赖一样,从中央仓库上下载,还有一种方式是,将源码存放到当前项目下的 buildSrc module,当应用插件时,会从中查找。为了方便起见,我们使用后面那种方式。

  • 首先,我们创建一个名为 buildSrc 的 module,记得在 setting.gradle 中配置,在该 module 的根目录下创建 build.gradle,因为我们打算用 Java 实现,所以先引入 Java Plugin,同时依赖 Gradle API。

    Gradle 同时也提供了一个 Plugin 用于简化这些步骤,具体可以查看 java-gradle-plugin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    apply plugin: 'java-library'

    dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation gradleApi()
    }

    sourceCompatibility = "1.8"
    targetCompatibility = "1.8"
  • 接着,创建一个 Task 类型,命名为 HelloWorld

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class HelloWorld extends DefaultTask {

    public String userName;

    @TaskAction
    public void run() {
    System.out.println("Hello World," + userName);
    }
    }

    一般继承于 DefaultTask 也可以选择其他 Task 基类,@TaskAction 是必须的,用于注解方法为 Task 运行时执行的代码,userName 是可选配置。

  • 其实上一步做完,我们已经可以在其他 module 下的 gradle 文件中直接引用这个 Task 类型,比如:

    1
    2
    3
    4
    5
    import com.example.gradle.HelloWorld

    task hello(type: HelloWorld) {
    userName = "leo"
    }

    执行 ./gradlew :app:hello 输出 “Hello World,leo”

    注意:上面这样做的前提是,这部分的代码位于 buildSrc module,这也是约定配置,符合约定大于配置规范

    在我们这个例子中,我们通过 Plugin 去动态创建一个 Task:

    1
    2
    3
    4
    5
    6
    7
    public final class HelloWorldPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    project.getTasks().create("pluginHello", HelloWorld.class, helloWorld ->
    helloWorld.userName = "leo");
    }
    }

    实现自定义 Plugin 需要实现于 Plugin,接着在 apply() 中添加一个名为 “pluginHello”,类型为 HelloWorld 的 Task。

  • 至此,我们已经写好了 Plugin 的逻辑代码,要想在当前项目中的其他 module 中引用 buildSrc 中的 Plugin,需要给 HelloWorldPlugin 指定一个 ID,注:直接写在构建脚本中的插件则不需要。具体配置如下:

    src/main/resources/META-INF/gradle-plugins/ 目录下创建一个配置文件,命名规则为:***[PluginID].properties***,在我们这个例子中为:com.example.hello.properties

    1
    implementation-class=com.example.gradle.HelloWorldPlugin

    implementation-class 表示 Plugin 类

    Plugin ID 应该符合以下规定:

    • 可以包含任何字母、字符,’.’、’-‘
    • 必须至少包含一个 ‘.’ 分隔命名空间和插件名称
    • 按照惯例,使用域名反向小写作为命名空间
    • 按照惯例,命名空间只使用小写字母
    • org.gradlecom.gradleware 命名空间不能被使用
    • 不能使用 ‘.’ 作为开始或结束字符
    • 不能使用连续的 ‘.’ 字符
  • 至此,我们已经写好了一个简单自定义 Plugin,我们在 app module 下的 build.gradle 引入这个它:

    1
    apply plugin: 'com.example.hello'

    执行 ./gradlew :app:pluginHello 输出 “Hello World,leo”

    上面我们提到了 java-gradle-plugin,如果使用这个 Plugin 的话,可以无需手动配置 properties,只需在 gradle 中配置如下即可:

    1
    2
    3
    4
    5
    6
    7
    8
    gradlePlugin {
    plugins {
    helloPlugin {
    id = 'com.example.hello'
    implementationClass = 'com.example.gradle.HelloWorldPlugin'
    }
    }
    }

插件源码

在上面的例子中,我们将代码写在了 buildSrc module 中,那么除此之外还有:

  • 构建脚本

    可以将 Plugin 的源码直接包含在构建脚本中,这样子可以方便地在当前构建脚本直接使用该 Plugin,但是无法在其他构建脚本中使用。

    build.gradle

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class TestPlugin implements Plugin<Project> {
    void apply(Project project) {
    project.tasks.create('pluginTest') {
    doLast {
    println "This is Test"
    }
    }
    }
    }

    apply plugin: TestPlugin

    需要注意:上面的书写顺序,还记得我们之前说的边读取边解释

  • 单独的项目

    你可以为 Gradle Plugin 创建一个单独的项目,这种方式和 buildSrc 类似,并且可以生成 JAR 上传到中央仓库提供给其他开发者使用,而 buildSrc 只能在当前项目中使用。

结束语

由于篇幅的限制,基础篇我们就讲到这里,后续的文章还会更深入地讲解 Gradle Plugin 知识。

CATALOG
  1. 1. 前言
  2. 2. Gradle插件
    1. 2.1. 设计
      1. 2.1.1. 架构
      2. 2.1.2. 技术实现
    2. 2.2. 例子
    3. 2.3. 插件源码
    4. 2.4. 结束语