[Gradle-3] DSL en Gradle, Groovy y Kotlin

Este artículo es el primer artículo firmado por la comunidad tecnológica de pepitas de tierras raras. Se prohíbe la reimpresión dentro de los 14 días. La reimpresión sin autorización está prohibida después de los 14 días. ¡Se debe investigar la infracción!

1. Introducción

Gradle es una herramienta de construcción, y el lenguaje de secuencias de comandos para los desarrolladores es Groovyy Kotlin, es decir, nuestro build.gradle y build.gradle.kts o complemento comúnmente utilizados.

Entonces, cuando Kotlin ya es compatible después de Gradle 5.0, ¿por qué deberíamos hablar de Groovy? ¿No podemos ir directamente a Kotlin?

Primero veamos una imagen:

Esta es la proporción de lenguajes de programación utilizados por Gradle. Groovy ocupa el primer lugar. Aunque algunos de ellos son códigos de prueba, también muestra que Groovy sigue siendo la corriente principal.

En segundo lugar, cuando creamos un nuevo proyecto, Groovy sigue siendo el lenguaje de secuencias de comandos de compilación predeterminado; además, todavía hay muchas empresas y proyectos que no han migrado a Kotlin hasta el momento. Entonces, por el momento, Groovy sigue siendo lo que Gradle tiene que mencionar 官方构建脚本语言.

En gradle, una gran cantidad de configuraciones están escritas en lenguajes de secuencias de comandos, ya sea Groovy o Kotlin, el rendimiento final es el mismo DSL, por lo que no puede escapar de DSL independientemente del lenguaje de programación.

El contenido principal de este artículo:

  1. ¿Qué es DSL?
  2. Groovy DSL y Kotlin DSL;
  3. sintaxis básica maravillosa;

2. ¿Qué es DSL?

El nombre completo de DSL: Lenguaje específico de dominio, que es un lenguaje específico de dominio, es una habilidad especial otorgada a los desarrolladores por un lenguaje de programación. A través de él, podemos escribir código que parece desviarse de su estructura gramatical original, construyendo así un Estructuras gramaticales propietarias.

Hay dos tipos de DSL, DSL externos y DSL internos.

2.1 ADSL externo

也称独立DSL。因为它们是从零开始建立起来的独立语言,而不基于任何现有宿主语言的设施建立。外部DSL是从零开发的DSL,在词法分析、解析技术、解释、编译、代码生成等方面拥有独立的设施。开发外部DSL近似于从零开始实现一种拥有独特语法和语义的全新语言。构建工具make 、语法分析器生成工具YACC、词法分析工具LEX等都是常见的外部DSL。例如:正则表达式、XML、SQL、JSON、 Markdown等;而大厂一般自研的动态化方案就是基于外部DSL来做的,从上传下发,到端侧解析渲染,从而实现一种不依赖发版且基于原生的动态化方案。

2.2、内部DSL

也称内嵌式DSL。因为它们的实现嵌入到宿主语言中,与之合为一体。内部DSL将一种现有编程语言作为宿主语言,基于其设施建立专门面向特定领域的各种语义。例如:Groovy DSL、Kotlin DSL等;简而言之可以理解为,这种DSL简化了原有的语法结构(看起来有点像lambda),这种简化大大的提高了简洁性,但也增加了理解成本,Gradle中的很多配置都是如此,这也是Gradle上手难的原因之一。

3、Gradle中的DSL

3.1、Groovy DSL

新建项目时Gradle默认的是Groovy语言,也即Groovy DSL,比如app>build.gradle:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.yechaoa.gradlex'
    compileSdk 32

    defaultConfig {
        applicationId "com.yechaoa.gradlex"
        minSdk 23
        targetSdk 32
        versionCode 1
        versionName "1.0"
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.7.0'
}
复制代码

我们来看第一段代码:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}
复制代码

这是一段用于声明引入Gradle插件plugin的DSL代码。

上面的plugin声明是简写版,完整版是这样的:

plugins {
    id 'com.android.application' version '7.3.0' apply false
}
复制代码

3.1.1、id

