Groovy Gradle JVM 基础语法 [MD] Groovy 简介 如何编译运行 Groovy Groovy 的语法

博文地址

我的GitHub 我的博客 我的微信 我的邮箱
baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

为何要学 Groovy

为何要学 Groovy

  • Gradle 是目前 Android 主流的构建工具,不管你是通过命令行还是通过 AndroidStudio 来 build,最终都是通过 Gradle 来实现的。所以学习 Gradle 非常重要。
  • 目前国内对 Android 领域的探索已经越来越深,不少技术领域如插件化、热修复、构建系统等都对 Gradle 有迫切的需求,不懂 Gradle 将无法完成上述事情。所以 Gradle 必须要学习。

Gradle 不单单是一个配置脚本,它的背后是几门语言:

  • Groovy Language(主)
  • Gradle DSL
  • Android DSL

DSL的全称是Domain Specific Language,即领域特定语言,或者直接翻译成特定领域的语言,再直接点,其实就是这个语言不通用,只能用于特定的某个领域,俗称小语言。因此,DSL 也是一门语言

实际上,Gradle 脚本大多都是使用 groovy 语言编写的。

Groovy 是一门 jvm 语言,功能比较强大,细节也很多,全部学习的话比较耗时,对我们来说收益较小,并且玩转 Gradle 并不需要学习 Groovy 的全部细节,所以其实我们只需要学一些 Groovy 基础语法与 API 即可。

为何要使用 Groovy

Groovy 是一种基于JVM的敏捷开发语言,它结合了众多脚本语言的强大的特性,由于同时又能与Java代码很好的结合。一句话:既有面向对象的特性又有纯粹的脚本语言的特性

由于 Groovy 运行在 JVM 上,因此也可以使用 Java 语言编写的组件。

简单来说,Groovy 提供了更加灵活简单的语法大量的语法糖以及闭包特性可以让你用更少的代码来实现和Java同样的功能

如何编译运行 Groovy

Groovy 是一门 jvm 语言,它最终是要编译成class文件然后在 jvm 上执行,所以Java语言的特性Groovy都支持,Groovy 支持 99% 的 java 语法,我们完全可以在 Groovy 代码中直接粘贴 java 代码

安装 Groovy SDK 来编译和运行

  • 下载 Groovy SDK,并解压到自己的存放路径
  • 配置的环境变量
    • 系统变量:GROOVY_HOME C:Androidgroovygroovy-3.0.4
    • 系统变量 -- Path:%GROOVY_HOME%in
  • 命令行中输入groovy -v判断配置是否成功
  • 命令中输入groovyConsole启动 GroovySDK 自带的编辑器,或者双击运行bingroovyConsole.bat
  • 编写 Groovy 代码,右键或CTRL + R编译运行
  • 也可以File -> SaveCTRL + S保存为.groovy格式的文件(保存为UTF-8格式),然后在命令行中通过groovy Test.groovy运行

Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法

在 AS 中编译运行的几种方式

通过 AndroidStudio 创建的 Android 项目,可以在build.gradle中新建任务后,通过 gradle 编译运行 Groovy 代码。

双击运行指定的 task

  • 在 Project 的build.gradle新建一个task,然后在 task 中编写 Groovy 代码,例如:
task(_testGroovy).doLast {
   println "开始运行自定义task"
   test("helloTask")
}

def test(s) {
   println s
   System.out.println("执行Java语法的代码!");
}
  • 然后在Gradle - Tasks - other里面找到对应的 Task,双击运行

Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法

在命令行中通过 gradle 命令运行

配置 gradle 环境变量后,可以在命令行终端中执行对应的 Task。

  • 配置的环境变量,个人不同版本的 gradle 的安装目录在C:AndroidAS_gradlewrapperdists
    • 系统变量:GRADLE_HOME C:AndroidAS_gradlewrapperdistsgradle-4.10.1-all455itskqi2qtf0v2sja68alqdgradle-4.10.1
    • 系统变量 -- Path:%GRADLE_HOME%in
  • 重启 AS,在命令行Terminal中输入gradle -v判断配置是否成功
  • 然后在命令行终端中执行如下命令:
gradle _testGroovy

Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法

Tips

我们知道,在Android项目中,我们只要更改build.gradle文件一点内容,AS就会提示我们同步: Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法Groovy Gradle JVM 基础语法 [MD]
Groovy 简介
如何编译运行 Groovy
Groovy 的语法

但是在我们测试 Groovy 时中,我们更改build.gradle文件后可以不必去同步,执行命令时会自动执行你修改后的最新逻辑。

在 AS 的 Groovy Console 中编译运行

