亮书房Android知识android实用技术

Gradle 学习笔记

2016-08-22  本文已影响1276人  唐一川

Gradle 学习笔记

以下内容,理论部分大部分是gradle和groovy的官方文档的解释,实例部分是自己的尝试,如有错误,请不吝指正,谢谢!

0x00 groovy 基本知识

groovy与java

groovy 是一个jvm语言,这意味着除了groovy编译器提供的语法糖和groovy标准库以外,它和java没有区别,我们来看一个Hello World

g2j.groovy

def author = 'chenlong'
def getAuthorName(){
    //return author  思考一下为什么这里不能访问到author变量
    return 'chenlong'
}
def sayHello(){
    println 'Hello '+getAuthorName()
}
sayHello()

看一下编译成jvm字节码后的结果

g2j.class

import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;

public class g2j extends Script
{
  public g2j()
  {
    g2j this;
    CallSite[] arrayOfCallSite = $getCallSiteArray();
  }

  public g2j(Binding context)
  {
    super(context);
  }

  public static void main(String[] args)
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray();
    arrayOfCallSite[0].call(InvokerHelper.class, g2j.class, args);
  }

  public Object run()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); 
    Object author = "chenlong";//注意这里,author并不是d2j类的成员变量,所以无法在方法内部访问
    if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass()))     
        return arrayOfCallSite[1].callCurrent(this); 
    else 
        return sayHello(); 
    return null;
  }
  
  public Object getAuthorName()
  {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); 
    return "chenlong"; 
    return null;
  }
  
  public Object sayHello() {
    CallSite[] arrayOfCallSite = $getCallSiteArray(); 
    if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass())) 
        return arrayOfCallSite[2].callCurrent(
                                this, 
                                arrayOfCallSite[3].call("Hello ", arrayOfCallSite[4].callCurrent(this))); 
    else 
        return arrayOfCallSite[5].callCurrent(
                                this,                                 
                                arrayOfCallSite[6].call("Hello ", getAuthorName()));
    return null;
  }
}

闭包

闭包的概念也许我们稍微陌生一点,但是实际上,我们可以简单把它当做一个匿名类,只是编译器提供了更加简单的语法来实现它的功能。

groovy的闭包是一种表示可执行代码块的方法,闭包也是对象,可以像方法一样传递参数,并且可以在需要的地方执行。一个最简单的闭包形如:

def clos = { params ->
    println "Hello ${params}"
}
clos("World")

同时,闭包可以作为参数传递,例如:

def clos = { it ->
    return it%2 == 0
}
def list = [1,2,3,4,5,6]
def filter(list,closure){
    def res = []
    for(i in list){
        if(clos(i))
            res.add()
    }
    return res
}

闭包有三个很重要的属性分别是:this,owner,delegate,分别代表以下概念:

this: 表示定义该闭包的类的实例对象(实例闭包)或者类本身(静态闭包)

owner: 它的含义基本上跟this的含义一样,只是除了一种情况,如果该闭包是在其他的闭包中定义的,那么owner是指向定义它的闭包对象

delegate: 它的含义大多数情况下是跟owner的含义一样,除非它被显示的修改(通过Closure.setDelegate()方法进行修改)

DSL

借助闭包的特性,我们可以尝试写一个简单的DSL。下面的代码展示了如何借助groovy的语法特性来实现一个DSL,这些特性我们稍后会在gradle的脚本中看到。

BookDSL.groovy

class Book {
    def _name = ''
    def _price = 0.0
    def shop = []
    def static config(config){
        Book book = new Book(shop:['A','B'])
        config.delegate = book
        config()
    }
    def name(name){
        this._name = name
    }
    def price(price){
        this._price = price
    }
    def getDetail(){
        println "name : ${_name}"
        println "price : ${_price}"
        println "shop : ${shop}"
    }
}
Book.config {
    name 'test'
    price  1.2
    detail
}

0x01 Gradle 基本概念

1. 基本元素

Project : 需要完成的工作,这里的工作不一定是构建,可以是任何一件事情,当然我们也可以简单的理解它就是我们需要构建的工程。

Script : gradle的脚本文件,通过脚本,我们可以定义一个Project

Task : Project中的具体执行的原子性工作,以构建一个工程为例,它可以是 编译,执行单元测试,发布 等。

2. Script元素

Init Script

似乎从来没有使用过,但是在每一次构建开始之前,都会执行init script,用来设置一些全局变量,有多个位置可以存放init script如下:

USER_HOME/.gradle/ 
USER_HOME/.gradle/init.d/
GRADLE_HOME/init.d/ 

Settings Script

用来在组织多工程的构建,存在于root工程下,settings.gradle

上述script在运行时都会被编译成一个实现了Script接口的class,同时每一个script都有一个委托对象

Build Script -> Project

Init Script  -> Gradle

Settings Script -> Settings

任何没有在脚本中定义的方法或者属性,都会被委托给这个脚本的委托对象执行。

Build Script

每一个build.gradle都是一个Build Scrpit,它由两种元素组成。

statement

可以包含方法调用,属性赋值,局部变量定义等

script blocks

block的概念稍微复杂一点,首先我们先要理解一个groovy的元素,闭包。可以参考上面对闭包的解释

有了闭包的概念,那么理解script block就没有障碍了,我们直接看文档中的定义:

A script block is a method call which takes a closure as a parameter. The closure is treated as a configuration closure which configures some delegate object as it executes.

翻译一下就是

一个脚本块是一个接受一个闭包作为参数的方法,这个闭包在执行的时候配置它的委托对象。

文档描述的比较抽象,我们看一个例子:

build.gradle

def buildVersion = '1.2.0'
def author = 'chenlong'
allprojects {
    println 'this project is created by ${author}'
    setVersion(buildVersion)
}
getAllprojects().each{
    println it.getVersion()
}

首先我们定义了两个变量分别是buildVersionauthor,在执行时,这个两个变量会成为Script Class的属性。然后,我们使用了一个script block,根据定义,这个block对应着一个同名方法allprojects,可是我们并没有在脚本中定义这样一个方法,那它如何执行呢?回想一下我们刚刚看到的build script的委托对象,没错,这个方法被委托给了Project对象执行,查看文档,我们确实在Project中找到了这个同名方法.

接下来,我们在块中写了两行代码,这就是这个闭包需要执行的代码,首先打印一行文字,其次setVersion()。同样的,我们没有定义setVersion这个方法,这就涉及到闭包的一些概念,我们换一种写法

delegate.setVersion(buildVersion)

setVersion 这个方法实际上是由闭包的委托对象执行的,那委托对象是什么呢?我们查阅一下allprojects这个方法的Api,如下

这个闭包的委托对象是当前的project和它的子project,也就是对于一个包含子工程的工程,这个闭包会执行多次,是不是这样呢,我们实验一下,我创建了一个结构如下的工程

在root工程下执行上述脚本,结果如下:

3. Project 对象

每一个build.gradle文件和一个Project对象一一对应,在执行构建的时候,gradle通过以下方式为每一个工程创建一个Project对象:

  1. 创建一个Settings对象,
  2. 根据settings.gradle文件配置它
  3. 根据Settings对象中定义的工程的父子关系创建Project对象
  4. 执行每一个工程的build.gradle文件配置上一步中创建的Project对

在build.gradle文件中定义的属性和方法会委托给Project对象执行,每一个project对象在寻找一个属性的时候有5个作用域作为范围,分别是:

属性可见范围

  1. Project 本身
  2. Project的extra属性
  3. 通过plugin添加的extension,每一个extension通过一个和extension同名的只读属性访问
  4. 通过plugin添加的属性。一个plugin可以通过Project的Convention对象为project添加属性和方法。
  5. project中的task,一个task对象可以通过project中的同名属性访问,但是它是只读的
  6. 当前project的父工程的extra属性和convention属性

方法可见范围

  1. Project对象本身
  2. build.gradle文件中定义的方法
  3. 通过plugin添加的extension,每一个extension都作为一个接受一个闭包/Action作为参数的方法被访问
  4. 通过plugin添加的方法。一个plugin可以通过Project的Convention对象为project添加属性和方法。
  5. project中的task,每一个task 都会在当前project中存在一个接受一个闭包或者/Action作为参数的方法,这个闭包会在task的configure(closure)方法中调用。
  6. 当前工程的父工程中的方法
  7. 当前工程的属性可见范围中所有的闭包属性都可以作为方法访问

4. 构建的生命周期

一次gradle的构建有三个过程

初始化

gradle支持单工程和多工程构建,在初始化的过程中,gradle决定了这次构建包含哪些工程,并且为每一个工程创建一个Project对象。需要注意一点,对于一个多工程的项目,即使通过传入的参数指定了需要构建的子工程,但是,所有在Settings script中包含的工程的build script仍然会执行,因为gradle需要为每一个Project对象配置完整的信息。

