Gradle入门教程学习笔记

文章目录

  • 一、Gradle基础
    • Gradle的基础概念
      • Distribution
      • Wrapper
      • GradleUserHome
      • Daemon
      • 总结
    • Groovy基础
      • 动态调用与MOP
      • 闭包
  • 二、Gradle构建
    • Gradle的核心模型
      • Lifecycle
      • Setting
      • Project
      • Task
      • TaskContainer
      • Hook
  • 三、Gradle插件的编写
    • 构建逻辑的复用
    • 简单插件
    • Script插件
    • BuildSrc插件
    • Binary(二进制)插件
  • 四、实际插件分析
  • 五、补充-java插件的war
  • 六、补充-java插件的test
  • 七、补充-idea插件
  • 八、补充-gradle.properties文件

本笔记对应的学习视频连接: 来自Gradle开发团队的Gradle入门教程,不适合纯小白观看,建议先看一下这一篇博客: Gradle 功能及使用详解。

一、Gradle基础

Gradle的基础概念

Distribution

即Gradle的发布包,下载路径:https://services.gradle.org/,Gradle在2019年开启了中国区的CDN,所以在中国区下载Gradle也很快了。Gradle有三种类型的发布包。
Gradle入门教程学习笔记_第1张图片

  • Gradle-src:Gradle的源码包,在https://github.com/gradle/gradle上也能找到。
  • Gradle-bin:包含Gradle的可运行程序
  • Gradle-all: 包含Gradle的可运行程序、用户文档、sample

我们一般不通过Distribution方式来下载Gradle,而是通过Wrapper方式(后面会讲到)。

Gradle的目录结构如下
Gradle入门教程学习笔记_第2张图片
跟Maven、Groovy很像(因为他们都是基于JVM的程序),bin/gradle是linux环境的启动脚本,bin/gradle.bat是windows环境的启动脚本,会启动一个jvm并加载lib目录下的所有jar(都是Gradle运行所需要的库)。

Wrapper

参考官方文档The Gradle Wrapper。

Gradle的发布速度非常快,目前基本上每6周发布一次,新的版本难免会不兼容旧版本,所以为了在不同Gradle版本之中保持稳定的项目构建,就需要gradle wrapper。wrapper 有版本区分,但是并不需要你手动去下载(简化了Gardle的安装和部署),当你运行脚本的时候,如果本地没有部署Gardle就使用gradle wrapper命令自动下载对应版本文件。

使用命令运行包装任务,并提供选项:

//  --gradle-version 6.8.2 --distribution-type all
$ gradle wrapper --gradle-version 6.8.2 --distribution-type all
> Task :wrapper
BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

为了使包装器文件对其他开发人员和执行环境可用,您需要将它们签入版本控制中。所有包装文件,都包括一个非常小的JAR文件,请将JAR文件添加到版本控制中。有些组织不允许项目向版本控制提交二进制文件。目前,除了该方法之外,没有其他选择可供选择。

该命令会在你的Gradle安装目录下生成一个很小的包装器jar文件、一个包装器properties文件和两个批处理文件(linux版、windows版)。让我们看看下面的项目布局,以说明包装文件

├── a-subproject
│   └── build.gradle
├── settings.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar					// 只有50KB,里面的代码用来下载真正的对应版本的Gradle distribution
│       └── gradle-wrapper.properties			// 负责配置包装器运行时行为的属性文件
├── gradlew										// shell脚本,用于使用包装器来执行构建
└── gradlew.bat									// Windows批处理脚本,用于使用包装器来执行构建

因此,您可以在包装器properties文件中找到所需的信息。例如,生成的distributionUrl:

distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip

使用wrapper批处理文件来执行构建:

$ gradlew.bat build
Downloading https://services.gradle.org/distributions/gradle-5.0-all.zip
.....................................................................................
Unzipping C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0-all.zip to C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-al\ac27o8rbd0ic8ih41or9l32mv
Set executable permissions for: C:\Documents and Settings\Claudia\.gradle\wrapper\dists\gradle-5.0-all\ac27o8rbd0ic8ih41or9l32mv\gradle-5.0\bin\gradle

