玩转Gradle构建工具(一)、Task

476 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前言

相信大家都听过Gradle了,他也是一个构建工具,同Maven一样,但他的速度比Maven更快,Gradle构建脚本是使用Groovy或Kotlin DSL编写的,或许这个原因也是大部分人不使用的原因吧,但是他确实比Maven更方便更快,举个例子,如果你要构建一个Maven项目,是不是需要手动安装Maven,而Gradle就不需要这样了,你从github上下载任何一个Gradle的项目后,在项目根目录执行./gradlew build就可以开始构建项目了,第一步其实也是下载Gradle构建工具,下载后位于/home/用户/.gradle/wrapper/dists/下,只不过这部分通过一个自带的脚本已经帮你完成了,Maven其实也可以这样做,但是他貌似没有,其他更加方便地方就不说了,一句话,使用Gradle准没错。

所以打算写一个系列,来介绍一下Gradle,下面是本系列目录。

  1. Task
  2. Project、Task常用API
  3. 文件操作
  4. 依赖管理
  5. 多模块构建
  6. 插件编写
  7. SpringBoot插件源码分析
  8. 过度到Kotlin

Groovy

本章是第一章,所以有必要了解下Groovy。

我们都知道,值钱的是JVM,当你具有编译原理和class文件方面的知识后,就可以写出自己的语言,并跑在JVM上,Groovy就是其中之一,它既可以用于面向对象编程,也可以用作纯粹的脚本语言。

让我们看一段Groovy的代码。

new File('/home/HouXinLin/test/a.txt').withReader('UTF-8') { reader ->
    reader.eachLine {
        println it
    }
}

这段代码的作用是读取并打印指定文件中所有行,当然,写法还有很多,比如下面这样。

Files.readAllLines(Paths.get("/home/HouXinLin/test/a.txt")).each {
    println(it)
}

我们知道Java中必须从main方法开始执行,但作为脚本语言形式,第一行就可以开始执行,不需要创建类,但如果你需要的话,同样可以使用类。

我们说点和Java不同的,比如下面代码。

def range = 1..10;
range.each { println it}

def用来定义变量,..被称为范围运算符,上述这个范围就是1-10,他的返回值是一个List,可以用get等方法对这个变量操作,除了Java本身的方法,Groovy也扩展了众多,丰富了我们对Java对象的操作,一个很有特色的是Groovy还"扩展"了Object上的方法,但并不是真的在Object中增加了新方法,只是让我们用起来觉得这是在调用Object上的方法,比如下面这段代码

class User{
    String name;
    String addr;
}

def user =new User()
println(user.properties)

他的作用是获取User中的所有属性,输出如下。

[name:null, addr:null, class:class com.main.User]

在Java中,需要通过反射去获取,比较繁琐,而Groovy天然支持这种操作,当然对反射调用方法同样也是天然支持,极大简化了以往的代码量。

class User{
    void print(name){
        println (name)
    }
}

def user =new User()
user.invokeMethod("print","name")

对于已经会java或者kotlin的人来说,Groovy学习成本低,使用Gradle也避不开使用他,但这里就不详细介绍了。

闭包

但是还是得说一下闭包,因为在使用Gradle时,很多时候都是以闭包形式出现,闭包是一个匿名的代码块,可以接受参数、返回值。闭包中也可以引用其他地方声明的变量。

如下,相当于声明了一个没有名字的函数,代码块位于{}中,并将引用赋值给hello变量,这样当调用时,使用call方法即可。

def hello={
    println("hello")
}
hello.call()

也可以直接加()调用。

hello()

并且闭包都是Closure的实例。

def hello={
    println("hello")
}
println( hello instanceof  Closure)

如果需要参数,也非常方便

def hello={name->
    println(name)
}
hello("张三")

闭包中的返回值默认是最后一行,如果有return 那么结果就是return的结果。

def hello={String name->
    println(name)
     name
}
println(hello("as"))

Gradle

同Maven一样,我们需要告诉Gradle一个"配置文件",Maven中的配置文件是pom.xml,Maven会根据里面引入的依赖或者插件去构建你的项目,而Gradle的配置文件是build.gradle,安装好Gradle工具后,可以通过gradle init命令来创建一个标准的Gradle项目,创建完成后通常结构设这样的。

ls