调用的是PluginDependenciesSpec中的id(String id)函数,返回PluginDependencySpec对象,PluginDependencySpec对象可以理解为是PluginDependenciesSpec的一层封装,比如id(String id)函数只有一个参数,那versionapply哪里来的呢,就是在PluginDependencySpec对象里的。

注意:PluginDependenciesSpec和PluginDependencySpec长得像,但不一样的。

3.1.2、version

插件版本号。Gradle默认插件是不需要指定版本号的,但是三方的插件则必须指定版本号。很好理解,自带插件是可以跟着Gradle版本一起发布的,三方插件则是发布到远端仓库的,比如Gradle仓库gradlePluginPortal(),plugins.gradle.org

3.1.3、apply

是否将插件应用于当前项目及子项目,默认是true

其实apply关键字就是用于管理依赖传递的。当我们不想传递时,就可以设置apply false

除此之外,还可以用表达式来管理依赖传递,比如这么写:

subprojects {
    if (isNeedApply) {
        apply plugin: "org.company.myplugin"
    }
}
复制代码

3.2、还原DSL

介绍完plugin的声明之后,再回到plugins这段DSL代码

plugins {
    id 'com.android.application'
}
复制代码

我们可以理解plugins是一个函数,plugins { } 里面接收的是一个闭包Closure,也可以这么写:

plugins ({
    id 'com.android.application'
})
复制代码

在Groovy中,当函数的最后一个参数或者只有一个参数是闭包时,是可以写在参数括号外面的,所以还可以继续还原:

plugins () {
    id 'com.android.application'
}
复制代码

在Groovy中,当函数只有一个参数的时候,是可以省略括号的,所以就回到了我们最初的DSL代码:

plugins {
    id 'com.android.application'
}
复制代码

3.3、Kotlin写法

还是以plugins为例,来看看Kotlin是怎么写的。

简写版:

plugins {
//    id 'com.android.application'
    id("com.android.application")
}
复制代码

完整版:

plugins {
//    id 'com.android.application' version '7.3.0' apply false
    id("com.android.application") version "7.3.0" apply false
}
复制代码

我们可以看出来,其实Kotlin DSLGroovy DSL的写法差别不大,也是调用PluginDependenciesSpec中的id(String id)函数,只不过差别在调用id(String id)函数时有显式的括号而已。

4、闭包

上面的还原DSL其实涉及到Groovy里面一个非常重要的概念,闭包(Closure)。

闭包的展现形式跟Kotlin里面的lambda不能说非常相似吧,只能说是一毛一样。(kt吸收了很多精华)

4.1、定义闭包

我们先简单定义一个闭包:

// Closure
def myClosure = {}
println(myClosure)
复制代码

{ } 这个就是闭包,里面是空的什么都没做,所以打印出来什么也没有。

这个闭包对象可以理解为是一个函数,函数是可以接收参数的,我们加个参数改造一下:

// Closure
def myClosure = { param ->
    param + 1
}
println(myClosure(1))
复制代码

加了一个param参数,并在闭包里执行 +1 操作,然后打印这个闭包的时候传参为 1 。

输出:

> // Closure
> def myClosure = { param ->
>     param + 1
> }
> println(myClosure(1))
2
复制代码

打印结果:2。

4.2、Kotlin DSL

看到这里,会Kotlin的同学肯定有感受,这跟Kotlin函数参数简直太像了。

我们看一下Kotlin的函数参数:

private fun setPrintln(doPrintln: (String) -> Unit) {
    doPrintln.invoke("yechaoa")
}

fun main() {
    setPrintln { param ->
        println(param)
    }
}
复制代码
  1. 定义了一个高阶函数setPrintln,参数doPrintln是一个函数参数,String表示函数参数doPrintln的参数类型,Unit表示doPrintln不需要返回值。
  2. 然后doPrintln执行并传参"yechaoa"。

setPrintln函数调用实际上是这样的:

    setPrintln {
        
    }
复制代码

setPrintln函数的这个{ },跟上面的闭包一毛一样,就也称为闭包吧(lambda,其实也是DSL语法)。