单击 AS 菜单 Tools -> Groovy Consoles,会进入一个 Groovy Consoles 界面,可以在里面编译运行 Groovy 代码,可以运行Java类型也可以是脚本类型的代码

Groovy 的语法

最基本的语法

  • Groovy中的类和方法默认都是public权限的,所以我们可以省略public关键字,除非我们想使用private
  • Groovy中的类型是弱化的,所有的类型都可以动态推断,但是Groovy仍然是强类型的语言,类型不匹配仍然会报错。
  • Groovy中通过def关键字来声明变量和方法

Groovy中很多东西都是可以省略的,比如

  • 语句后面的分号是可以省略的
  • def变量的类型中的其中之一是可以省略的(不能全部省略)
  • def方法的返回值类型中的其中之一是可以省略的(不能全部省略)
  • 方法调用时的圆括号是可以省略的
  • 方法声明中的参数类型是可以省略的
  • 方法的最后一句表达式可作为返回值返回,而不需要return关键字。省略 return 关键字并不是一个好的习惯
def s = "hello world"; //存在分号时会提示你 Semicolon is unnecessary
def int a = 1 //如果 def 和 类型同时存在,IDE 会提示你 def is unnecessary
def c = 1 //省略类型

def hello(String msg) { //省略方法声明中的返回值类型
    println "hello" + msg //省略方法调用时的圆括号
    1                  //省略return,不建议
}

int hello(msg) { //省略方法声明中的参数类型
    return 2 // 这个return不能省略
    println msg//IDE 会提示你 Unreachable statement,但语法没错
}

支持的数据类型

在Groovy中,数据类型有:

  • Java中的基本数据类型和对象
  • 加强的List、Map、File、Stream等集合和 IO 类型
  • 闭包Closure

类型可以显示声明,也可以用 def 来声明,用 def 声明的类型 Groovy 将会进行类型推断。 
基本数据类型和对象和 Java 中的一致,只不过在 Gradle 中,对象默认的修饰符为public

String

String的特色在于字符串的拼接

def a = 1
def b = "hello"
def c = "a=${a}, b=${b}"
println c //a=1, b=hello

闭包 Closure

在 Groovy 中,闭包是一段匿名的代码段,它可以有参数,返回值,并且能够赋值给一个变量。闭包中使用的变量可以是在闭包外部定义的,也可以是在闭包内部定义的。

  • 可以作为方法的参数和返回值
  • 可以作为一个变量而存在
  • 可以有返回值和参数,也可以没有
def test() {
    def closure = { String params -> //闭包的基本格式
        println params
    }
    def closure2 = { a, b -> // 省略了闭包的参数类型声明,省略返回值声明
        println "a=${a}, b=${b}"
        a //省略return
    }

    closure("包青天") //包青天
    closure2 10086, "包青天" //省略小括号
}
test() //主动调用方法才会执行
def test() {
    def closure3 = { a ->
        return a + 1
    }
    def closure4 = { // 省略了闭包的参数声明
        println "参数为 ${it}" //如果闭包不指定参数,那么它会有一个隐含的参数 it
    }

    println closure3(1) //2
    println closure3("1") //11
    //println closure3 2 //不允许省略圆括号,否则运行时会提示:Cannot get property '2' on null object
    closure4() //参数为 null
    closure4 //省略圆括号时没有任何打印,但是执行时并不会报错
    closure4 10086 //参数为 10086
}
test() //主动调用方法才会执行

闭包的一个难题是如何确定闭包的参数(包括参数的个数、参数的类型、参数的意义),尤其当我们调用 Groovy 的 API 时,这个时候没有其他办法,只有查询 Groovy 的文档才能知道。

加强的集合

Groovy 加强了 Java 中的集合类,比如 List、Map、Set 等。

基本使用如下:

def emptyList = []
def list = [10086, "hello", true]

list[1] = "world"
assert list[1] == "world" //assert 后面的语句是true时才继续向下执行,否则异常退出
println list[0] + "-" + list[1] //10086-world

list << 5 //相当于 add(5)
if (5 in list) {// 是否包含
    println list //[10086, world, true, 5]
}

def range = 1..5
println range //1..5
println range.size() //5
def emptyMap = [:]
def map = ["id": 1, "name": "包青天"]

map << [age: 29] //添加元素
map["id"] = 10086 //访问元素方式一
map.name = "哈哈" //访问元素方式二,这种方式最简单

println map.size() + "-" + map //3-{id=10086, name=哈哈, age=29}
println map.id + "-" + map["name"] + "-" + map.get("age")

可以看到,通过 Groovy 来操作 List 和 Map 显然比 Java 简单的多。