配置

在配置的过程中,本次构建包含的所有工程的build script 都会执行一次,同时每个工程的Project对象都会被配置,运行时需要的信息在这个过程中被配置到Projec对象中。最重要的是,在build script中定义的task将在这个过程创建,并被初始化。需要注意的是,在一般情况下,只要在初始化阶段创建的Project对象都会被配置,即使这个工程没有参与本次构建。但是,在1.4之后,一个新的特性被引入,Configuration on demand 如果开启这个选项,那么即使在setting.gradle中包含了工程,但是这个工程不参与本次构建,那么它的Project对象会被创建,但是不会被配置。这也就解释了为什么gradle在多工程构建时运行缓慢了,因为默认情况下,每一个工程的build script都会执行。

执行

gradle通过传入的参数来决定哪些任务会被执行,并生成任务的依赖图(DAG),这意味着,即使在build Script中定义了一个task,但是在构建时这个任务并不会执行,那么这个task对象虽然会被创建,但是不会在任务依赖图中出现。

下面我们通过一个例子来看看gradle的构建生命周期究竟是怎么样的。

工程结构如下:

root Project:gradleStudy
    -- lib
        build.gradle
    -- sub
        build.gradle
    build.gradle
    settings.gradle

root/settings.gradle

println "setting srcipt execute "
rootProject.name = 'gradleStudy'
include ':lib'
include ':sub'

root/build.gradle

println "root build srcipt execute"
allprojects {
    afterEvaluate { project ->
        println "define in root project : after ${project.name} evaluate"
    }
    beforeEvaluate { project ->
        println "define in root project : before ${project.name} evaluate"
    }
}

gradle.settingsEvaluated { settings ->
    println "emit by gradle : Setting evaluated"
}

gradle.projectsLoaded { gradle ->
    println "emit by gradle : project loaded"
}

gradle.beforeProject { project ->
    println "emit by gradle : before ${project.name} evaluate "
}

gradle.afterProject { project ->
    println "emit by gradle : after ${project.name} evaluate "
    
}

gradle.projectsEvaluated { gradle ->
    println "emit by gradle : all project evaluated "
}

gradle.taskGraph.whenReady { graph ->
    println "task graph is ready"
    graph.getAllTasks().each {
        println "task ${it.name} will execute"
    }
}

gradle.taskGraph.beforeTask { task ->
   println "before ${task.name} execute" 
}

gradle.taskGraph.afterTask { task ->
    println "after ${task.name} execute"
}

gradle.buildFinished { buildResult ->
    println "emit by gradle : build finished"
}

println "all project size : ${allprojects.size()}"

lib/build.gradle

println 'lib build script execute'
task libTask0 <<{
    println "${name} execute"
}
task libTask1 << {
   println "${name} execute" 
}
task libTask2(dependsOn:'libTask1')<<{
   println "${name} execute" 
}

beforeEvaluate { project ->
    println "define in lib : before ${project.name} evaluate"
}
afterEvaluate { project ->
    println "define in lib : after ${project.name} evaluate"
}

println "lib project has ${tasks.size()} tasks"

sub/build.gradle

println 'sub build script execute'
task subTask1 << {
   println "${name} execute" 
}
task subTask2(dependsOn:'libTask1')<<{
   println "${name} execute" 
}
beforeEvaluate { project ->
    println "define in sub : ${project.name} evaluate"
}
afterEvaluate { project ->
    println "define in sub : ${project.name} evaluate"
}

在根目录下执行 gradle -q :lib:libTask2 --configure-on-demand

在根目录下执行 gradle -q :lib:libTask2

从上述例子中我们验证了以下结果:

  1. 在configuration demand 模式下执行构建时,虽然sub 工程的Project对象会创建,但是sub/build.gradle并不会执行,并且sub工程的Project不会被配置
  2. 如果一个task在build.gradle中定义,但是在构建中不会执行,那么它的Task对象会创建,但是不会在任务图中出现。
  3. 我们可以通过Gradle或者Project对象中定义的方法获取生命周期中每一个过程在执行中的回调。这里注意一下,我们定义的一些回调在实际执行中似乎并没有被触发,例如,settingsEvaluated,projectsLoaded。这个问题,似乎和gradle的版本更新有关,我正在尝试获取官方的解释。

0x02 写一个简单的插件

我跳过了一些比较简单的内容,例如如何在Build Script中定义一个Task,有兴趣的可以参考文档。