BUILD SUCCESSFUL in 12s
1 actionable task: 1 executed

gradlew = gradle-wrapper

更新wrapper版本:

$ ./gradlew wrapper --gradle-version 6.8.2

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed

GradleUserHome

在一个项目中,Gradle除了会与项目目录打交道,还会GRADLE_USER_HOME打交道。
在这里插入图片描述
.gradle/init.d目录用来对整个电脑的所有gradle项目做统一处理,譬如仓库替换(换成阿里云镜像,来加速下载);
.gradle/caches目录用来存储本地缓存的jar包;

Daemon

参考官方文档The Gradle Daemon

Maven中,每次构建项目都会创建一个Mvn JVM,先加载构建所需的所有jar包及一些上下文之类的东西,然后开始构建项目,构建完成后,这个JVM就被销毁了。所以每次构建都会比较耗时。

而Gradle3.0中,有了Client JVM和Daemon JVM,每次构建项目时会创建一个Client JVM,只用来接收请求和转发请求到Daemon JVM,所以它很轻量很快,当构建结束后,Client JVM会被销毁,而Daemon JVM会一直存在。若下次再来一个新的请求,Client JVM会再次被创建并转发请求到Daemon JVM,而此时构建所需的jar包或者相关的项目上下文之类的东西都在Daemon JVM中已经被缓存了,从而大大提升速度。
Gradle入门教程学习笔记_第3张图片

在默认情况下,Daemon是开启的,但Client与Daemon会有版本不兼容或者构建所需要的参数不兼容问题,如果不兼容,Client会自动创建一个新的Daemon,当然旧的Daemon在空闲3h后会自动被销毁。
Gradle入门教程学习笔记_第4张图片
若不想启用Daemon,可以在构建命令中添加启动参数-- no-daemon。但推荐所有的构建都使用Daemon,因为可加速构建。

总结

每次执行gradlew xxx命令时,gradle都会先去找机器上有没有安装这个gradlew指定版本的gradle,若没有就会先去下载,存放到GRADLE_USER_HOME中,然后查找与该版本Gradle兼容及构建所要求的参数兼容的Daemon JVM,若没找到就启动一个,否则就通过socket连接这个Daemon,然后把当前任务以及相关的上下文(eg.当前项目路径、当前环境变量)发给Daemon JVM,Daemon JVM会处理这一切。

Groovy基础

Groovy对自己的定义就是:Groovy是在 java平台上的、 具有像Python, Ruby 和 Smalltalk 语言特性的灵活动态语言, Groovy保证了这些特性像 Java语法一样被 Java开发者使用。

推荐读《Groovy in action》,这本书是基于groovy2.4写的。

Idea中"Tools" -> “Groovy Console…”,可以很方便的编写及执行Groovy代码。
Gradle入门教程学习笔记_第5张图片

动态调用与MOP

Groovy的NOP类似于java的invokedynamic,底层就是使用的发射调用。这也是Groovy的很多语法糖的底层原理。

闭包

Grrovy DSL(领域专属语言)的一些约定。

println(test(21, {2 * it}))
// 在不会引起歧义的情况下,可以省略圆括号
println test(21, {2 * it})
// 当闭包为最后一个参数时,可以放在圆括号的外面
println test(21) {2 * it}

更多约定参考Groovy Language Documentation,理解了这个文档后,就会发现gradle的脚本非常简单了!
在这里插入图片描述
本质上等效于
在这里插入图片描述
即调用Project(后面会讲到)的plugins()方法,传入一个闭包。

二、Gradle构建

Gradle的核心模型