然后我们在Kotlin的这个闭包里执行了一行代码:

println(param)
复制代码

这个param就是doPrintln函数传的"yechaoa"参数。

然后我们看下输出:

yechaoa

Process finished with exit code 0
复制代码

4.3、Groovy 闭包参数

对比完,我们再次回到Groovy的闭包。

上面我们定义了一个参数param,其实一个参数定义的时候是可以省略显示定义的,用it表示:

// Closure
def myClosure = {
    it + 1
}
println(myClosure(1))
复制代码

顺便来看下两个参数的:

// Closure
def myClosure = { param, param2 ->
    param + 1 + param2
}
println(myClosure(1, 1))
复制代码

输出:

> // Closure
> def myClosure = { param, param2 ->
>     param + 1 + param2
> }
> println(myClosure(1, 1))
3
复制代码

其实跟一个参数的方式没差别,跟Kotlin的写法也一样。

4.4、闭包函数参数

来看下当闭包作为函数参数时是怎样的表现:

def myClosure(Closure closure) {
    println closure()
}

myClosure {
    "yechaoa"
}
复制代码
  1. 定义了一个myClosure函数,参数是一个闭包,然后在函数里面对闭包进行打印;
  2. 直接调用这个函数,在闭包 { } 里传了一个字符串"yechaoa"。

我们这里的打印是println closure(),而不是println(closure())了,在Groovy DSL中,不产生歧义的情况下是可以去掉括号的,这也是我们前面提到的Groovy DSL和Kotlin DSL的区别,即id 'com.android.application'和id("com.android.application")的差别。

好了,看下输出:

> def myClosure(Closure closure) {
>     println closure()
> }
> 
> myClosure {
>     "yechaoa"
> }
yechaoa
复制代码

到这里,有没有发现,这里的myClosure { }是不是跟plugins { }很像,不对,简直一毛一样。

我们在前文(【Gradle-2】一文搞懂Gradle配置)中的源码分析部分也有提到,Gradle中的配置块其实都是闭包调用。

myClosure { }就如同plugins { },"yechaoa"如同plugins { }里面的配置,myClosure(Closure closure)函数里面可以获取到"yechaoa"并打印出来,Gradle Project对象同样可以获取到plugins { }里面依赖的插件,然后解析执行编译构建。

通过闭包演示,是不是对Gradle的DSL理解轻松很多了~(别跑,点个赞)

以上Groovy演示代码都可以在 IDEA>Tools>Groovy Console 里执行测试。

5、Groovy

5.1、简介

Groovy是Apache 旗下的一种强大的、可选类型的和动态的语言,具有静态类型和静态编译功能,用于Java平台,旨在通过简洁、熟悉和易于学习的语法提高开发人员的生产力。它与任何Java程序顺利集成,并立即为您的应用程序提供强大的功能,包括脚本功能、领域特定语言创作、运行时和编译时元编程以及函数编程。

并且,Groovy可以与Java语言无缝衔接,可以在Groovy中直接写Java代码,也可以在Java中直接调用Groovy脚本,非常丝滑,学习成本很低。

5.2、基础语法

5.2.1、注释

注释和 Java 一样,支持单行//、多行/**/和文档注释/** */

不同的是Groovy在脚本里的注释用#

5.2.2、关键字

as、assertbreakcasecatch、class、const、continue、def、defaultdoelseenum、extends、falsefinallyfor、goto、if、implements、
import、in、instanceof、interface、newnullpackagereturnsuperswitchthisthrowthrows、trait、truetrywhile
复制代码
asin、permitsrecord、sealed、trait、var、yields
复制代码
nulltruefalsebooleancharbyteshortintlongfloatdouble
复制代码

5.2.3、字符串

在Groovy种有两种字符串类型,普通字符串java.lang.String和插值字符串groovy.lang.GString

普通字符串:

println 'yechaoa'
复制代码

插值字符串:

def name = 'yechaoa'
println "hello ${name}"
// or
println "hello $name"
复制代码

5.2.4、数字