首先看一下工程结构

gradleStudy                      //root工程
    lib                          //子工程
    repo                         //本地maven目录
    simplePlugin                 //plugin工程
    build.gradle                 //root工程的build script
    settings.gradle              //root工程的setting script

simplePlugin工程的目录结构

simplePlugin
    src
        main
            groovy
                com
                    haizhi
                        oa
                            GreetingExtension.groovy     //Extension Model
                            GreetingPlugin.groovy        //插件类
                            SayHelloTask.groovy          //任务类
            resources
                META-INF
                    gralde-plugins
                        greeting-plugin.properties       //插件名
    build.gradle

首先,插件工程可以用任意的jvm语言编写,例如,scala,groovy,java等,最终每一个插件都会打包成一个jar包,其中META-INF文件下中每一个.properties文件代表一个Plugin,里面指明了插件类的全类名,如下:

implementation-class=com.haizhi.oa.GreetingPlugin

GreetingExtension.groovy

package com.haizhi.oa

//定义一个属性类,其中包含message和showAuthorName
class GreetingExtension {
    def message = ''
    def showAuthorName = false
}

GreetingPlugin.groovy

package com.haizhi.oa
import org.gradle.api.Project
import org.gradle.api.Plugin

class GreetingPlugin implements Plugin<Project>{
    @Override
    public void apply(Project project){
        //创建一个类型为GreetingExtension,名称为greeting的属性
        project.extensions.create('greeting',GreetingExtension) 
        //创建一个类型为SayHelloTask,名称为sayHello的任务
        project.task('sayHello',type:SayHelloTask)
    }
}

SayHellTask.groovy

package com.haizhi.oa
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class SayHelloTask extends DefaultTask{
    //定义一个属性
    def author = ''
    @TaskAction
    public void sayHello()  {
        //从Project中获取greeting属性
        if(project.greeting.showAuthorName){
            println "Hello ${author} ${project.greeting.message}"   
        }else{
            println "Hello ${project.greeting.message}" 
        }
    }
}

build.gradle

apply plugin: 'groovy'
apply plugin: 'maven'

//定义发布到maven的版本,group,name
version = '1.0.0'
group = 'com.haizhi.oa'
archivesBaseName='greeting-plugin'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.codehaus.groovy:groovy-all:2.4.4'
    compile gradleApi()
    compile localGroovy()
}
uploadArchives{
    repositories.mavenDeployer{
        repository(url : 'file:../repo')//定义本地maven地址
    }
}

在simplePlugin的根目录下执行,gralde uploadArchives,编译插件工程,并发布到../repo目录。

以上,我们就创建好了一个gradle plugin,那么如何使用它呢?

首先,在root工程下,我们通过buildscript引入插件

buildscript{
    repositories{
        mavenCentral()
        maven {
            url uri('./repo')
        }
    }
    dependencies {
        classpath 'com.haizhi.oa:greeting-plugin:1.0.0'
    }
}

然后,在lib工程下,我们应用这个插件,

apply plugin : 'greeting-plugin'
greeting.message = 'this is a simple plugin' //为greeting属性赋值
greeting.showAuthorName = true               //为greeting属性赋值
sayHello.author = 'chenlong'                 //为sayHello任务的author属性赋值

似乎和我们通常看到的插件配置方式不太一样,我们换一种配置方式

apply plugin : 'greeting-plugin'
greeting {
    message 'this is a simple plugin'   
    showAuthorName true
}
sayHello {
    author 'chenlong'   
}

看上去是不是熟悉了很多

下面,在lib或者根目录下执行,gradle sayHello,执行结果如下:

以上就是一个自定义插件的创建和应用过程,虽然很简单,但是可以帮助我们理解gradle是如何通过plugin完成很多复杂的工作的。

0x03 总结

gradle在定义一个构建工作的时候,主要是围绕着它的两个核心类,Project和Task,在这两个类的基础上,gradle定义了它的规则,例如它的生命周期,例如task的依赖图。在保证了这些核心功能后,所有外围的功能都是通过Plugin来进行的,这一点非常值得我们学习。同时,task的依赖图这个功能也是gradle的一个很强大的功能,它让我们有机会在任务执行之前就能对本次构建有一个全局的理解,同时也提供了接口让我们hook到task的执行过程中,完成一些非常酷炫的功能。


原文:Gradle学习笔记

上一篇下一篇

猜你喜欢

热点阅读