Gradle入门教程学习笔记_第6张图片
一个项目有一个 setting.gradle、一个顶层的 build.gradle文件、每个Module 都有自己的一个 build.gradle文件。

  • setting.gradle: 这个 setting 文件定义了哪些module 应该被加入到编译过程,对于单个module 的项目可以不用需要这个文件,但是对于 multimodule 的项目我们就需要这个文件,否则gradle 不知道要加载哪些项目。这个文件的代码在初始化阶段就会被执行。

  • 顶层的build.gradle: 顶层的build.gradle文件的配置最终会被应用到所有项目中。它典型的配置如下:

    // buildscript 定义了 Android 编译工具的类路径。repositories中,jCenter是一个著名的 Maven 仓库。
    buildscript {
        repositories {
            jcenter()
        }
     
        dependencies {
            classpath 'com.android.tools.build:gradle:1.2.3'
        }
    }
    
    // allprojects 中定义的属性会被应用到所有 module 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。
    allprojects{
        repositories{
            jcenter()
        }
    }
    
  • 每个Module单独的 build.gradle: 针对每个module 的配置,如果这里的定义的选项和顶层build.gradle定义的相同,后者会被覆盖。

注意:网上有人说可以把GRADLE_USER_HOME设置为maven的localRepository路径,就可以与maven本地仓库共用相同的jar包了。实际上是不可以的,因为gradle本地仓库的目录结构与maven的不一样。
Gradle入门教程学习笔记_第7张图片
Gradle入门教程学习笔记_第8张图片

Lifecycle

参考Gradle Build Lifecycle

  • 初始化阶段:Gradle支持单个和多项目构建。在初始化阶段,Gradle确定哪些项目将参与构建,并为每个项目创建一个Project实例。
  • 配置阶段:在此阶段,将配置项目对象。所有要参与到这次构建中的项目的构建脚本都被执行。即把build.gradle中的代码从头到尾执行一遍。注意:任务的闭包类型的参数中的代码是不会立马执行的,要在真正执行这个任务的时候才会执行
  • 执行阶段:Gradle确定要执行的在配置阶段创建和配置的任务子集。子集由传递给Gradle命令和当前目录的任务名称参数确定。然后,Gradle执行每个选定的任务。

Setting

对应项目根目录的setting.gradle

Project

对应每个Module单独的 build.gradle,build.gradle中一切无主的方法,都会去Project中查找。

Task

Gradle中最小的执行单元是Task,一个Task可以声明一些操作,Task之间可以进行互相依赖。
[build.gradle]

task('helloworld', {
    println('configure')

    doLast({
        println('Executing task')
    })
})

Gradle入门教程学习笔记_第9张图片
Gradle入门教程学习笔记_第10张图片

任何的gradle任务,都会经历三个阶段。在配置阶段,执行到’task('helloworld', {xxx})时,首先会创建一个名为"helloworld"的Task,然后配置该Task:对该Task运行后面的闭包(该Task为后面闭包的this,用的是Grrovy的delegate机制),怎么运行呢?当然是一行一行地执行闭包中的代码啦。注意:task方法的闭包中的doLast方法是把doLast方法中的闭包添加到这个Task的 action list 的末尾,但不真正的执行它,只有在真正执行这个Task的时候才被执行。

若执行 ./gradlew help,输出如下:
Gradle入门教程学习笔记_第11张图片

gradle help 是一个特殊任务。

若执行gradlew helloworld,输出如下:
Gradle入门教程学习笔记_第12张图片
根据Groovy的DSL,上述脚本代码可以简化为(感觉看起来很别扭,可读性比较差):

task 'helloworld' {
    println 'configure'

    doLast{
        println('Executing task')
    }
}

Gradle甚至还支持动态地创建Task
[build.gradle]

for (int i = 0; i < 5; i++) {
    task('task' + i) {
        // 若要在闭包中使用闭包外的变量,需要先捕获住它。
        def capturedI = i;
        doLast {
            println("executing task ${capturedI}")
        }
    }
}

Gradle入门教程学习笔记_第13张图片

Task之间可以相互依赖

task('first') {
    doLast { println('executing first task') }
}

(0..10).each { i->
    task('task' + i) {
        if (i % 2 == 0) {
            dependsOn('first')
        }
    }
}

奇数号的Task不依赖first-Task
Gradle入门教程学习笔记_第14张图片
偶数号的Task依赖first-Task,所以Gradle在执行他们之前,会自动先去执行first-Task
Gradle入门教程学习笔记_第15张图片

TaskContainer

