Gradle 学习笔记

作者: 唐一川 | 来源:发表于2016-08-22 10:57 被阅读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学习笔记

    相关文章

      网友评论

      • 4e70992f13e7:println 'this project is created by ${author}'
        亲,单引号是不支持运算的
      • 3484cf783db0:settingsEvaluated,projectsLoaded要定义在setting.gradle里面。

      本文标题:Gradle 学习笔记

      本文链接:https://www.haomeiwen.com/subject/rcnwsttx.html