build.gradle    gradlew  gradlew.bat  settings.gradle
  • build.gradle是我们的学习核心,大部分配置都需要在这个文件中更改。

  • gradlew注意这里有个w,w的意思是wrapper,意味包装,使用Gradle必须安装这是无疑的,但问题是需要手动还是自动呢?答案是自动,当gradlew脚本第一次在本地电脑上运行时,它自己会从网上下载Gradle,并缓存它,这就是开头我们说的,在任何地方都可以非常轻松地构建项目,这个文件还依靠gradle/wrapper/下的gradle-wrapper.jar、gradle-wrapper.properties这两个文件。如果安装好了Gradle,那么gradlew会把参数传递给它进行下一步构建。

    1. gradle-wrapper.jar用于下载Gradle的jar 文件。

    2. gradle-wrapper.properties 负责配置Gradle的属性文件,例如与此版本兼容的Gradle版本

  • gradlew.bat这是windows版本中的gradlew。

  • settings.gradle 主要在多模块的时候使用,用于配置所有包含的子模块.

还有一个 gradle.properties是可选的,它的主要目的是配置gradle本身的启动选项。

目录

Gradle有两个主要目录, 用户主目录和项目根目录,用户目录位于$USER_HOME/.gradle,用于存储全局配置属性和初始化脚本以及缓存和日志文件,这个目录结构比较多,后续下载的依赖也都在这个目录下,而由于每个项目可能会使用不同Gradle版本,那么不同的版本也都位于这里。

项目根目录就是配置我们项目的,主要信息就是由build.gradle文件确定。

任务

下面我们编写第一个Gradle构建代码。

#build.gradle

task hello {
     println 'hello gradle'
}

使用./gradlew hello运行这个任务,会看到输出。

./gradlew hello

> Configure project :
hello gradle

这里提到了任务,没错,任务是一个很好理解的词,Gradle构建由不同的任务组成,上面我们用task定义一个名为hello的任务,这个任务的任务就是输出一段话,如果用它来构建SpringBoot项目,打包时,我们会执行./gradlew bootJar,bootJar这个任务是Spring团队编写,负责将SpringBoot项目打包成一个jar文件。

还有个词叫项目(Project),每个build.gradle就对应一个Project,Gradle在执行build.gradle时都会创建一个Project对象,我们可以用this获取这个Project对象,所以也可以说,build.gradle是对一个Project的配置,在后续我们会构建一个多项目的环境,也可以叫多模块,在这个环境中,我们会看到很多build.gradle,每个build.gradle会用来配置对应模块。

Task和Project都有众多的API可供我们调用,使用Project中Api时,this可以省略,如果要查看有哪些API可以调用,可以一到 docs.gradle.org/current/jav… 这里查看。

每个Task都属于一个Project,可以使用各种方法来创建和查找任务实例。例如,TaskContainer.create(java.lang.String)使用指定名称创建一个空任务,这里的TaskContainer对象可以从Project中提供的getTasks方法获取,也可以直接使用tasks。

Task创建的时候还可以通过下面这种方法指定一个Type,Type告诉Gradle我这个Task对象会从哪个基类Task派生。Gradle本身提供了一些通用的Task,最常见的有Copy任务,当我们通过task hello(type:Copy) 创建的时候,创建的Task就是一个Copy Task,用来做文件复制,这个操作我们在下一篇文章会说。

task myTask(type: SomeType)   // SomeType可以指定任务类型,Gradle本身提供有Copy、Delete、Sync等

下面演示下通过this获取当前项目名、遍历所有项目。

task taskA {
    doFirst {
        println ('当前项目名称:'+this.getName())
        this.getAllprojects().forEach{
            println(it.name +"  "+it.projectDir)
        }
    }
}

在任务中,就可以使用Groovy的语法,比如下面代码,其作用是打印1..10

 task hello {
	def list =1..10
	list.each {
	    println(it)
	}
}

doLast、doFirst

在上面你会发现我们的代码在doFirst块中运行,也可以直接在Task块中运行,其实还有doLast 。

task hello {
    doLast {
        println("doLast")
    }
    doFirst {
        println("doFirsh")
    }
    println("hello")
}

doFirst和doLast是两个不同任务块,在doFirst块中,用于编写首先执行的任务,而doLast块用于最后执行的任务,输出如下。(这里就是闭包代码块)

> Configure project :
hello

> Task :hello
doFirst
doLast

你会发现,先打印hello,然后才会依次执行doFirst、doLast,这是因为Gradle有三个不同的阶段,也被称为生命周期。

  1. 初始化

    Gradle在初始化阶段会确定哪些项目将参与构建,并为每个项目创建一个Project实例。

  2. 配置

    解析所有的Project对象中的Task,构建好所有Task的拓扑图,在上面hello被打印时,正处于这个阶段。

  3. 执行

    执行具体的Task及其依赖Task,这个阶段执行的就是doFirst、doLast。

register

还可以使用下面方式声明一个任务

tasks.register ("hello") {

    println("hello")
}

灵活注册