加强的 IO

在Groovy中,文件访问要比Java简单的多

def file = new File("settings.gradle")
file.eachLine { line, lineNo ->
    println "${lineNo} ${line}" //行号,内容
}

file.eachLine { line ->
    println "${line}" //内容
}

Groovy访问xml有两个类:XmlParser 和XmlSlurper,二者几乎一样,在性能上有细微的差别要。

def xml = '<root><one a1="uno!"/><two>Some text!</two></root>'
//或者 def xml = new XmlParser().parse(new File("filePath.xml"))

def rootNode = new XmlParser().parseText(xml) //根节点
assert rootNode.name() == 'root' //根节点的名称
assert rootNode.one[0].@a1 == 'uno!' //根节点中的子节点 one 的 a1 属性的值
assert rootNode.two.text() == 'Some text!'  //根节点中的子节点 two 的内容
rootNode.children().each { assert it.name() in ['one','two'] }

如何查看文档

File 类的 eachLine 方法

1、首先去 JDK文档 中找相应的类,例如 File 类

2、在找到相应的方法,例如eachLine方法

public Object eachLine(Closure closure)
  • Iterates through this file line by line.
    • Each line is passed to the given 1 or 2 arg closure.
    • The file is read using a reader which is closed before this method returns.
  • Parameters: closure - a closure
    • arg 1 is line,第一个参数表示当前行的内容
    • optional arg 2 is line number starting at line 1,可选的第二个参数表示从1开始的行号
  • Returns: the last value returned by the closure

3、然后就可以尝试着使用了

new File("settings.gradle").eachLine { line ->
    println "${line}
}

4、在 AS 中点击此方法后的声明,会发现是以下形式:

public class ResourceGroovyMethods extends DefaultGroovyMethodsSupport {
    public static <T> T eachLine(File self, @ClosureParams(value = FromString.class,options = {"String", "String,Integer"}) Closure<T> closure) throws IOException {
        return eachLine((File)self, 1, closure);
    }
}
  • public static <T> T:表明这是一个 groovy 扩展出来的一个静态方法
  • File self:表明第一个参数是自己,也就是 file,实际使用中都不需要这个参数
  • @ClosureParams(..):用来声明闭包的参数个数、类型的
    • value = FromString.class:用于在编译时推断闭包的参数类型(IDE会用来提示)
    • options = {...}:推断类型时,传递给提示的一组选项,也可供resolver使用
      • "String":表明第一个参数是String类型
      • "String,Integer":表明第二个参数是String类型,但是可以转换为Integer类型
  • Closure<T> closure:表明需要一个闭包类型的参数
new File("settings.gradle").eachLine { line -> println "$line" }
new File("settings.gradle").eachLine { line, i ->
    int num = Integer.parseInt("$i") + 1
    println "${line}----$num"
}

集合的左移位运算符 <<

集合的左移位运算符<<表示向集合中添加新元素,在 AS 中点击这个操作符可以看到,实际上其调用的是一个叫leftShift的方法:

public static <T> List<T> leftShift(List<T> self, T value) {
    return (List)leftShift((Collection)self, (Object)value);
}

public static <T> Collection<T> leftShift(Collection<T> self, T value) {
    self.add(value);
    return self;
}

//接口 Collection 中定义的 add 方法
boolean add(E e);

从 List文档 当中可以查到此方法的介绍:

  • Overloads重载 the left shift operator左移位运算符 to provide an easy way to append objects to a List.
  • Parameters: value - an Object to be added to the List.
  • Returns: same List, after the value was added to it.

实际上,这个运算符是大量使用的,并且当你用 leftShift 方法时 IDE 也会提示你让你使用左移位运算符<<替换。

map 的 each 方法

each 方法的声明:

public Map each(Closure closure)
  • Allows a Map to be iterated through using a closure.
    • If the closure takes one parameter then it will be passed the Map.Entry
    • if the closure takes two parameters then it will be passed the key and the value.
  • In general, the order in which the map contents are processed cannot be guaranteed 通常无法保证处理元素的顺序.
  • In practise在实践中, specialized forms特殊形式的 of Map, e.g. a TreeMap will have its contents processed处理其内容 according to the natural ordering自然顺序 of the map.
[a: 1, b: 3].each { kv -> println "一个参数 $kv" }
[a: 1, b: 3].each { key, value -> println "二个参数 ${key}=$value" }
[a: 1, b: 3].each { println "隐含参数 ${it.key}=${it.value}" } //隐含参数 it,key 和 value 是属性名

其他的一些语法特性

Getter和Setter