org.gradle.api.tasks.TaskContainer 是一个接口,表示装有所有Task的容器,使用 Project.getTasks()可得到该容器,所以在 build.gradle 中直接写 tasks 等效于 Project.getTasks() ,得到的是TaskContianer实例。TaskContianer接口中包含以下主要方法:

//创建task
create(name: String, configure: Closure): Task 
create(name: String, type: Class): Task
create(options: Map<String, ?>, configure: Closure): Task

//根据类型查找Task
withType(type: Class): TaskCollection
//根据名称查找Task
getByName(name: String): Task

//任何一个task被添加加到Task容器中时
whenTaskAdded(action: Closure)

示例:

// tasks.withType(JavaCompile) = project.getTasks().withType(JavaCompile)
tasks.withType(JavaCompile) {
	// 闭包内是对在Task容器中找到的所有JavaCompile类型的task进行配置,可配置项都在JavaCompile类中!
	// 这行代码 = JavaCompile.getOptions().setEncoding("UTF-8")
    options.encoding = "UTF-8"
    // 这行代码 = JavaCompile.setSourceCompatibility(JavaVersion.VERSION_1_8)
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

Hook

afterEvaluate是把方法中的闭包放在所属project的action list中,并不会真正执行它,等所属project实例的evaluate完成后才执行。
[build.gradle]

afterEvaluate {
    println('after evaluate')
}

task('first') {
    println('configuring  first')
    doLast { println('executing first task') }
}

(0..5).each { i->
    task('task' + i) {
        println("configuring task $i")
        if (i % 2 == 0) {
            doLast {println("executing task $i")}
            dependsOn('first')
        }
    }
}

执行gradlew task4
Gradle入门教程学习笔记_第16张图片

三、Gradle插件的编写

构建逻辑的复用

简单插件

[build.gradle]

class MyAwesomePlugin implements Plugin<Project> {
    // 我们接触到的绝大多数的apply参数类型都是Project
    @Override
    void apply(Project project) {
        (0..5).each { i->
            project.task('task' + i) {
            	println("configuring task $i")
                if (i % 2 == 0) {
                    dependsOn('first')
                }
                def capturedI = i;
                doLast {
                    println("executing task $capturedI")
                }
            }
        }
    }
}
//等价于 apply([plugin: MyAwesomePlugin]),是Projrct继承自PluginAware的apply(Map var1)方法。
//所以这一行就是在调用Project#apply(Map)方法,这个方法的内部,Gradle会实例化一个MyAwesomePlugin对象并调用它的apply(Project)方法。
apply plugin: MyAwesomePlugin

执行gradlew task4,效果跟之前完全一样
Gradle入门教程学习笔记_第17张图片

Script插件

参考官方文档Using Gradle Plugins

上面的简单插件方式看起来貌似没什么用,代码还是很长,其实我们可以将声明的class放在任何地方,譬如放在了网络上,则可以这样引入:

// Gradle会去下载这个脚本,然后按照与简单插件相同的方式去调用apply()
apply plugin: 'http://myserver.com/my-script'

BuildSrc插件

首先需要明白两个classpath的概念。gradle脚本(build.gradle)的classpath与java项目(CompileJava)的classpath是互相独立的。
Gradle入门教程学习笔记_第18张图片
譬如,若想要在build.gradle中使用apache-commons的StringUtils,则需要把它添加到gradle脚本的classpath中。
[build.gradle]

import org.apache.commons.lang3.StringUtils

// buildscript中的声明是gradle脚本自身需要使用的资源(并非项目需要)。可以声明的资源包括依赖项、第三方插件、maven仓库地址等。
buildscript {
    repositories {
        mavenCentral()
    }
    // 添加到gradle脚本的classpath
    dependencies {
        //注意这里是'classpath',而不是'compile'
        classpath(group: 'org.apache.commons', name: 'commons-lang3', version: '3.9')
        
		//1.从repositories中指定的仓库中找对应jar,并添加到gradle的classpath
        classpath 'mysql:mysql-connector-java:5.1.40'
        classpath "org.flywaydb:flyway-gradle-plugin:4.0.3"
        //2.将项目的 /gradleLibs/dependency-management-plugin-1.0.5.RELEASE.jar 和 /libs/commons-lang-2.6.jar 添加到gradle的classpath
        classpath files('gradleLibs/dependency-management-plugin-1.0.5.RELEASE.jar', 'libs/commons-lang-2.6.jar')
        //3.将项目的 /gradleLibs 目录下所有jar添加到gradle的classpath
        classpath fileTree(include: ['*.jar'], dir: 'gradleLibs')
    }
}

// 因为buildscript中引入了commons-lang3包,所以在gradle的脚本中就可以使用StringUtils了
if (StringUtils.isNoneEmpty('')) {
    // execute builds
}

同理,若想要在build.gradle中apply自定义的plugin,也需要把它添加到gradle脚本的classpath中
Gradle入门教程学习笔记_第19张图片
gradle约定:在外层的build.gradle执行之前,若发现内部有buildSrc项目,Gradle会先编译buildSrc,然后自动把打包输出的jar(buildSrc/build/libs/buildSrc.jar)添加到外层build.gradle -> buildScript -> dependencies -> classpath 中去。所以我们在外层build.gradle中直接apply plugin: myPlugin就行了。这样就实现了把插件逻辑抽取出来,放到一个独立的buildSrc项目中。这一过程非常像Maven中的将项目install到本地仓库,然后其他项目可以很方便的引用它。具体操作如下:

  1. 先在外层根目录下新建buildSrc/src/main/java目录,然后点击Gradle工具窗的“Reload All Projects”按钮,Gradle就会自动识别出该buildSrc项目了。外层的build.gradle什么都不用做,就可以自动识别!
    Gradle入门教程学习笔记_第20张图片
    Gradle入门教程学习笔记_第21张图片
  2. 在buildSrc/src/main/java目录下编写自定义的插件类,eg. 路径为[buildSrc/src/main/java/MyPlugin.java]
import org.gradle.api.Plugin;
import org.gradle.api.Project;

// 注意现在写的是java代码了!
public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        for (int i = 0; i < 5; i++) {
        	// 注意task后面不要有空格
            project.task("task" + i);
        }
    }
}
  1. 在外层[build.gradle]中直接使用apply plugin:xxx 引入自定义的插件,然后刷新项目。
