• Groovy&Gradle总结


    欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)
    我的github地址:https://github.com/jeasonlzy

    0x01 Groovy 概述

    Groovy 是一个基于 JVM 的语言,代码最终编译成字节码(bytecode),并在 JVM 上运行。它具有类似于 Java 的语法风格,但是语法又比 Java 要灵活和方便,同时具有动态语言(如 ruby 和 Python)的一些特性。

    正因为如此,所以Groovy适合用来定义DSL(Domain Specific Language)。

    简单的来讲 DSL 是一个面向特定小领域的语言,如常见的 HTML、CSS 都是 DSL,它通常是以配置的方式进行编程,与之相对的是通用语言(General Purpose Language),如 Java 等。

    0x02 groovy 基本知识

    1)首先需要安装groovy环境,具体的环境安装就不说了,网上很多,安装完成后配置环境变量,出现以下结果,即安装成功

    2)groovy与java

    因为Groovy是基于JVM的语言,所以我们来看看最后生成的字节码文件。我们写一个类:
    hello.groovy

    name = "lzy"
    def say(){
        "my name is $name"
    }
    println say()
    • 1
    • 2
    • 3
    • 4
    • 5

    在命令行输入groovy hello.groovy,运行脚本,输出以下结果:

    上面的操作做完后,有什么感觉和体会?
    最大的感觉可能就是groovy和shell脚本,或者python好类似。
    另外,除了可以直接使用JDK之外,Groovy还有自己的一套GDK。

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

    我们输入groovyc -d classes hello.groovy命令将当前文件生成字节码文件,-d参数表示在classes文件夹下,最终结果如下:
    hello.class

    import groovy.lang.Binding;
    import groovy.lang.Script;
    import org.codehaus.groovy.runtime.BytecodeInterface8;
    import org.codehaus.groovy.runtime.GStringImpl;
    import org.codehaus.groovy.runtime.InvokerHelper;
    import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
    import org.codehaus.groovy.runtime.callsite.CallSite;
    
    public class hello extends Script {
        public hello() {
            CallSite[] var1 = $getCallSiteArray();
        }
    
        public hello(Binding context) {
            CallSite[] var2 = $getCallSiteArray();
            super(context);
        }
    
        public static void main(String... args) {
            CallSite[] var1 = $getCallSiteArray();
            var1[0].call(InvokerHelper.class, hello.class, args);
        }
    
        public Object run() {
            CallSite[] var1 = $getCallSiteArray();
            String var2 = "lzy";
            ScriptBytecodeAdapter.setGroovyObjectProperty(var2, hello.class, this, (String)"name");
            return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()?var1[3].callCurrent(this, this.say()):var1[1].callCurrent(this, var1[2].callCurrent(this));
        }
    
        public Object say() {
            CallSite[] var1 = $getCallSiteArray();
            return new GStringImpl(new Object[]{var1[4].callGroovyObjectGetProperty(this)}, new String[]{"my name is ", ""});
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    到这里我们可以发现,其实groovy脚本本质就是java,他与java几乎没有区别,只是在java语言在语法上的扩展,支持DSL,增强了可读性。并且我们得出以下结论:
    1. hello.groovy被转换成了一个hello类,它从Script派生。
    2. 每一个脚本都会生成一个static main函数。这样,当我们groovy hello.groovy去执行的时候,其实就是用java去执行了这个main函数
    3. 脚本中的所有代码都会放到run函数中。比如,say()方法的调用,这句代码实际上是包含在run()方法里的。
    4. 如果脚本中定义了方法,则方法会被定义在hello类中。
    5. 脚本中定义的变量是有它的作用域的,name = “lzy”,这句话是在run()中创建的。所以,name看起来好像是在整个脚本中定义的,但实际
    上say()方法无法直接访问它。

    接着我们把上述的hello.groovy文件修改,在定义name前的加上def修饰符,其余不做任何修改,我们再次运行代码,发现以下错误:

    我们将修改后的代码编译成class文件后,与之前的正常结果做对比,发现以下不同:

    左边是正确的,右边是错误的,相比下来就是多调用了一个方法,这个方法看起来就是将你定义的属性保存到了某个全局的环境中,确保下面的say()方法在调用的时候,能从全局取到这个属性。

    ScriptBytecodeAdapter.setGroovyObjectProperty(var2, hello.class, this, (String)"name");
    • 1

    但是这样还是与我们的想象有差距,name并没有在成员位置,那如何才能才能让我们定义的属性就生成在成员变量的位置呢?这时候需要@Field注解,如下:

    import groovy.transform.Field
    
    @Field name = "lzy"
    def say(){
        "my name is $name"
    }
    println say()
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    加上这行注解后,生成的字节码如下:
    name属性确实变成了成员变量,并且是在构造方法中被初始化了。

    import groovy.lang.Binding;
    import groovy.lang.Script;
    import org.codehaus.groovy.runtime.BytecodeInterface8;
    import org.codehaus.groovy.runtime.GStringImpl;
    import org.codehaus.groovy.runtime.InvokerHelper;
    import org.codehaus.groovy.runtime.callsite.CallSite;
    
    public class hello extends Script {
        Object name;
    
        public hello() {
            CallSite[] var1 = $getCallSiteArray();
            String var2 = "lzy";
            this.name = var2;
        }
    
        public hello(Binding context) {
            CallSite[] var2 = $getCallSiteArray();
            super(context);
            String var3 = "lzy";
            this.name = var3;
        }
    
        public static void main(String... args) {
            CallSite[] var1 = $getCallSiteArray();
            var1[0].call(InvokerHelper.class, hello.class, args);
        }
    
        public Object run() {
            CallSite[] var1 = $getCallSiteArray();
            Object var10000 = null;
            return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()?var1[3].callCurrent(this, this.say()):var1[1].callCurrent(this, var1[2].callCurrent(this));
        }
    
        public Object say() {
            CallSite[] var1 = $getCallSiteArray();
            return new GStringImpl(new Object[]{this.name}, new String[]{"my name is ", ""});
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    0x03 Groovy 语法

    这里只讲一些比较重要的特性,其余比较基本的语法比较简单,可以参考这里过一遍:
    工匠若水的博客:Groovy脚本基础全攻略

    1)方法的输入参数优化

    groovy中定义的函数,如果至少有一个参数,在调用的时候可以省略括号。如果某个函数没有参数,那就不能省略括号,否则会当成一个变量使用。

    def func(String a){
        println(a)
    }
    
    func 'hello'
    • 1
    • 2
    • 3
    • 4
    • 5

    在android项目中,比如build.gradle

    android {
        compileSdkVersion 25
        buildToolsVersion "25.0.0"
    }
    • 1
    • 2
    • 3
    • 4

    比如这里compileSdkVersion 和 buildToolsVersion 其实就是调用了同样名字的两个函数,在AndroidStudio里面可以点进去查看函数实现

    2)闭包

    闭包的概念也许我们稍微陌生一点,但是实际上,我们可以简单把它当做一个匿名类,只是编译器提供了更加简单的语法来实现它的功能。
    闭包(Closure)是groovy中一个很重要的概念,而且在gradle中广泛使用。简而言之,闭包就是一个可执行的代码块,类似于C语言中的函数指针。在很多动态类型语言中都有广泛的使用,java8 中也有类似的概念:lambda expression,但是groovy中的闭包和java8中的lambda表达式相比又有很多的不同之处。

    我们可以把闭包当做一个匿名内部类,只是编译器提供了更加简单的语法来实现它的功能。在Groovy中闭包也是对象,可以像方法一样传递参数,并且可以在需要的地方执行。

    def clos = { params ->
        println "Hello ${params}"
    }
    clos("World")
    
    //Closure类型的实例,比如上面的闭包我们又可以定义为:
    //参数可以声明类型,也可以不声明,还可以有缺省值
    Closure clos1 = { a, def b, int c = 2 ->
        a + b + c //默认返回值就是最后一行计算的结果,return关键字可省略
    }
    println clos1(5, 3)
    
    //可以制定一个可选的返回类型
    //如果闭包内没有生命任何参数,没有->, 那么闭包内置会定义一个隐含参数it
    Closure<String> clos2 = {
        println it
        return "clos2"
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    • this: 对应于定义闭包时包含他的class,可以通过getThisObject或者直接this获取
    • owner: 对应于定义闭包时包含他的对象,可以通过getOwner或者直接owner获取
    • delegate: 闭包对象可以指定一个第三方对象作为其代理,用于函数调用或者属性的指定,可以通过getDelgate或者delegate属性获取

    我们编写如下代码:test1.groovy

    class A {
        def closure1 = {
            println "--------------closure1--------------"
            println "this:" + this.class.name
            println "owner:" + owner.class.name
            println "delegate:" + delegate.class.name
            def closure2 = {
                println "-------------closure2---------------"
                println "this:" + this.class.name
                println "owner:" + owner.class.name
                println "delegate:" + delegate.class.name
                def closure3 = {
                    println "-------------closure3---------------"
                    println "this:" + this.class.name
                    println "owner:" + owner.class.name
                    println "delegate:" + delegate.class.name
                }
                closure3()
            }
            closure2()
        }
    }
    
    def a = new A()
    def closure1 = a.closure1
    closure1()
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    运行后得到如下结果:

    --------------closure1--------------
    this:com.lzy.A
    owner:com.lzy.A
    delegate:com.lzy.A
    -------------closure2---------------
    this:com.lzy.A
    owner:com.lzy.A$_closure1
    delegate:com.lzy.A$_closure1
    -------------closure3---------------
    this:com.lzy.A
    owner:com.lzy.A$_closure1$_closure2
    delegate:com.lzy.A$_closure1$_closure2
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3)代理策略

    如果在闭包内,没有明确指定属性或者方法的调用是发生在this, owner,delegate上时,就需要根据代理策略来判断到底该发生在谁身上。有如下几种代理策略:

    • Closure.OWNER_FIRST 默认的策略,如果属性或者方法在owner中存在,调用就发生在owner身上,否则发生在delegate上
    • Closure.DELEGATE_FIRST 跟owner_first正好相反
    • Closure.OWNER_ONLY 忽略delegate
    • Closure.DELEGATE_ONLY 忽略owner
    • Closure.TO_SELF 调用不发生在owner或delegate上,只发生在闭包内

    我们编写如下代码:test2.groovy

    import groovy.transform.Field
    
    @Field String name = "abc"
    
    class P {
        String name
        def pretty = {
            "my name is $name"
        }
    }
    
    class T {
        String name
    }
    
    def upper = {
        name.toUpperCase()
    }
    println upper()
    
    def p = new P(name: 'ppp')
    def t = new T(name: 'ttt')
    
    upper.delegate = t
    upper.resolveStrategy = Closure.DELEGATE_FIRST
    println upper()
    
    p.pretty.delegate = this
    println p.pretty()
    p.pretty.resolveStrategy = Closure.DELEGATE_FIRST
    println p.pretty()
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行后得到如下结果:

    ABC
    TTT
    my name is ppp
    my name is abc
    • 1
    • 2
    • 3
    • 4

    这里重点强调一下,成员变量name一定要加上@Field注解,否者出现的结果一定不是上述结果,原因之前已经分析过,不加这个注解,name属性将不会是成员变量

    4)类的Property

    Groovy中的class和java中的Class区别不大,值得我们关注的区别是,如果类的成员变量没有加任何权限访问,则称为Property, 否则是Field,filed和Java中的成员变量相同,但是Property的话,它是一个private field和getter setter的集合,也就是说groovy会自动生成getter setter方法,因此在类外面的代码,都是会透明的调用getter和setter方法

    我们在上述的test1.groovy的类A中加入以下几行代码:

    String name = "aaa"
    public String name1 = "bbb"
    private String name2 = "ccc"
    • 1
    • 2
    • 3

    然后对这个test1.groovy进行编译groovyc -d classes test1.groovy,生成的文件结构如下:

    点开A.class发现以下代码:

    public class A implements GroovyObject {
        private String name;
        public String name1;
        private String name2;
        private Object closure1;
    
        ···
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String var1) {
            this.name = var1;
        }
    
        ···
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5)操作符重载

    我们经常在gradle中看见以下代码:

    task hello << {
        println "hello world"
    }
    • 1
    • 2
    • 3

    实际上他的原始调用含义是:

    task("hello").leftShift(closure)
    • 1

    因为task重载了leftShift,所以可以使用 << 操作符,这和c++的特性是一样的

    6)Command Chains

    这个特性不仅可以省略函数调用中的括号,而且可以省略,连续函数调用中的. 点号, 比如
    a(b).c(d) 这里a c是函数, b d是函数参数, 就可以缩写为a b c d。这个特性强大之处在于不仅适用于单个参数类型函数,而且适用于多个参数类型的函数,当参数类型为闭包时同样适用。

    task("task1").doLast({
        println "111"
    }).doLast({
        println("222")
    })
    //简写
    task task1 doLast { println "111" } doLast { println("222") }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    7)DSL

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

    test3.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
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    上面所提到的这些groovy的语法特性,构成了Gradle中DSL的基础

    0x04 Gradle 基本概念

    我们在AndroidStudio中创建基于Gradle的project时,会默认生成一个多项目结构的Gradle工程,他有如下结构:

    ├── app
    │   └── build.gradle
    ├── lib
    │   └── build.gradle
    ├── build.gradle
    └── settings.gradle
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果是单工程结构,这个Setting.gradle其实可以省略

    Gradle中,每一个待编译的工程,或者叫每一个Library和App都是单独的Project。根据Gradle的要求,每一个Project在其根目录下都需要有一个build.gradle。build.gradle文件就是该Project的编译脚本,类似于Makefile。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。具体一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西。

    apply plugin: 'com.android.application' //app插件
    apply plugin: 'com.android.library'     //lib插件
    
    android{
        ...
    }  
    dependencies{
      ....
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果我们把以上代码在build.gradle中删掉,执行gradle tasks命令列出所有可执行的task,会发现很多task都不见了,由此我们能得出结论,

    这些task都是android application插件生成的。我们能使用Gradle构建Android 工程,一切都基于这个插件。这个插件从android这个扩展中读取了我们的配置,生成了一些列构建android 所需要的任务。

    我们执行gradle projects列出所有的工程:

    这个图片的最后几句话也告诉了我们,如果你在根目录下,想查看或者运行某个项目下的任务,可以用gradle :app:tasks这种语法,如果你cd到子项目的根目录下,是不需要加:app这样的前缀的。

    对Android来说,gradle assemble这个Task会生成最终的产物Apk,所以如果一个工程包含5个Model,那么需要分别编译这5个,他们可能还有一些依赖关系,这样就很麻烦了,而在Gradle中,是支持多工程编译的(Multi-Projects Build),我们在根目录下直接执行gradle assemble,就能按照依赖关系把这5个Model全部编译出来生成最终的Apk,但是为什么可以呢?

    1. 我们需要在根目录下也添加一个build.gradle。这个build.gradle一般干得活是:配置其他子Project的。比如为子Project添加一些属性。这个build.gradle有没有都无所谓。

    2. 继续在根目录下添加一个名为settings.gradle。这个文件很重要,名字必须是settings.gradle。它里边用来告诉Gradle,这个multi-projects包含多少个子Project,内部一般就是一个include指令。根据groovy的语法,他就是在gradle生成的settings对象调用函数 include(‘app’),include接受的参数是一个string数组,因此include后可以加很多参数,这个函数的意义就是:指明那些子project参与这次gradle构建

    所以对于一个工程,我们能对构建过程做出改变的,就只能发生在这些.gradle文件中,这些文件称为Build Script构建脚本。对于Gradle中的构建脚本,一方面可以理解为配置文件,每一种类型脚本文件都是对某一种类型的构建对象进行配置。另一方面也可以把每个脚本理解为一个Groovy闭包,这样我们在执行构建脚本时,就是在执行每一个闭包函数,只不过每个闭包所设置的delegate不一样。

    以下来自于文档:Gradle Build Language Reference这个文档很重要,后面会经常使用!!!

    • Project对象:每个build.gradle会转换成一个Project对象,或者说代理对象就是Project。
    • Gradle对象:当我们执行gradle xxx或者什么的时候,gradle会从默认的配置脚本中构造出一个Gradle对象。在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是Gradle。我们一般很少去定制这个默认的配置脚本。
    • Settings对象:每个settings.gradle会转换成一个Settings对象,或者说代理对象是Setting。

    补充一点:Init Script其实就是配置gradle运行环境。似乎从来没有使用过,但是在每一次构建开始之前,都会执行init script,我们可以对当前的build做出一些全局配置,比如全局依赖,何处寻找插件等。有多个位置可以存放init script如下:
    1. 通过在命令行里指定gradle参数 -I 或者–init-script

    1)Build生命周期

    Gradle的构建脚本生命周期具备三大步,如下:

    上图告诉我们以下信息,

    1. Gradle工作包含三个阶段:
    2. 首先是初始化阶段。对我们前面的multi-project build而言,就是执行settings.gradle
    3. Initiliazation phase的下一个阶段是Configration阶段。
    4. Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build例子中,
      解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook。这当然是通过
      API来添加的,需要特别注意:每个Project都会被解析。
    5. Configuration阶段完了后,整个build的project以及内部的Task关系就确定了。一个
      Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的
      依赖关系,是一个有向图,所以,我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作.
    6. 最后一个阶段就是执行任务了。你在gradle xxx中指定什么任务,gradle就会将这个xxx任务链上的所有任务全部按依赖顺序执行一遍!当然,任务执行完后,我们还可以加Hook。

    gradle具有以下几个常用的命令:
    gradle tasks //列出所有的任务
    gradle projects //列出所有的项目
    gradle properties //列出所有的属性

    2)Setting对象

    先看文档,方法文档都在文档中:Settings
    其中有这么句话比较重要:

    In addition to the properties of this interface, the Settings object makes some additional read-only properties available to the settings script. This includes properties from the following sources:

    • Defined in the gradle.properties file located in the settings directory of the build.
    • Defined the gradle.properties file located in the user’s .gradle directory.
    • Provided on the command-line using the -P option.

    翻译后就是,除了Setting这个接口自己提供的属性方法外,你还可以在以下位置添加自己的额外属性:
    - setting.gradle 平级目录下的 gradle.properties 文件
    - 用户.gradle目录下的 gradle.properties 文件
    - 使用命令行 -P 属性

    其余的文档中比较详细。

    3)Project对象

    以下内容均来自与文档:Project

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

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

    其中有很多有用的方法:

    //apply一个插件或者脚本
    void apply(Map<String, ?> options);
    //配置当前project的依赖
    void dependencies(Closure configureClosure);
    //配置当前脚本的classpath
    void buildscript(Closure configureClosure);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    属性可见范围

    • 1.Project 本身
    • 2.Project的ext属性
    project.ext.prop1 = "prop1"
    ext.prop2 = "prop2"
    project.ext {
        prop3 = "prop3"
        prop4 = "prop4"
    }
    ext {
        prop5 = "prop5"
        prop6 = "prop6"
    }
    
    println project.ext.prop1
    println project.ext.prop2
    println project.prop3
    println project.prop4
    println prop5
    println prop6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 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,每个extensions都可以作为一个方法访问,它接受一个闭包或Action作为参数
    4. 通过plugin添加的方法。一个plugin可以通过Project的Convention对象为project添加属性和方法。
    5. project中的task,每一个task 都会在当前project中存在一个接受一个闭包或者Action作为参数的方法,这个闭包会在task的configure(closure)方法中调用。
    6. 当前工程的父工程中的方法
    7. 当前工程的属性可见范围中所有的闭包属性都可以作为方法访问

    4) Task对象

    先来文档 Task

    A Task is made up of a sequence of Action objects. When the task is executed, each of the actions is executed in turn, by calling Action.execute(T). You can add actions to a task by calling Task.doFirst(org.gradle.api.Action) or Task.doLast(org.gradle.api.Action).

    Groovy closures can also be used to provide a task action. When the action is executed, the closure is called with the task as parameter. You can add action closures to a task by calling Task.doFirst(groovy.lang.Closure) or Task.doLast(groovy.lang.Closure).

    There are 2 special exceptions which a task action can throw to abort execution and continue without failing the build. A task action can abort execution of the action and continue to the next action of the task by throwing a StopActionException. A task action can abort execution of the task and continue to the next task by throwing a StopExecutionException. Using these exceptions allows you to have precondition actions which skip execution of the task, or part of the task, if not true.

    创建一个简单的task的语法为:

    task <taskName> << {
    }
    • 1
    • 2

    这句话应该这么理解:

    1. 首先调用project的task方法,传入一个taskName,返回一个task
    2. 调用task的leftShift 方法 传入一个closure,根据leftShift的解释,我们知道这个闭包将添加到task的action list里去,在任务执行的时候运行

    有时候,我们可能会错写成

    task <taskName> {
    }
    • 1
    • 2

    少了这个<< 操作符,意思就完全不一样了,这个时候调用的函数为

    //Project类
    Task task(String name, Closure configureClosure);
    • 1
    • 2

    这时,第二个参数closure用来配置task,在task创建的时候,也就是构建整个任务有向图的时候执行,而不是在task执行的时候运行。不过我们可以在这个闭包内配置task的一些属性。

    //指定Copy类型task的属性
    task copyDocs(type: Copy) {
       from 'src/main/doc'
       into 'build/target/doc'
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    当然我们还可以这样指定task的行为:

    task exampleTask {
        doLast{
    
        }
    }
    
    task exampleTask doLast{
    
    }
    
    task exampleTask << {
    
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    还可以指定task的类型

    task name(type: Type){
        doLast{
    
         }
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    gradle内置为我们生成了很多task类型,比如Copy,Delete,可以点击链接 查看gradle内置的task类型列表,如果创建task时没有指定type,则他默认是DefaultTask类型。我们还可以创建自己的task类型,我们在稍后就会讲到。

    我们还可以可以指定task之间的依赖关系, 通过dependsOn, mustRunAfter, shouldRunAfter来指定。 还可以指定task的分组group, 如果不指定,将会出现在other里面。

    5) 构建的生命周期测试

    以下各个方法参考文档:
    Gradle相关
    Task相关

    ├── app
    │   └── build.gradle
    ├── lib
    │   └── build.gradle
    ├── build.gradle
    └── settings.gradle
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    root/settings.gradle

    println '------setting.gradle execute------'
    
    include ':app', ':lib', ':helloPlugin', ':simplePlugin'
    • 1
    • 2
    • 3

    root/build.gradle

    println '------root build.gradle execute------'
    
    apply from: uri('./build_1.gradle')
    apply from: uri('./build_2.gradle')
    
    println "all project size : ${allprojects.size()}"
    
    gradle.settingsEvaluated { settings ->
        println "settingsEvaluated"
    }
    
    gradle.projectsLoaded { gradle ->
        println "projectsLoaded"
    }
    
    gradle.beforeProject { project ->
        println "beforeProject: ${project.name} "
    }
    
    gradle.afterProject { project ->
        println "afterProject: ${project.name}"
    }
    
    gradle.projectsEvaluated { gradle ->
        println "projectsEvaluated"
    }
    
    gradle.buildFinished { buildResult ->
        println "buildFinished"
    }
    
    gradle.taskGraph.whenReady { graph ->
        println "============task graph is ready============"
        graph.getAllTasks().each {
            println "task ${it.name} will execute"
        }
        println "============task graph is over============="
    }
    
    gradle.taskGraph.beforeTask { task ->
        println "before ${task.name} execute"
    }
    
    gradle.taskGraph.afterTask { task ->
        println "after ${task.name} execute"
    }
    
    tasks.whenTaskAdded { task ->
        println "taskAdded:" + task.name
    }
    
    task subTask1 {
        group "hello"
        doLast {
            println "${name} execute"
        }
    }
    
    task subTask2(dependsOn: 'subTask1') {
        group "hello"
        doLast {
            println "${name} execute"
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    app/build.gradle

    println '------app build.gradle execute------'
    
    beforeEvaluate { project ->
        println "beforeEvaluate: ${project.name} --in--"
    }
    
    afterEvaluate { project ->
        println "beforeEvaluate: ${project.name} --in--"
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    lib/build.gradle

    println '------lib build.gradle execute------'
    • 1

    在根目录下执行 gradle libTask2

    ------setting.gradle execute------
    ------root build.gradle execute------
    all project size : 5
    taskAdded:subTask1
    taskAdded:subTask2
    afterProject: GradlePlugin
    afterEvaluate: GradlePlugin
    beforeProject: app 
    beforeEvaluate: app
    ------app build.gradle execute------
    afterProject: app
    afterEvaluate: app
    Incremental java compilation is an incubating feature.
    beforeEvaluate: app --in--
    beforeProject: lib 
    beforeEvaluate: lib
    ------lib build.gradle execute------
    afterProject: lib
    afterEvaluate: lib
    projectsEvaluated
    ============task graph is ready============
    task subTask1 will execute
    task subTask2 will execute
    ============task graph is over=============
    :subTask1
    before subTask1 execute
    subTask1 execute
    after subTask1 execute
    :subTask2
    before subTask2 execute
    subTask2 execute
    after subTask2 execute
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

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

    1. 如果一个task在build.gradle中定义,但是在构建中不会执行,那么它的Task对象会创建,但是不会在任务图中出现。
    2. 我们可以通过Gradle或者Project对象中定义的方法获取生命周期中每一个过程在执行中的回调。这里注意一下,我们定义的一些回调在实际执行中似乎并没有被触发,例如,settingsEvaluated,projectsLoaded。具体原因需要细看。

    0x05 自定义一个插件

    首先看一下工程结构

    ├── app                 //root工程
    ├── repo                //本地maven目录
    ├── helloPlugin         //plugin工程
    │   ├── build.gradle
    │   └── src
    │       └── main
    │           ├── groovy
    │           │   └── com.lzy.plugin
    │           │       ├── HelloPlugin.groovy
    │           │       ├── Person.groovy
    │           │       └── PersonExt.groovy
    │           └── resources
    │               └── META-INF
    │                   └── gradle-plugins
    │                       └── helloPlugin.properties  //插件名
    │
    ├── build.gradle
    └── settings.gradle
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    apply plugin: 'helloPlugin'
    • 1

    这个文件里面内容指明了插件类的全类名,如下:

    implementation-class=com.lzy.plugin.HelloPlugin
    • 1

    HelloPlugin.groovy:很简单,就定义一个任务,打印一个字符串

    package com.lzy.plugin
    
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    class HelloPlugin implements Plugin<Project> {
        public void apply(Project project) {
    
            project.task("sayHello") {
                group "hello"
                doLast {
                    println "Hello Plugin"
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    首先我们必须说明的是,插件可以以三种形式存在:
    1. 在我们构建项目的build.gradle脚本中直接编写
    2. 在我们构建项目的rootProjectDir/buildSrc/src/main/groovy 目录下
    3. 以单独的project存在

    这里采用第三种方式:在插件目录下编写

    这种编写方式只能发布到本地
    build.gradle

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    version = '0.1.1'
    group = 'com.lzy.plugin'
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        compile gradleApi()
        compile localGroovy()
    }
    
    uploadArchives {
        repositories.mavenDeployer {
            repository(url: 'file:../repo')
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    有时我们更想开源出去给其他人用,像Small这样,我们就可以这么写
    build.gradle

    apply plugin: 'maven-publish'
    
    publishing {
        publications {
            mavenJava(MavenPublication) {
                from components.java
    
                //这两行是发布源码和文档的,可以不发布
                artifact sourcesJar
                artifact javadocJar
    
                groupId 'com.lzy.plugin'
                artifactId 'helloPlugin'
                version '0.1.1'
            }
        }
    
        repositories {
            maven {
                url "../repo"
            }
        }
    }
    
    //默认打包时只会包含编译过的jar包,我们可以增加以下两个task,将源代码和javadoc打包发布,并通过上述artifact指定:
    
    task javadocJar(type: Jar, dependsOn: groovydoc) {
        classifier = 'javadoc'
        from "${buildDir}/javadoc"
    }
    
    task sourcesJar(type: Jar) {
        from sourceSets.main.allSource
        classifier = 'sources'
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    groupId、artifactId、version,这三者组成了插件使用者在声明依赖时的完整语句 groupId:artifactId:version

    对于第一种方式:在helloPlugin的根目录下执行,gralde uploadArchives,编译插件工程,并发布到../repo目录。

    对于第二种方式:有两种publish任务,publish 和 publishToMavenLocal,
    - publish:任务依赖于所有的mavenPublication的generatePomFileFor任务和publishxxxPublicationToMavenRepository,意思是将所有的mavenPublication发布到指定的repository,
    - publishToMavenLocal依赖于所有的mavenPublication的generatePomFileFor和publishxxxTomavenLocal任务,意思是将所有的mavenPublication发布到本地的m2 repository。

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

    首先,在root工程下的build.gradle中,我们通过buildscript引入插件

    buildscript{
        repositories{
            mavenCentral()
            maven {
                url uri('./repo')
            }
        }
        dependencies {
            classpath 'com.lzy.plugin:helloPlugin:0.1.1'
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    apply plugin: 'helloPlugin'
    • 1

    最后,在app或者根目录下执行,gradle sayHello,这样就打印出了我们插件中定义的文字。

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

    Extension

    1)情况一:

    有时候我们希望在使用插件的时候,额外配置一些参数,这时候就需要额外写一个Ext类,如下:
    PersonExt.groovy

    public class PersonExt {
        String name
        int age
        boolean boy
    
        @Override
        public String toString() {
            return "I am $name, $age years old, " + (boy ? "I am a boy" : "I am a girl")
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接着修改
    HelloPlugin.groovy

    class HelloPlugin implements Plugin<Project> {
        public void apply(Project project) {
    
            project.extensions.add("person", PersonExt)
    
            project.task("sayHello") {
                group "hello"
                doLast {
    
                    //以下两种方式都可以
                    def personExt = project.person
                    def personExt1 = project.extensions.getByName('person')
                    println personExt
                    println personExt1
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    最后我们再使用插件的地方添加以下属性:

    person {
        name = "abc"
        age = 18
        boy = true
    }
    • 1
    • 2
    • 3
    • 4
    • 5

    这里使用=号进行复制,实际上可加可不加,本质是利用了grooy特性,调用了setName方法,并且省略了方法调用的括号。

    执行 gradle sayHello得到如下结果:

    :sayHello
    I am abc, 18 years old, I am a boy
    I am abc, 18 years old, I am a boy
    • 1
    • 2
    • 3

    到这里说明我们定义的Extension正确设置并读取成功了。

    2)情况二

    如果我们希望设置的Extension是一个集合列表,并且该列表长度未知,又该怎么写呢?

    我们需要使用NamedDomainObjectContainer,我们后面都简称NDOC 这是一个容纳object的容器,它的特点是它的内部使用SortedSet实现的,内部对象的name是unique的,而且是按name进行排序的。通常创建NDOC的方法就是调用Project里的方法:

    这里type有一个要求:必须有一个public的构造函数,接受string作为一个参数,必须有一个叫做name 的property。

    新增一个类:
    HobbyExt.groovy

    public class HobbyExt {
        String name
        int level
        String school
    
        HobbyExt(name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return "My hobby is $name, level $level, School $school"
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    修改HelloPlugin.groovy代码如下:

    class HelloPlugin implements Plugin<Project> {
        public void apply(Project project) {
    
            NamedDomainObjectContainer<HobbyExt> hobbies = project.container(HobbyExt.class)
            project.extensions.add('hobbies', hobbies)
    
            project.task("sayHello") {
                group "hello"
                doLast {
                    def hobbiesExt = project.hobbies
                    def hobbiesExt1 = project.extensions.getByName('hobbies')
                    println hobbiesExt
                    println hobbiesExt1
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在使用插件的地方添加以下代码:

    hobbies {
        basketball {
            level = 4
            school = "beijing"
        }
        football {
            level = 6
            school = "qinghua"
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    执行 gradle sayHello得到如下结果:

    :sayHello
    [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]
    [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]
    • 1
    • 2
    • 3

    到这里说明我们定义的Extension正确设置并读取成功了。

    3)情况三

    我们也可以混合列表和单个属性,就像android{…}一样
    新建一个类
    Team.groovy

    public class TeamExt {
        NamedDomainObjectContainer<HobbyExt> hobbies
        String name
        int count
    
        public TeamExt(NamedDomainObjectContainer<HobbyExt> hobbies) {
            this.hobbies = hobbies
        }
    
        def hobbies(Closure closure) {
            hobbies.configure(closure)
        }
    
        String toString() {
            "this is a team, name: $name, count $count, hobbies: $hobbies"
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这里的hobbies(closure)函数是必须的,只有实现了这个函数,Gradle在解析team的extension,遇到hobbies配置时,才能通过调用函数,调用 NamedDomainObjectContainer的configure方法,往里面添加对象。
    接着我们修改
    HelloPlugin.groovy

    class HelloPlugin implements Plugin<Project> {
        public void apply(Project project) {
    
            NamedDomainObjectContainer<HobbyExt> hobbyExt = project.container(HobbyExt)
            def team = new TeamExt(hobbyExt)
            project.extensions.add("team", team)
    
            project.task("sayHello") {
                group "hello"
                doLast {
                    def teamExt = project.team
                    def teamExt1 = project.extensions.getByName('team')
                    println teamExt
                    println teamExt1
                }
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在使用插件的地方添加以下代码:

    team {
        name = "android"
        count = 10
        hobbies {
            basketball {
                level = 4
                school = "beijing"
            }
            football {
                level = 6
                school = "qinghua"
            }
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    执行 gradle sayHello得到如下结果:

    :sayHello
    this is a team, name: android, count 10, hobbies: [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]
    this is a team, name: android, count 10, hobbies: [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]
    
    • 1
    • 2
    • 3
    • 4

    到这里说明我们定义的Extension正确设置并读取成功了。

    以上的写法是不是特别像build.gradle文件中的android标签,他的内部可以配置很多属性,原理都和这个一样。

    到这里,我们就通过一个简单的例子就熟悉了Gradle插件的编写规则,而且通过对groovy语法的了解,让我们对gradle的DSL不再陌生。

    0x06 Groovy和Gradle的调试

    主要参考 Small 中 Debug gradle on android studio

    核心就是下面两个命令:

    export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
    
    ./gradlew someTask -Dorg.gradle.daemon=false
    • 1
    • 2
    • 3

    其中有以下几个注意事项:

    - 对于项目根目录下的gradle.properties文件需要做修改,一定要将org.gradle.jvmargs这个参数给注释掉,原因是上述在配置remote调试参数的时候已经配置了jvmargs,如果此时配置文件中仍然配置,会导致remote失效,从而不能监听端口
    - 在执行./gradlew someTask -Dorg.gradle.daemon=false这行命令时,为了方便可以省略后面的-D参数,改为在配置文件中增加上述配置。这是-D参数的描述

    如果你觉得好,对你有过帮助,请给我一点打赏鼓励吧,一分也是爱呀!

  • 相关阅读:
    js 将图片连接转换称base64格式
    mysql性能优化-慢查询分析、优化索引和配置
    MySQL集群(三)mysql-proxy搭建负载均衡与读写分离
    MySQL集群(二)之主主复制
    MySQL集群(一)之主从复制
    JavaSE(八)之Map总结
    JDBC(二)之JDBC处理CLOB和BLOB及事务与数据库元数据获取
    JavaSE(八)之Collection总结
    JavaSE集合(八)之Map
    JavaSE(八)之集合练习一
  • 原文地址:https://www.cnblogs.com/jpfss/p/9883606.html
Copyright © 2020-2023  润新知