当你在 Groovy 中创建一个 beans 的时候,通常我们称为POGOS(Plain Old Groovy Objects),Groovy 会自动帮我们创建getter/setter方法。

当你对getter/setter方法有特殊要求,你尽可提供自己的方法,Groovy 默认的getter/setter方法会被替换。

构造器

class Server {
    String name
    Cluster cluster
}

初始化一个实例的时候你可能会这样写:

def server = new Server()
server.name = "Obelix"
server.cluster = aCluster

其实你可以用带命名的参数的默认构造器,会大大减少代码量:

def server = new Server(name: "Obelix", cluster: aCluster)

Class类型

在Groovy中Class类型的.class后缀不是必须的,比如:

def func(Class clazz) {
    println clazz
}
func(File.class) //class java.io.File
func(File) //class java.io.File

使用with()操作符

当更新一个实例的时候,你可以使用with()来省略相同的前缀,比如:

Book book = new Book() 
book.with {
   id = 1 //等价于 book.id = 1
   name = "包青天"
   start(10086)
   stop("包青天")
}

判断是否为真

所有类型都能转成布尔值,比如nullvoid相当于0或者相当于false,其他则相当于true,所以:

if (name) {}
//等价于
if (name != null && name.length > 0) {}

在 Groovy 中可以在类中添加asBoolean()方法来自定义是否为真

简洁的三元表达式

在Groovy中,三元表达式可以更加简洁,比如:

def result = name ?: ""
//等价于
def result = name != null ? name : ""

捕获任何异常

如果你实在不想关心try块里抛出何种异常,你可以简单的捕获所有异常,并且可以省略异常类型

try {
    // ...
} catch (any) { //可以省略异常类型
    // something bad happens
}

这里的any并不包括Throwable,如果你想捕获Throwable,你必须明确的表明你想捕获Throwable

简洁的非空判断

在 java 中,你要获取某个对象的值必须要检查是否为null,这就造成了大量的if语句;在 Groovy 中,非空判断可以用?.表达式,比如:

println order?.customer?.address
//等价于
if (order != null) {
   if (order.getCustomer() != null) {
       if (order.getCustomer().getAddress() != null) {
           System.out.println(order.getCustomer().getAddress());
       }
   }
}

使用断言

在 Groovy 中,可以使用assert来设置断言,当断言的条件为false时,程序将会抛出异常:

def check(String name) {
   assert name // 检查方法传入的参数是否为空,name non-null and non-empty according to Groovy Truth
   assert name?.size() > 3
}

== 和 equals

  • Groovy里的is()方法等同于Java里的==
  • Groovy中的==是更智能的equals(),比较两个类的时候,你应该使用a.is(b)而不是==
  • Groovy中的==可以自动避免 NullPointerException 异常
status == "包青天"
//等价于Java中的
status != null && status.equals("包青天")

switch方法

在 Groovy 中,switch 方法变得更加灵活,可以同时支持更多的参数类型:

def x = null
def result = ""
switch (x) {
    case "foo": result = "found foo" //没有 break 时会继续向下判断
        break
    case "bar": result += "bar"
    case [4, 5, 6]: result = "list" //匹配集合中的元素
    case 12..30: result = "range" //匹配某个范围内的元素
    case Integer: result = "integer" //匹配Integer类型
    case { it > 3 }: result = "number > 3" //匹配表达式
    case Number: result = "number" //匹配Number类型
    default: result = "default"
}
println result

字符串分行

Java 中,字符串过长需要换行时我们一般会这样写:

throw new PluginException("Failed to execute command list-applications:" +
    " The group with name " +
    parameterMap.groupname[0] +
    " is not compatible group of type " +
    SERVER_TYPE_NAME)

Groovy中你可以用  字符,而不需要添加一堆的双引号:

throw new PluginException("Failed to execute command list-applications: 
The group with name ${parameterMap.groupname[0]} 
is not compatible group of type ${SERVER_TYPE_NAME}")

或者使用多行字符串""":

throw new PluginException("""Failed to execute command list-applications:
    The group with name ${parameterMap.groupname[0]}
    is not compatible group of type ${SERVER_TYPE_NAME)}""")

Groovy中,单引号引起来的字符串是java字符串,不能使用占位符来替换变量,双引号引起的字符串则是java字符串或者Groovy字符串。

Import 别名

在java中使用两个类名相同但包名不同的两个类,像java.util.Listjava.wt.List,你必须使用完整的包名才能区分。Groovy中则可以使用import别名:

import java.util.List as jurist //使用别名
import java.awt.List as aList
import java.awt.WindowConstants as WC
import static pkg.SomeClass.foo //静态引入方法

2019-1-12