下面代码通过一个循环,注册4个任务,任务名分别是task1-task4,并在hello任务中依赖task1,所以当执行hello时,会先将task1执行完毕。

4.times { counter ->
    tasks.register("task$counter") {
        doLast {
            println "task-$counter"
        }
    }
}
task hello(){
    dependsOn "task1"
    doLast {
        println ("hello")
    }
}

结果如下

> Task :task1
task-1

> Task :hello
hello

任务依赖

dependsOn用于将给定的依赖项添加到此任务,如下,在执行test任务时,需要先将hello任务执行完毕。

tasks.register('hello') {
    doLast {
        println 'hello gradle!'
    }
}
tasks.register('test') {
    dependsOn "hello"
    doLast {
        println "test task"
    }
}

还可以这样写,由方法参数指定依赖。

tasks.register('hello') {
    doLast {
        println 'hello gradle!'
    }
}
task test(dependsOn:"hello"){
    doLast {
        println "test task"
    }
}

操作现有任务

一旦注册了任务,就可以通过API访问它们,如下,tasks是TaskContainer类型,用于管理任务实例,其中named方法用来按名称查找任务,这段代码用来在原来hello任务后面继续增加一个doLast。

tasks.register('hello') {
    doLast {
        println 'hello gradle!'
    }
}
tasks.named("hello"){
    doLast{
        println(it.getName())
    }
}

也可以通过下面这种方法获取对应的Task实例。

def helloTask =tasks.named("hello").get()

然后你需要参考Task类,看看有什么方法可以调用,比如下面通过finalizedBy方法为hello任务增加一个最终执行的任务,这样当hello任务执行后,会自动执行finalized任务。

tasks.register('finalized') {
    doLast {
        println 'by!'
    }
}
def helloTask =tasks.named("hello").get()
helloTask.finalizedBy("finalized")

使用方法

Gradle提供了大量API,这需要自己参考他们的文档,比如下面,使用file方法创建一个File对象,然后遍历其目录输出其下所有文件名。

tasks.register('hello') {
    doLast {
        file("/home/HouXinLin/project/gradle-test/").list().each{
            println(it)
        }
    }
}

默认任务

如果在./gradlew后面不加任务名,那么不会执行任何任务,如果需要,可以指定一个默认的任务。

defaultTasks 'hello'
tasks.register('hello') {
    doLast {
        file("/home/HouXinLin/project/gradle-test/").list().each{
            println(it)
        }
    }
}

任务跳过

如果在任务执行时,需要动态检测这个任务到底要不要执行,可以这样做。

tasks.register('hello') {
    doLast {
        file("/home/HouXinLin/project/gradle-test/").list().each{
            println(it)
        }
    }
}
hello.configure {
    onlyIf { project.file("/home/HouXinLin/test/a.txt").exists() }
}

使用onlyIf()方法并返回一个true或false,如果任务需要执行,则应该返回true,如果应该跳过任务,则返回 false。在这里,我们检测指定文件是否存在,如果不存在,则跳过。

为任务添加描述

当要为任务添加一个描述时,需要这样做。

tasks.register('hello') {
    description '遍历目录'
    doLast {
        file("/home/HouXinLin/project/gradle-test/").list().each{
            println(it)
        }
    }
}

这样当执行./gradlew help --task hello后,将会显示这个描述。

finalizedBy

通常在java中,会使用finally来做一些资源清理,同样在gradle中也可以,通过finalizedBy指定一个任务名,当本任务执行完成后,会调用目标任务。

tasks.register('by') {
    doLast {
        println 'by'
    }
}
task test(){
    doLast {
        println "test task"
    }
    finalizedBy("by")
}

自定义任务

task xx这种声明的任务仅在该项目中可用,如果要在不同项目中使用,复制过去这种做法已经习以为常,但是更好的做法是使用自定义任务,打包成一个jar后,所有项目依赖这个jar即可。

自定义任务通过继承DefaultTask来创建,并使用@TaskAction注解来告诉Gradle这个方法将作为任务执行的方法。

在这里,为了简单起见,自定义任务在同一个build.gradle中定义。

abstract class HelloTask extends DefaultTask {
    @Input
    String message;

    @TaskAction
    def hello() {
        println "hello ${message}"
    }
}
task hello(type:HelloTask){
    message="gradle"
}

这段代码会输出hello gradle,注意的是,参数输入必须加入@Input注解。

同样也可以通过构造方法接收参数,同时构造方法中也必须加入@javax.inject.Inject注解。

abstract class HelloTask extends DefaultTask {
    @Input
    String message;
    @Inject
    HelloTask(String message) {
        this.message = message
    }
    @TaskAction
    def hello() {
        println "hello ${message}"
    }
}

tasks.register("hello",HelloTask,"gradle")