聊聊Gradle插件

Gradle插件构建

Posted by Anriku on May 13, 2019

相信作为一个Android开发者,对Gradle这个强大的构建工具一定不陌生。Gradle插件是Gradle作为优秀的构建工具很大的一个特点。在Android中不同的模块下看到的apply plugin:xxx表达式就是插件的使用。一直都是使用别人的插件,今天我们就自己动手来实现一下简单的插件。

为什么要使用Gradle插件?

Gradle插件主要的作用是可以实现代码的构建代码的重用。

脚本插件和对象插件

  • 脚本插件就是通过把重用的构建逻辑单独写一个gradle文件中实现重用。

  • 对象插件则是通过实现org.gradle.api.Plugin来插件的编写。严格意思上来说,这个才能叫真正的插件,前面方式只能叫做一种逻辑的封装。

脚本插件

现在执行下面命令:

mkdir GradleProject # 创建我们的工作目录
mkdir ScriptPlugin # 创建脚本插件的项目目录

touch build.gradle hello.gradle # 创建两个gradle文件

hello.gradle的内容:

task("hello") {
    doLast {
        println "Hello, Script Plugin!"
    }
}

没什么说的,就是创建一个简单的Task。

build.gradle的内容:

apply from: "hello.gradle"

gradle中通过apply函数进行文件引用、插件引用等。这里使用from作为key进行hello.gradle文件的引用。

最后执行:

gradle hello

可以看到成功地执行了名为hello的Task。

buildSrc目录

在讲对象插件之前,先说一下这个目录。每个项目中这个目录中的类会在gradle进行构建的是进行代码的编译。使用对象插件一种简单的方式就是在此目录下写插件相关的逻辑。

只不过这样做有一个很大的缺点就是该项目中进行插件的引用。

对象插件

对象插件可以用任何的JVM语言进行编写,比如说:Groovy、Java、Kotlin等都行。本篇博客中将使用Groovy进行编写。

buildSrc中实现

文件结构总览:

buildSrcPlugin

回到GradleProject目录下,执行下面的命令:

mkdir Plugin
cd Plugin
mkdir -p buildSrc/src/main/groovy/com/anriku/plugin/
mkdir -p buildSrc/src/main/resources/META-INF/gradle-plugins/

// 进行文件的创建
touch build.gradle
buildSrc/src/main/groovy/com/anriku/plugin/HelloPlugin.groovy buildSrc/src/main/groovy/com/anriku/plugin/HelloTask.groovy
buildSrc/src/main/groovy/com/anriku/plugin/HelloExtension.groovy
buildSrc/src/main/resources/META-INF/gradle-plugins/plugin.properties

HelloTask.groovy:

package com.anriku.plugin

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.TaskAction

class HelloTask extends DefaultTask{

  	// @Input指出Task接收的输入,当对应输入为null将会抛异常。解决方法是加上@Optional注解。
    @Input String firstName
    @Input String lastName

  	// @TaskAction指明Task的Action
    @TaskAction
    void greet() {
      	// 这里只能调用getFirstName()和getLastName()(不能使用firstName、lastName)是因为在后面的插件
      	// 中使用的是约定映射conventionMapping
        println "Hello," + getFirstName() + " " +  getLastName()
    }
}

这是一个自定义的Task类。接收firstName、lastName进行一个问候语打印。

HelloTaskExtension.groovy:

package com.anriku.plugin

class HelloTaskExtension {

    String extFirstName
    String extLastName

}

此类为扩展对象,主要作用是为了给插件设置一些在构建脚本中可以设定的属性

HelloPlugin.groovy:

package com.anriku.plugin

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


class HelloPlugin implements Plugin<Project> {

		// 扩展对象在构建脚本中的名字
    static final String EXTENTION_NAME = "helloExtension"

    @Override
    void apply(Project project) {
      	// 给project创建扩展对象
        project.extensions.create(EXTENTION_NAME, HelloTaskExtension)
        project.task("hello", type: HelloTask) { task ->
          	// 读取扩展对象的中设置的值并赋值给Task对应的约定映射
            def extension = project.extensions.findByName(EXTENTION_NAME)
            conventionMapping.firstName = { extension.extFirstName }
            conventionMapping.lastName = { extension.extLastName }
        }
    }
}

自定义的Plugin继承自org.gradle.api.Plugin,覆写的apply方法用于写实现逻辑。

这里主要完成了两个任务:

  • 创建一个名为helloExtension的扩展对象。用于在构建脚本中进行值的设定。
  • 创建一个名为hello的Task。