与java类型相同:

  • byte(8位)
  • char(16位,可用作数字类型,表示UTF-16代码)
  • short(16位)
  • int(32位)
  • long(64位)
  • java.math.BigInteger(2147483647个int)

可以这么声明:

byte  b = 1
char  c = 2
short s = 3
int   i = 4
long  l = 5
BigInteger bi =  6
复制代码

类型会根据数字的大小调整。

def a = 1
assert a instanceof Integer

// Integer.MAX_VALUE
def b = 2147483647
assert b instanceof Integer

// Integer.MAX_VALUE + 1
def c = 2147483648
assert c instanceof Long

// Long.MAX_VALUE
def d = 9223372036854775807
assert d instanceof Long

// Long.MAX_VALUE + 1
def e = 9223372036854775808
assert e instanceof BigInteger
复制代码

5.2.5、变量

Groovy提供了def关键字,跟Kotlin的var一样,属于类型推导。

def a = 123
def b = 'b'
def c = true 
boolean d = false
int e = 123
复制代码

但Groovy其实是强类型语言,但是与Kotlin一样,也支持类型推导。

下面这几种定义也都是ok的

def a1 = "yechaoa"
String a2 = "yechaoa"
a3 = "yechaoa"
println(a3)
复制代码

5.2.6、运算符

算术运算符

assert  1  + 2 == 3
assert  4  - 3 == 1
assert  3  * 5 == 15
assert  3  / 2 == 1.5
assert 10  % 3 == 1
assert  2 ** 3 == 8
复制代码

一元运算符

def a = 2
def b = a++ * 3             

assert a == 3 && b == 6

def c = 3
def d = c-- * 2             

assert c == 2 && d == 6

def e = 1
def f = ++e + 3             

assert e == 2 && f == 5

def g = 4
def h = --g + 1             

assert g == 3 && h == 4
复制代码

赋值运算符

def a = 4
a += 3

assert a == 7

def b = 5
b -= 3

assert b == 2

def c = 5
c *= 3

assert c == 15

def d = 10
d /= 2

assert d == 5

def e = 10
e %= 3

assert e == 1

def f = 3
f **= 2

assert f == 9
复制代码

关系运算符

assert 1 + 2 == 3
assert 3 != 4

assert -2 < 3
assert 2 <= 2
assert 3 <= 4

assert 5 > 1
assert 5 >= -2
复制代码

逻辑运算符

assert !false           
assert true && true     
assert true || false  
复制代码

位移运算符

assert 12.equals(3 << 2)           
assert 24L.equals(3L << 3)         
assert 48G.equals(3G << 4)         

assert 4095 == -200 >>> 20
assert -1 == -200 >> 20
assert 2G == 5G >> 1
assert -3G == -5G >> 1
复制代码

5.2.7、List

Groovy用大括号表示列表,参数用逗号分割。

def numbers = [1, 2, 3]         

assert numbers instanceof List  
assert numbers.size() == 3  
复制代码

除了可以定义同类型之外,也可以定义不同类型值的列表

def heterogeneous = [1, "a", true] 
复制代码

迭代通过调用eacheachWithIndex方法,与Kotlin类似

[1, 2, 3].each {
    println "Item: $it"//it是对应于当前元素的隐式参数
}
['a', 'b', 'c'].eachWithIndex { it, i -> //it是当前元素, i是索引位置
    println "$i: $it"
}
复制代码

5.2.8、Arrays

跟List差不多,不一样的是 需要显示声明类型

String[] arrStr = ['Ananas', 'Banana', 'Kiwi']  

assert arrStr instanceof String[]    
assert !(arrStr instanceof List)

def numArr = [1, 2, 3] as int[]      

assert numArr instanceof int[]       
assert numArr.size() == 3
复制代码

5.2.9、Map

key-value映射,每一对键值用冒号:表示,对与对之间用逗号分割,最外层是中括号。

def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']   

assert colors['red'] == '#FF0000'    
assert colors.green  == '#00FF00'    

colors['pink'] = '#FF00FF'           
colors.yellow  = '#FFFF00'           

assert colors.pink == '#FF00FF'
assert colors['yellow'] == '#FFFF00'

