即Gradle的发布包,下载路径:https://services.gradle.org/,Gradle在2019年开启了中国区的CDN,所以在中国区下载Gradle也很快了。Gradle有三种类型的发布包。
我们一般不通过Distribution方式来下载Gradle,而是通过Wrapper方式(后面会讲到)。
Gradle的目录结构如下
跟Maven、Groovy很像(因为他们都是基于JVM的程序),bin/gradle是linux环境的启动脚本,bin/gradle.bat是windows环境的启动脚本,会启动一个jvm并加载lib目录下的所有jar(都是Gradle运行所需要的库)。
参考官方文档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
在一个项目中,Gradle除了会与项目目录打交道,还会GRADLE_USER_HOME打交道。
.gradle/init.d
目录用来对整个电脑的所有gradle项目做统一处理,譬如仓库替换(换成阿里云镜像,来加速下载);
.gradle/caches
目录用来存储本地缓存的jar包;
参考官方文档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中已经被缓存了,从而大大提升速度。
在默认情况下,Daemon是开启的,但Client与Daemon会有版本不兼容或者构建所需要的参数不兼容问题,如果不兼容,Client会自动创建一个新的Daemon,当然旧的Daemon在空闲3h后会自动被销毁。
若不想启用Daemon,可以在构建命令中添加启动参数-- no-daemon
。但推荐所有的构建都使用Daemon,因为可加速构建。
每次执行gradlew xxx
命令时,gradle都会先去找机器上有没有安装这个gradlew指定版本的gradle,若没有就会先去下载,存放到GRADLE_USER_HOME中,然后查找与该版本Gradle兼容及构建所要求的参数兼容的Daemon JVM,若没找到就启动一个,否则就通过socket连接这个Daemon,然后把当前任务以及相关的上下文(eg.当前项目路径、当前环境变量)发给Daemon JVM,Daemon JVM会处理这一切。
Groovy对自己的定义就是:Groovy是在 java平台上的、 具有像Python, Ruby 和 Smalltalk 语言特性的灵活动态语言, Groovy保证了这些特性像 Java语法一样被 Java开发者使用。
推荐读《Groovy in action》,这本书是基于groovy2.4写的。
Idea中"Tools" -> “Groovy Console…”,可以很方便的编写及执行Groovy代码。
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()方法,传入一个闭包。
一个项目有一个 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 Build Lifecycle
对应项目根目录的setting.gradle
对应每个Module单独的 build.gradle,build.gradle中一切无主的方法,都会去Project中查找。
Gradle中最小的执行单元是Task,一个Task可以声明一些操作,Task之间可以进行互相依赖。
[build.gradle]
task('helloworld', {
println('configure')
doLast({
println('Executing task')
})
})
任何的gradle任务,都会经历三个阶段。在配置阶段,执行到’task('helloworld', {xxx})
时,首先会创建一个名为"helloworld"的Task,然后配置该Task:对该Task运行后面的闭包(该Task为后面闭包的this,用的是Grrovy的delegate机制),怎么运行呢?当然是一行一行地执行闭包中的代码啦。注意:task方法的闭包中的doLast
方法是把doLast方法中的闭包添加到这个Task的 action list 的末尾,但不真正的执行它,只有在真正执行这个Task的时候才被执行。
gradle help 是一个特殊任务。
若执行gradlew helloworld
,输出如下:
根据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}")
}
}
}
Task之间可以相互依赖
task('first') {
doLast { println('executing first task') }
}
(0..10).each { i->
task('task' + i) {
if (i % 2 == 0) {
dependsOn('first')
}
}
}
奇数号的Task不依赖first-Task
偶数号的Task依赖first-Task,所以Gradle在执行他们之前,会自动先去执行first-Task
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
}
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')
}
}
}
略
[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
参考官方文档Using Gradle Plugins
上面的简单插件方式看起来貌似没什么用,代码还是很长,其实我们可以将声明的class放在任何地方,譬如放在了网络上,则可以这样引入:
// Gradle会去下载这个脚本,然后按照与简单插件相同的方式去调用apply()
apply plugin: 'http://myserver.com/my-script'
首先需要明白两个classpath的概念。gradle脚本(build.gradle)的classpath与java项目(CompileJava)的classpath是互相独立的。
譬如,若想要在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约定:在外层的build.gradle执行之前,若发现内部有buildSrc项目,Gradle会先编译buildSrc,然后自动把打包输出的jar(buildSrc/build/libs/buildSrc.jar)添加到外层build.gradle -> buildScript -> dependencies -> classpath 中去。所以我们在外层build.gradle中直接apply plugin: myPlugin
就行了。这样就实现了把插件逻辑抽取出来,放到一个独立的buildSrc项目中。这一过程非常像Maven中的将项目install到本地仓库,然后其他项目可以很方便的引用它。具体操作如下:
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);
}
}
}
apply plugin:xxx
引入自定义的插件,然后刷新项目。apply pugin: MyPlugin
gradlew task2
当我们的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
中使用引入了该插件。
先下载com.android.tools.build:gradle:$gradleVersion
这个插件的二进制jar包并解压,方便后面分析
分析得知:当Gradle执行到app/build.gradle中的apply plugin:‘com.android.applicaiton’
时,就会从classpath的所有jar中找到com.android.applicaiton.properties
文件(一般位置是/META-INF/gradle-plugins/xx.properties
),然后根据properties的内容,得到插件实现类,再反射创建实现类的实例并调用它的apply()
方法。
[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插件中配置的目录。
[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")
})
}
如果你的项目使用了Gradle作为构建工具,那么你一定要使用Gradle来自动生成IDE的项目文件,无需再手动的将源代码导入到你的IDE中去了。
在build.gradle中引入idae插件:
apply plugin: "idea"
然后在命令行中输入gradle idea
就可以生成idea的项目文件,直接使用idea打开生成的项目文件即可。如果在已经存在Intellij的项目文件情况下,想根据build.gradle中的配置来更新项目文件,可以输入gradle cleanIdea idea
。cleanIdea
可以清除已有的Intellij项目文件。
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
}
}
第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中的内部定义的。