conventionMapping叫做约定映射,每个继承于DefalutTask的类都有,它的主要作用就是确保扩展属性可以在运行时赋值。

PS:约定映射设置的属性在Task中必须要显示的使用getter进行获取。可以回头看下自定义的Task的实现。

plugin.properties

implementation-class=com.anriku.plugin.HelloPlugin

此文件实现很简单,它作用就是简化apply plugin的使用。

假设文件名为xxx.properties

那么可以使用apply plugin: 'xxx' -> apply plugin: 类的全限定名

PS:其中前面的方式是字符串,后面的方式是类名

build.gradle

// 之所以能这么使用就是上面的文件进行了设定。
apply plugin: "plugin"

// 下面是扩展对象完成的功能
helloExtension {
    extFirstName = "anriku"
    extLastName = "wen"
}

现在在/.../Plugin目录下执行gradle hello,没有出错应该是能成功引用插件。

独立的对象插件

对于上面在buildSrc中实现对象插件的方式有一个问题就是这个对象插件只能够被当前项目所使用。如果想要给多个项目使用这时候就需要独立的对象插件。

文件总览:

standalonePlugin

现在仍然会到GradleProject目录下执行下面的命令

# 创建项目
mkdir StandalonePlugin
cd StandalonePlugin

# 创建目录
mkdir Application
mkdir -p Plugin/src/main/groovy/com/anriku/standaloneplugin
mkdir -p Plugin/src/main/resources/META-INF/gradle-plugins

# 创建文件
touch Plugin/build.gradle Application/build.gradle
Plugin/src/main/groovy/com/anriku/standaloneplugin/HelloPlugin.groovy
Plugin/src/main/groovy/com/anriku/standaloneplugin/HelloTask.groovy
Plugin/src/main/groovy/com/anriku/standaloneplugin/HelloTaskExtension.groovy
Plugin/src/main/reources/META-INF/gradle-plugins/standaloneplugin.properties

HelloPlugin.groovy、HelloTask.groovy、HelloTaskExtension.groovy和上面一节中的基本一样(只是把包名改一下);standaloneplugin.properties的内容和上面一小节的类似(不过同样要把包名改一下)。

Plugin中的build.gradle

plugins {
    id 'groovy'
  	// maven插件用于将上传插件,这里我们进行的是本地插件的部署
    id 'maven'
}

// 插件的classpath将会由下面的参数决定。下面组成的classpath将是com.anriku:standaloneplugin:1.0
group 'com.anriku'
version '1.0'
archivesBaseName = 'standaloneplugin'

dependencies {
   	// 由于在插件的编写中使用到了Gradle相关的类。比如说:org.gradle.api.Plugin。因此这里需要添加对
    // Gradle API的支持。上面在buildSrc目录可以直接使用Gradle API。
    implementation gradleApi()
}

// maven预定的发布插件的Task
uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: "file://$projectDir/../repo")
        }
    }
}

在Plugin目录下的的build.gradle主要是一些发布本地插件相关的构建逻辑。

在Plugin目录下执行gradle uploadArchives命令没有出错的话,应该会产生一个与Plugin目录同级的repo目录。本地插件就以jar包的形式放在此目录下。

Application中的build.gradle

// 在buildScript中添加对刚才发布的插件的依赖
buildscript {
    repositories {
      	// 插件的位置
        maven { url "file://$projectDir/../repo" }
    }

    dependencies {
      	// 插件的classpath,就是刚才对group、version、archivesBaseName的设置。
        classpath 'com.anriku:standaloneplugin:1.0'
    }
}

// 使用插件。当然也可以使用apply com.anriku.standaloneplugin.HelloPlugin
apply plugin: 'standaloneplugin'

// hello Task的扩展对象
helloExtension {
    extFirstName = "anriku"
    extLastName = "wen"
}

在Application目录下执行gradle hello。成功出现Hello,anriku wen就说明成功了。

总结

Gradle插件分为两种:脚本插件和对象插件。

它们的优缺点分别是:

脚本插件:

优点:

  • 编写简单。只需要把重用的逻辑单独放在一个gradle脚本文件中就行。

缺点:

  • 添加的task越多,脚本插件越复杂,并且越难以维护
  • 不能进行单元测试以及集成测试。

对象插件:

优点:

  • 通过继承与特定的类以及实现特定的接口来实现。插件更容易管理和封装。
  • 独立的对象插件(buildSrc中实现的对象插件只能在当前项目中使用)可以上传到网上的仓库中给各个项目以及不同开发者重用。
  • 更容易进行测试。

缺点:

  • 编写较为复杂