assert colors instanceof java.util.LinkedHashMap
复制代码

5.2.10、闭包

Groovy 提供了闭包的支持,语法和 Lambda 表达式有些类似,简单来说就是一段可执行的代码块或函数指针。闭包在 Groovy 中是groovy.lang.Closure类的实例,这使得闭包可以赋值给变量,或者作为参数传递。

Groovy 定义闭包的语法很简单:

{ [closureParameters -> ] statements }
复制代码

尽管闭包是代码块,但它可以作为任何其他变量分配给变量或字段:

def listener = { e -> println "Clicked on $e.source" }      
assert listener instanceof Closure
Closure callback = { println 'Done!' }                      
Closure<Boolean> isTextFile = {
    File it -> it.name.endsWith('.txt')                     
}
复制代码

闭包可以访问外部变量,而函数则不能。

def str = 'yechaoa'
def closure={
    println str
}
closure()//yechaoa 
复制代码

闭包调用的方式有两种,闭包.call(参数)或者闭包(参数),在调用的时候可以省略圆括号。

def closure = {
    param -> println param
}
 
closure('yechaoa')
closure.call('yechaoa')
closure 'yechaoa'
复制代码

闭包的参数是可选的,如果没有参数的话可以省略->操作符。

def closure = {println 'yechaoa'}
closure()
复制代码

多个参数以逗号分隔,参数类型和函数一样可以显式声明也可省略。

def closure = { String x, int y ->                                
    println "hey ${x} the value is ${y}"
}
复制代码

如果只有一个参数的话,也可省略参数的定义,Groovy提供了一个隐式的参数it来替代它。类似Kotlin。

def closure = { it -> println it } 
//和上面是等价的
def closure = { println it }   
closure('yechaoa')
复制代码

闭包可以作为参数传入,闭包作为函数的唯一参数或最后一个参数时可省略括号。

def eachLine(lines, closure) {
    for (String line : lines) {
        closure(line)
    }
}

eachLine('a'..'z',{ println it }) 
//可省略括号,与上面等价
eachLine('a'..'z') { println it }
复制代码

类似kotlin的高阶函数。

再举个常见的例子,我们经常用到的dependencies

dependencies {
    testImplementation 'junit:junit:4.13.2'
}
复制代码

这个dependencies就是一个闭包,等价于

dependencies ({
    testImplementation 'junit:junit:4.13.2'
})
复制代码

继续等价于

dependencies ({
    testImplementation('junit:junit:4.13.2')
})
复制代码

没错,testImplementation也是闭包,所以现在我们在kotlin中也可以经常见到implementation('xxx')这种写法。

5.2.11、IO操作

5.2.11.1、读文件

读取文本文件并打印每一行文本

new File(baseDir, 'yechaoa.txt').eachLine{ line ->
    println line
}
复制代码

InputStream

new File(baseDir,'yechaoa.txt').withInputStream { stream ->
    // do something ...
}
复制代码

5.2.11.1、写文件

new File(baseDir,'yechaoa.txt').withWriter('utf-8') { writer ->
    writer.writeLine 'Into the ancient pond'
    writer.writeLine 'A frog jumps'
    writer.writeLine 'Water’s sound!'
}
复制代码

OutputStream

new File(baseDir,'data.bin').withOutputStream { stream ->
    // do something ...
}
复制代码

5.2.12、小结

Groovy整体语法特性与java、kotlin都有相似之处,比较容易上手。

当然,也可以用kotlin开发,kotlin相比groovy在开发上也有不少优势,比代码补全、支持跳转、显示注释等。

从groovy迁移到kotlin可以参考两篇官方的文档:

6、总结

本文先是介绍了什么是DSL,然后对比了Groovy DSL 和 Kotlin DSL语法上的一些差异,以及闭包的讲解,最后简单介绍了一下Groovy的基础语法。核心是DSL和闭包的概念,希望本文对你有所帮助~

写作不易,点个赞吧~

7、相关文档

Supongo que te gusta

Origin juejin.im/post/7166638852503765006
Recomendado
Clasificación