(仅供个人学习。)
原文
http://benweizhu.github.io/blog/categories/gradleshen-ru-yu-shi-zhan/
http://benweizhu.github.io/blog/2015/01/31/deep-into-gradle-in-action-1/
什么是构建工具?
一个可编程的工具,能够以可执行和有序的任务来表达满足需要的自动化过程。
以Java为例,要得到一个简单可运行的Jar文件,需要下面几步:
1.编译源代码
2.运行测试(前提是你有测试)3.拷贝Class文件到目标目录
4.打包Class文件为Jar文件
这是一个完整的可自动化的过程,在没有构建工具之前,是由谁来做?IDE。一个强大的IDE,以上的步骤都只需要按几个按钮,这让开发人员的生活变得很美好,完全集中在写出优秀的代码。
Java世界的构建工具
在Java的世界里,目前在被使用的常用构建工具有三个:Ant,Maven,Gradle。
Ant的核心是由Java编写,采用XML作为构建脚本,这样就允许你在任何环境下,运行构建。Ant基于任务链思想,任务之间定义依赖,形成先后顺序。缺点是使用XML定义构建脚本,导致脚本臃肿,Ant自身没有为项目构建提供指导,导致每个build脚本都不一样,开发人员对于每个项目都需要去熟悉脚本内容,没有提供在Ant生态环境内的依赖管理工具。
Maven团队意识到Ant的缺陷,采用标准的项目布局,和统一的生命周期,采用约定由于配置的思想,减少构建脚本需要的编写内容,活跃的社区,可以方便找到合适的插件,强大的依赖管理工具。缺点是采用默认的结构和生命周期,太过限制,编写插件扩展麻烦,XML作为构建脚本。
如果有一个构建工具可以折中,同时拥有Ant和Maven的优点,是不是很爽?告诉你有,那就是Gradle。
Gradle
基于Groovy的DSL,提供声明式的构建语言
采用标准的项目布局,但拥有完全的可配置性,就是可以改
通过插件,提供默认的构建生命周期,也可以自己定义任务,单独运行任务,定义任务间的依赖
强大的依赖管理工具,与Maven和Ivy仓库结合
与Ant天生兼容,有效的重用Ant的任务
多种实现插件的方式,强大的官方插件库
从构建级别,支持从Ant或者Maven的逐步迁移
通过包装器,无缝的在各个平台运行
Gradle是一个通用构建工具,也就是说,它不单是为Java而生。比如,还可以做Groovy,Scala的构建。这取决于你使用什么样的插件。
大部分Java项目的基本步骤都非常类似,编译Java源代码,运行单元测试,拷贝生成的class文件到目标目录,打包Jar文件(或者War包,Ear包),而这些重复且约定俗成的任务,如果可以不用写一行构建代码就实现,是多么的棒!Maven就做到这一点,采用约定由于配置的思想,预先定义常用的任务,并定义它们的执行顺序。
Gradle吸收了Maven的这个优点,通过插件,实现预定义任务和任务之间依赖关系的导入,这样就可以在一行代码都不写的情况下(如果应用插件,你觉得也算一行的话,那就写一行吧),直接使用已经定义的任务。
apply plugin: 'java'
你也可以通过命令gradle tasks –all来查看每个task各自有什么依赖。
当然,这里还是重点提下,Java插件中四个重要和常用的任务,assemble,check,build,clean。
assemble
All archive tasks in the project, including jar. Some plugins add additional archive tasks to the project.
check
All verification tasks in the project, including test. Some plugins add additional verification tasks to the project.
build
check and assemble
clean
Deletes the project build directory.
assemble被用来产生Jar文件,输出目录在build/libs下。
check用来运行所有的验收任务,包括test任务,以及其他验收任务,比如checkstyle。
大部分的项目都不是自包含的,也就是说,需要使用到其他项目的构建结果,比如一些Jar文件。它们作为输入文件,必须存在于项目的ClassPath下,程序才能编译和运行。这些输入文件有一个很表意的名字,叫做依赖。
Gradle允许你告诉它项目的依赖是什么,然后它就会负责找到这些依赖。这些依赖会从Maven或者Ivy的远程仓库下载下来(大部分情况),并缓存在本地的某个路径,这个过程叫做依赖解析。
Maven和Gradle一样也提供了类似的功能,而Ant没有,你只能告诉Ant依赖文件的相对或者绝对路径,让它去加载。
常常一个依赖自己也存在依赖,我们称为传递依赖,依赖管理工具又具有解析传递依赖的能力。
Gradle的依赖管理
那么如何在Gradle中定义依赖呢?看个最简单的例子。
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:4.11'// testCompile group: 'junit', name: 'junit', version: '4.11'
}
项目使用了Java的插件,在repositories块中告诉Gradle使用maven的远程仓库作为依赖下载地址,在dependencies块定义了一个junit的依赖,并说明了分组(Maven中的Scope),后面注释中有一个表意更完整的依赖定义,说明了依赖声明使用的三个坐标group,name,version。
整个看起来是那么的表意,使用过Maven更会觉得是无缝转换,甚至更简洁。
Dependency configurations 依赖分组
在Gradle中,依赖都被会分配到某一个具体的configuration中(这里我不倾向于翻译成配置,我觉得布局,或者分组更适合)。Configuration代表着一个或多个构件及构件所需依赖的一个分组。
Java插件已经预定义了一些configuration,比如,compile,runtime,testCompile,testRuntime等。
compile 放在这个configuration下的依赖是在编译产品代码时所使用的,但它作为一个分组,包含产品代码和编译所需的依赖。
runtime 产品代码在运行时需要的依赖,默认,也会包含compile中的依赖。
testCompile 编译测试代码时所需要的依赖,默认,被编译的产品代码和产品代码需要的编译依赖也属于该分组。
testRuntime 运行测试时需要的依赖。默认,包含compile,runtime和testCompile的分组的构建和依赖。
定义SourceSet时,添加的Configuration
上一节,在介绍Java插件的时候,提到了SourceSet概念。针对每一个新添加的SourceSet,Java插件都会动态的给它添加两个Configuration,分别是sourceSetCompile和sourceSetRuntime。
比如:新添加一个SourceSet,叫做int,那么对应的Configuration是intCompile和intRuntime。
这一特性也正好印证,Java插件是如何识别自定义SourceSet来进行编译和运行。
依赖的多种定义方式
除了通过远程仓库和依赖坐标来定义依赖,Gradle还提供了另外两种常用的依赖定义方式,对本地文件的依赖,对某个项目的依赖。
对文件的依赖
这种情况看起来是不是很奇葩,都有依赖管理了和Maven仓库了还要什么文件依赖。其实不然,使用这种定义方式,最常见场景是项目构建工具的迁移,从Ant到Gradle。无论任何项目,迁移过程都是小步前进,Gradle提供文件依赖的配置,就是为了解决这些特殊性。
dependencies {
runtime files('libs/a.jar', 'libs/b.jar')
runtime fileTree(dir: 'libs', include: '*.jar')
}
对另一个工程的依赖
项目中划分子模块是很平常的事情,前端Controller和数据层Dao分离管理就是一个例子,那么在进行前端Controller模块构建时,就需要将数据层模块作为依赖。定义方式如下:
dependencies {
compile project(':shared')
}
依赖版本冲突
依赖冲突是所以依赖管理中最头痛的问题,这常常出现在传递依赖中。Gradle对解决传递依赖提供了两种策略,使用最新版本或者直接导致构建失败。默认的策略是使用最新版本。虽然这样的策略能够解决一些问题,但是还是不够。常见的一种情况是,NoSuchMethond或者ClassNotFound。这时候,你可能需要一些特殊手段,比如排除不想要的传递依赖。
排除传递依赖
排除传递依赖有多种原因,远程仓库中不存在,运行时不需要,或者版本冲突。排除传递依赖的方式有两种:1.直接在configuration中排除 2.在具体的某个dependency中排除
configurations {
compile.exclude module: 'commons'
all*.exclude group: 'org.gradle.test.excludes', module: 'reports'
}
dependencies {
compile("org.gradle.test.excludes:api:1.0") {
exclude module: 'shared'
}
}
通过命令行查看依赖关系
当出现依赖冲突时,最主要的还是要分析依赖冲突的原因,Gradle提供了两个任务来帮助你分析依赖关系
dependencies - Displays all dependencies declared in root project ‘projectReports’.
dependencyInsight - Displays the insight into a specific dependency in root project ‘projectReports’.
Tips:输出依赖关系图到文件
在命令行中直接使用gradle dependencies可以打印出依赖图,但是在命令行中查看始终不太方便,我们可以将结果输出到一个文件中,如下:
gradle dependencies > dependencies.txt
dependencies.txt保存在项目的根目录
---穿插一点自定义任务的知识。
Gradle Task
在前面已经介绍过Gradle和Ant相似,由任务驱动,以任务依赖的方式形成任务链,从而实现构建生命周期。所以,任务是Gradle中一个完整的可执行单元。
如何定义任务:
task hello {
println 'hello Gradle'
}
执行该任务,只需要输入命令gradle hello。定义task的方式有很多种:
task myTask
task myTask { configure closure }
task myType << { task action }
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }
其中有一种定义方式,传入了一个参数type,作用是预定义该task的类型,指定类型之后,在传入的闭包中就可以使用该类型task提供的特殊变量或函数。
比如一个拷贝类型的task
task copyDocs(type: Copy) {
from 'src/main/doc'
into 'build/target/doc'
}
自定义集成测试任务
现在我们开始写一个集成测试的task,需求是这样的:
作为一个Java的程序员,我想要将单元测试和集成测试分离
1.我想要 将单元测试全部放在src/test/unit目录中,将集成测试全部放在src/test/intgetaion中
2.我想要 能够单独运行我的集成测试
3.我想要 在运行build命令时,同时跑单元测试和集成测试
根据这样的一个需求,划分几步来做:
1.建立目录
2.目录结构已经和原来的默认规约不同,所以要更改Java插件提供的SourceSet test,来映射单元测试目录结构
3.需要新建一个SourceSet intTest,来映射集成测试目录结构
4.Java插件会给新建的SourceSet intTest定义两个Configuration,分别是intTestCompile和intTestRuntime,那么就需要给这两个分组指定构件内容和依赖
5.定义一个名字叫做integrationTest的测试的task
那么我们从第二步和第三步开始,修改Java插件提供的SourceSet test和新建SourceSet intTest:
// 定义一些常量,在其他位置使用
ext {
unitJavaSrcDir = 'src/test/unit/java'
unitResourcesSrcDir = 'src/test/unit/resources'
intJavaSrcDir = 'src/test/integration/java'
intResourcesSrcDir = 'src/test/integration/resources'
}
sourceSets {
test {
java {
srcDir unitJavaSrcDir
}
resources {
srcDir unitResourcesSrcDir
}
}
intTest {
java {
srcDir intJavaSrcDir
}
resources {
srcDir intResourcesSrcDir
}
}
}
第三步,给intTestCompile和intTestRuntime指定指定构件内容(产品代码)和依赖
dependencies {
testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-core:1.9.5'
intTestCompile sourceSets.main.output // 将sourceSets.main中的输出class指定到intTestCompile中
intTestCompile configurations.testCompile // 将configurations.testCompile的依赖拿过来
}
最后一步,定义一个test类型的task,并让check任务依赖于它
task integrationTest(type: Test) {
testClassesDir = sourceSets.intTest.output.classesDir
classpath = sourceSets.intTest.runtimeClasspath
}
check.dependsOn integrationTest
然后,你就可以在命令行中运行gradle integrationTest。
完整版本如下:
apply plugin: 'java'
apply plugin: 'idea'
ext {
unitJavaSrcDir = 'src/test/unit/java'
unitResourcesSrcDir = 'src/test/unit/resources'
intJavaSrcDir = 'src/test/integration/java'
intResourcesSrcDir = 'src/test/integration/resources'
}
sourceSets {
test {
java {
srcDir unitJavaSrcDir
}
resources {
srcDir unitResourcesSrcDir
}
}
intTest {
java {
srcDir intJavaSrcDir
}
resources {
srcDir intResourcesSrcDir
}
}
}
repositories {
mavenCentral()
}
dependencies {
testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-core:1.9.5'
intTestCompile sourceSets.main.output
intTestCompile configurations.testCompile
}
task integrationTest(type: Test) {
testClassesDir = sourceSets.intTest.output.classesDir
classpath = sourceSets.intTest.runtimeClasspath
}
check.dependsOn integrationTest
idea {
module {
testSourceDirs += file(unitJavaSrcDir)
testSourceDirs += file(unitResourcesSrcDir)
testSourceDirs += file(intJavaSrcDir)
testSourceDirs += file(intResourcesSrcDir)
}
}
自定义插件
利用Gradle做构建,必然逃不掉Gradle的插件的使用,即便是最简单的Java或Groovy的应用都需要使用Java插件或者Groovy插件。
Gradle插件的作用就是将会被重复利用的逻辑打包,这样就可以在不同的项目中重复的使用。比如在上一节中实现的集成测试任务,就可以打包到插件中,然后在其他的工程中使用,而不需要重复的写相同的任务。
Gradle提供了三种写插件的方式:
1.直接在build.gradle文件中写插件,然后直接使用,不好的地方很明显,不能在该build.gradle脚本之外的位置(其他脚本或者工程)中使用。
2.将插件写在项目的rootProjectDir/buildSrc/src/main/groovy包下,Gradle会负责编译和放置到classpath,虽然可以多个gradle脚本中使用,但是不能在其他工程中使用。
3.一个独立的插件工程,很明显,这是最常见的实现方式,因为可以被任何脚本或者工程使用。
为了节省时间,我们直接进入到最常见的实现方式:实现一个独立的插件工程。
这是一个Groovy工程
我们知道,Gradle项目是基于Groovy语言开发的,所以插件功能必然是一个Groovy工程,构建创建一个build.gradle文件。
apply plugin: 'idea'
apply plugin: 'groovy'
dependencies {
compile gradleApi()
compile localGroovy()
}
task wrapper(type: Wrapper) {
gradleVersion = '1.11'
}
按照Groovy插件推荐的目录结构建立好下面结构的目录,你可以忽略java那一级//?
src/main/java
src/main/resources
src/main/groovy
src/test/java
src/test/resources
src/test/groovy
利用Plugin接口实现插件
然后,我们写一个Groovy类,让它实现Plugin接口,如下:
package me.zeph.gradle.plugin
import me.zeph.gradle.extension.HelloExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class HelloPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.task('hello') << {
println 'hello plugin'
}
}
}
这里,我们通过project的task方法实现了一个名字是hello的task,里面打印了一句话。这个任务很简单,现在我们来增加一点点复杂度。记不记得大部分插件在使用之后,除了提供一些列的task,还提供了许多的closure(闭包),可以通过这些闭包传递一些参数进去。那么,这是怎么是实现的呢?很简单,利用project提供的扩展。
扩展的使用
定义一个名字是HelloExtension的Groovy类(名字其实无所谓叫什么,而且居然不需要实现任何的接口):
package me.zeph.gradle.extension
class HelloExtension {
String message;
}
改变一些插件的实现:
package me.zeph.gradle.plugin
import me.zeph.gradle.extension.HelloExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class HelloPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.add('hello', HelloExtension)
project.task('hello') << {
println project.hello.message
}
}
}
project.extensions.add(‘hello’, HelloExtension),这段代码将HelloExtension添加到project的extensions中,于是task就可以通过project.hello.message来获取。是不是很简单?//?
告诉别人这是个插件:插件id
那么,功能部分都写完了,怎么样让其他构件脚本知道这是一个插件能?配置META-INF。
在resources目录下建立这样一个目录结构:/resources/META-INF/gradle-plugins
然后在这里建立一个名字是me.zeph.hello.properties的Property文件,文件里的内容是:
implementation-class=me.zeph.gradle.plugin.HelloPlugin
这个Property文件的命名并不是随意定义的,名字的作用是定义该插件的id,什么意思?说白了就是apply时使用的名字。如下:
apply plugin: 'me.zeph.hello'
使用生成的插件
到这里,一个独立的插件工程就完成了,实验一把!!
运行gredlew clean assemble,将生成的jar文件,拷贝到其他的项目目录中(这里没有upload到仓库,所以直接文件形式引入依赖)。
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath fileTree(dir: 'libs', include: '*.jar')
}
}
apply plugin: 'me.zeph.hello'
hello {
message = 'hello gradle plugin'
}
然后运行gradlew hello,就可以看到hello任务的执行。
总结,其实实现一个Gradle的独立插件工程,从建立工程的角度还是比较简单的,关键在如何通过Groovy实现插件,以及理解插件的api。
参考资料
https://gradle.org/docs/current/userguide/custom_plugins.html
理解DSL(领域特定语言)
DSLs come in two main forms: external and internal. An external DSL is a language that’s parsed independently of the host general purpose language: good examples include regular expressions and CSS. External DSLs have a strong tradition in the Unix community. Internal DSLs are a particular form of API in a host general purpose language, often referred to as a fluent interface. —- Martin Fowler
Martin Fowler将DSL分为两类:外部DSL和内部DSL。外部DSL是一种独立的可解析的语言,举一个最常见的是例子,SQL,它专注于数据库的操作。内部DSL是通用语言所暴露的用来执行特定任务的API,它利用语言本身的特性,将API以特殊的形式(或者格式)暴露出来的。比如,Martin Fowler给出了关于流接口(fluent interface)。
总结一下,外部DSL是一种特定的独立语言,内部DSL是通用语言为实现特殊目的提供的API。
Gradle的DSL
Gradle为了很好的描述构建,它提供了一套DSL,它是一种内部DSL。这套语言基于Groovy,但增加了一点点的特殊处理,以便在利用Groovy语言特性的之外,让它更像一个构建语言。
所以,Gradle的构建脚本和Groovy之间的关系是大于(>)。换句话说,如果你在Gradle的构建脚本中,写纯粹的Groovy代码是绝对没有问题的,这一点在官方文档的用户手册中也单独拿出一章来说明。
构建脚本
我们先暂且不去验证这一点,也不要着急着用Groovy去写一个功能强大且复杂的task,从构建脚本中跳出来,站在一个更高的位置来俯视Gradle。
在我们写Gradle脚本的时候,我们一般会写两种类型的,一种脚本,默认命名是build.gradle,另一种脚本,命名是settings.gradle。它们各自的作用,相信不用我在这里过多的介绍。在Gradle的世界里面,它们都叫做配置脚本。举个很简单的例子,在settings中配置,多项目结构有哪些模块,或者在build.gradle中,配置Java的sourceset。
那可能就有人问了,那在构建脚本中写一个task也算配置吗?我并没有配置什么东西,而是在用语言实实在在的写一系列的动作。这个算配置吗?
我想,这取决于对“配置”这个概念的理解。如果,我只是应用Java插件,写一个sourceset,或者写一个dependencies,这个配置就有点类似,传统意义上理解的“键值对”的配置。
如果涉及到写一个task的时候,我们就要把它想象成“对某一个对象的配置”,想象成一系列的set或者add操作(即便是sourceset,你也应该这么去理解,因为这才是本质)。
基于Groovy的本质
在前面介绍DSL概念的时候,我们了解到Gradle是一种内部DSL,是一系列的API,它基于Groovy。Groovy是什么?它是一种面向对象的编程语言,它的核心概念是对象。
Gradle基于Groovy但大于Groovy,它是经过“定制”的Groovy,是经过“定制”的面向对象语言,所以,由始至终,Gradle都离不开对象这个概念。
如果你明白了这个本质,那么就明白了写Gradle脚本,就和写Java代码是一个道理,写Gradle构建脚本就是写代码调用Gradle的API,只不过因为一些特性和特殊处理,让他看上去不太像一个标准的类C的编程语言(我更倾向于说类Java,因为C语言不是面向对象的),那么接下来,你所需要知道的就是Gradle提供的API长啥样。
Gradle的对象
既然我们知道了Gradle的本质是经过“定制”的面向对象语言(Groovy语言),那么我们就来看看Gradle里面有些什么对象。
如果你有读过Gradle的用户手册,那么,第六章,Build Script Basics,肯定是你必读的一章节,即便你当时看不太明白,只是依葫芦画瓢。现在你可以回过头来看下,该章节在一开始就进入主题,介绍了Gradle中两个的核心概念,project和task。
从组成关系上来看,我们知道,或者文档是这么说的,一个构建是由多个project组成,一个project是由多个task组成,task之间的依赖关系,构成了整个构建的流水线。
对于task的概念相对比较好理解,因为在命令行中,我们通过gradle build,进行Java的构建,这是一个看得见,摸得着的概念。
那project是什么?从你学习Gradle开始,到应用Java插件,实现Java的构建,好像从头到尾都没有直接接触过project这个概念,至少没有像task这样如此真实的接触。我们需要了解它吗?如果你只是依葫芦画瓢,参考Gradle的文档,进行构建的“配置”,那么你不用。
如果你想知道写的那些配置在本质上是什么?那么就有必要。
Project对象和build.gradle
为了不深究Gradle的实现原理(就是去读源代码),又要让大家觉得有据可依。我通过引用官方文档的一些描述来帮助大家理解Project对象。
For each project in the build, Gradle creates an object of type Project and associates this Project object with the build script. (Chapter 13. Writing Build Scripts)
构建中的每一个project,Gradle都会创建一个Project对象,并将这个对象与构建脚本相关联。
There is a one-to-one relationship between a Project and a “build.gradle” file. (Interface Project API)
Project对象与build.gradle是一对一的关系。
First, Gradle scripts are configuration scripts. As the script executes, it configures an object of a particular type. For example, as a build script executes, it configures an object of type Project. This object is called the delegate object of the script. (Gradle Build Language Reference)
Gradle的脚本是配置脚本,当脚本执行时,它是在配置某一个特殊类型的对象。比如一个构建脚本的执行,它就是在配置一个Project类型的对象。这个对象叫做脚本的代理对象。
读完这三句话,应该可以清楚的明白build.gradle的本质,简单的说,build.gradle是对一个Project对象的配置。
深入理解
如果你还没明白,你可以仔细考量这三句话。因为这里,我们要进一步深入探讨上个部分引出的另一个概念:代理。
这个概念并不来自于Gradle,如果你熟悉Groovy,你肯定会立刻想到这是Groovy中很重要的一个概念。
在Groovy中,Object对象提供了一个重要的方法with,这个方法在JavaScript中也是存在的。with方法可以在一个闭包内辅助实现委托调用,在with的作用域内调用的任何方法,都被定向到该上下文对象上,这样就去掉了对该实例的多余引用,举个例子:
def list = [1, 2]
list.add(3)
list.add(4)
println(list.size())
println(list.contains(2))
def listWith = [1, 2]
listWith.with {
add(3)
add(4)
println(size())
println(contains(2))
}
在with中,省去了对 list对象的引用。
而build.gradle和project对象,虽然从解析的角度不一定是通过with方式实现,但是它们之间就是这样的一个关系。
闭包内的内容就是build.gradle对project对象的操作。
这里我引用Gradle用户手册第十三章的内容来进一步说明,
Any method you call in your build script which is not defined in the build script, is delegated to the Project object.Any property you access in your build script, which is not defined in the build script, is delegated to the Project object.(Chapter 13. Writing Build Scripts)
通过现象看本质
我们从理论上讲了这么多关于Gradle本质的东西,而且好像还有点道理,但我们还是要验证一下,透过现象来看本质。通过实践,进一步加强我们的理解。
一个小小的task
举个例子,我们在build.gradle中,写一个简单的task
task helloWorld << {
println 'helloWorld'
}
如果你熟悉Groovy,并且知道它是针对project为上下文的一段代码,你会怎么看上面这段代码。是不是会有几个疑问?
问题一:是否有一个project的方法叫做task?答案:是,Project.task(String name),返回一个Task对象。
问题二:helloWorld是一个参数吗?答案:是,它被解析为一个String类型的实参变量
问题三:符号“<<”是什么意思?答案:Groovy的强大特性,操作符重载。Task.leftShift(Closure action),用来给task的action列表中添加一个action。
如果我用Groovy的写法,把它写成下面这样,是否就更好理解一些呢?
task("helloWorld").leftShift({
println 'hello world'
})
总结
当我们透过现象看到本质之后,你对Gradle的理解是不是不再是冷冰冰的闭包配置。是不是觉得Gradle其实没有那么神秘,不需要为Gradle中的奇怪的DSL感到困惑,它只是个API,读下API文档就好了。
最后总结,Gradle is Groovy but more than Groovy。
参考资料:
1.http://gradle.org/docs/current/userguide/writing_build_scripts.html
2.http://gradle.org/docs/current/javadoc/
3.http://docs.groovy-lang.org/latest/html/documentation/index.html#_delegation_strategy
4.Groovy程序设计