apply pugin: MyPlugin

Gradle入门教程学习笔记_第22张图片
Gradle入门教程学习笔记_第23张图片

  1. 执行自定义的任务
gradlew task2

Gradle入门教程学习笔记_第24张图片

Binary(二进制)插件

当我们的buildSrc足够成熟的时候,可以将它从我们的项目移除出来,变成一个真正的独立的插件项目,把它发布到仓库上,然后就可以在其他要使用它的项目中先手动将它添加到build.gradle -> buildScript -> dependencies -> classpath 中,然后用apply plugin: xxx引入该插件了。

四、实际插件分析

分析GitHub - android/sunflower,可以看到在build.gradle中添加了com.android.tools.build:gradle:$gradleVersion到buildScript的classpath中,然后在app/build.gradle中使用引入了该插件。

Gradle入门教程学习笔记_第25张图片
Gradle入门教程学习笔记_第26张图片

先下载com.android.tools.build:gradle:$gradleVersion这个插件的二进制jar包并解压,方便后面分析
Gradle入门教程学习笔记_第27张图片
Gradle入门教程学习笔记_第28张图片

分析得知:当Gradle执行到app/build.gradle中的apply plugin:‘com.android.applicaiton’时,就会从classpath的所有jar中找到com.android.applicaiton.properties文件(一般位置是/META-INF/gradle-plugins/xx.properties),然后根据properties的内容,得到插件实现类,再反射创建实现类的实例并调用它的apply()方法。
Gradle入门教程学习笔记_第29张图片

五、补充-java插件的war

[build.gradle]

apply plugin 'java'

war{
    from("src/main/java/com/xx/dao") {
        include "*.xml"
        into("WEB-INF/classes/com/xx/dao")
    }

    //将依赖的jar包拷贝到lib 下
    task copyJars(type:Copy) {
        from configurations.runtime
        into 'src/main/webapp/WEB-INF/lib' // 目标位置
    }
}

这样设置之后,可以看到Project Settings -> modules -> Web Gradle -> Web Resource Directories 新增了一个目录,就是我们在war插件中配置的目录。
Gradle入门教程学习笔记_第30张图片

六、补充-java插件的test

[build.gradle]

apply plugin 'java'

test {
	//使得执行gradle的test任务时,testable的Mock方法能够生效
    jvmArgs("-javaagent:${classpath.find { it.name.contains("testable-agent") }.absolutePath}")
    
	//(默认)开启Junit支持,还可以useTestNG()、useTestFramework()
    useJUnit()
    
    //模糊匹配测试类
    //include 'com/xx/upload/**'
    //精确匹配测试类
    //include 'com/xx/upload/UploadProcessTaskTest.class'
    filter({
        //精确匹配测试类
        //includeTestsMatching("com.xx.upload.UploadProcessTaskTest")
        //精确匹配测试方法
        //includeTestsMatching("com.xx.upload.UploadProcessTaskTest.getFiles")
        //模糊匹配测试类
        includeTestsMatching("com.xx.upload.*Test")
    })  
}

七、补充-idea插件

如果你的项目使用了Gradle作为构建工具,那么你一定要使用Gradle来自动生成IDE的项目文件,无需再手动的将源代码导入到你的IDE中去了。

在build.gradle中引入idae插件:

apply plugin: "idea"

然后在命令行中输入gradle idea就可以生成idea的项目文件,直接使用idea打开生成的项目文件即可。如果在已经存在Intellij的项目文件情况下,想根据build.gradle中的配置来更新项目文件,可以输入gradle cleanIdea ideacleanIdea可以清除已有的Intellij项目文件。

Intellij主要有以下几种项目文件:

  • .ipr Intellij工程文件
  • .iml Intellij 模块文件
  • .iws Intellij 工作区文件

如果只简单的使用gradle idea生成Intellij的工程文件,其实在使用Intellij打开项目以后,我们还要做一些手工配置,比如指定JDK的版本,指定源代码管理工具等。Gradle的idea命令本质上就是生成这三个xml文件,所以Gradle提供了生成文件时的hook(钩子),让我们可以方便的做定制化,实现最大程度的自动化。这就需要自定义idea这个任务了。

//若mapper.xml不在resource包下,且使用的开发工具是IDEA,则必须添加该配置
//task mapperXmlCopy(type: Copy) {
//    copy {
//        from("src/main/java") {   //把src.main.java目录下的所有静态资源(例如XML)
//            include ("**/*Mapper.xml")        //标明以Mapper结尾的XML文件资源
//        }
//        into("${projectDir}/out/production/resources")   //拷贝到build后那些在resource包下的资源文件输出的目录(out/production/resources)下,测试模块的也要copy吧?
//    }
//    print "Copy Success\n"
//}

apply plugin: "idea"
idea {
    project {
        //配置项目的jdk及languageLevel
        jdkName = '1.8'
        languageLevel = '1.8'
    }

    module {
        //默认为false,即模块的编译输出目录不继承项目的编译输出目录
        inheritOutputDirs = false
    }
}

八、补充-gradle.properties文件

第1点:build.gradle中可以自动读取gradle.properties中配置的key-value,请注意key尽量不要使用“.”和“-”,因为在groovy的语法中有特殊含义,建议使用驼峰命名。

gradle.properties

springfoxSwaggerVersion=2.1.0

build.gradle

compile(
// 使用${key}的方式读取value
"io.springfox:springfox-swagger2:${springfoxSwaggerVersion}",
// 若不引起争议,花括号可以省略
"io.springfox:springfox-swagger-ui:$springfoxSwaggerVersion",
)

第2点:若项目中使用了“io.spring.dependency-management”插件(具体使用参考我的另一篇博客Spring中依赖版本管理),可以通过gradle.properties文件覆盖jar的默认版本。注意覆盖的key必须是在spring-boot-dependencies.pom或platform-bom.pom中的内部定义的。

你可能感兴趣的:(maven/gradle,gradle,java)