gradle小解

169 阅读36分钟

gradle入门

基本介绍

  • Ant: 2000 年Apache 推出的纯Java 编写构建工具,通过xml[build.xml] 文件管理项目

优点:使用灵活,速度快(快于gradle和maven),

缺点:Ant没有强加任何编码约定的项目目录结构,开发人员需编写繁杂 XML 文件构建指令,对开发人员是一个挑战。

  • Maven: 2004年Apache 组织推出的再次使用xml文件[pom.xml]管理项的构建工具。

优点: 遵循一套约定大于配置的项目目录结构,使用统一的 GAV 坐标进行依赖管理,侧重于包管理.

缺点:项目构建过程僵化,配置文件编写不够灵活、不方便自定义组件,构建速度慢于gradle。

  • Gradle: 2012 年 Google 推出的基于 Groovy 语言的全新项目构建工具,集合了 Ant 和 Maven 各自的优势。

优点:集 Ant 脚本的灵活性+Maven 约定大于配置的项目目录优势支持多种远程仓库和插件,侧重于大项目构建

缺点:学习成本高、资料少、脚本灵活、版本兼容性差等。

gradle也是需要配置环境变量的,另设GRADLE_USER_HOME变量设置gradle的本地仓库位置

image.png

Gradle 项目默认目录结构和 Maven 项目的目录结构一致,都是基于约定大于配置【Convention Over Configuration】

其完整项目目录结构如下所示:

image.png tips:gradlew与gradlew.bat执行的是指定wrapper版本中的gradle指令,不是本地安装的gradle指令哦

可以借助spring脚手架创建gradle第一个项目:Spring Initializr 使用gradle命令创建项目

gradle init

image.png image.png

常用指令

gradle常用命令说明

常用gradle命令作用
gradle clean清空build目录
gradle classes编译业务代码和配置文件(/src/main目录下)
gradle test编译测试代码,生成测试报告(/src/test目录下)
gradle build构建项目(默认执行gradle classes与gradle test)
gradle build -x test跳过测试构建构建

需要注意的是,gradle的指令要在含有build.gradle的目录执行

修改maven下载源 Gradle自带的 Maven 源地址是国外的,该 Maven 源在国内的访问速度是很慢的,除非使用了特别的手段。一般情况下我们建议使用国内的第三方开放的Maven 源或企业内部自建Maven 源。

认识init.d文件夹

我们可以在 gradle 的 init.d目录下创建以 .gradle结尾的文件 .gradle 文件可以实现在 build 开始之前执行,所以你可以在这个文件配置一些你想预先加载的操作。

在init.d文件夹创建init.gradle文件

allprojects {
    repositories {
        mavenLocal()
        maven { name "Alibaba" ; url "https://maven.aliyun.com/repository/public"}
        maven { name "Bstek" ; url "https://nexus.bsdn.org/content/groups/public/"}
        mavenCentral()
    }
    buildscript{
        repositories {
            maven { name"Alibaba" ; url 'https://maven.aliyun.com/repository/public'}
            maven { name"Bstek"; url 'https://nexus.bsdn.org/content/groups/public/'}
            maven { name"M2" ; url 'https://plugins.qradle.org/m2/'}
        }
    }
}

项目所需要的jar包会从repositories指定的这些仓库去下载,而buildscript是给build.gradle构建脚本使用的,如果构建脚本本身需要一些依赖,可以去repositories仓库里去下载

下载jar 的时候是按照从上到下的顺序依次执行的

拓展一 启用init.gradle文件的方法有:

  1. 在命令行指定文件,例如:gradle-init-script yourdir/init.gradle-q taskName。你可以多次输入此命令来指定多个init文件
  2. 把init.gradle文件放到USER_HOME/.gradle/init.d/目录下
  3. 把以.gradle结尾的文件放到USER_HOME/.gradle/init.d/目录下
  4. 把以.gradle结尾的文件放到GRADLE_HOME/init.d/目录下

如果存在上面的4种方式的2种以上,gradle会按上面的1-4序号依次执行这些文件,如果给定目录下存在多个init脚本,会按拼音a-z顺序执行这些脚本,每个init脚本都存在一个对应的gradle实例,你在这个文件中调用的所有方法和属性,都会委托给这个gradle实例,每个init脚本都实现了Script接口。

拓展二 仓库地址说明

  • mavenLocal(): 指定使用maven本地仓库,而本地仓库在配置maven时settings文件指定的库位置如E:/repository.gradl查找jar包顺序如下:USER_HOME/m2/settings.xml>> M2_HOME/conf/settings.xml>>USER_HOME/.m2/repository
  • maven {url 地址},指定maven仓库,一般用私有仓库地址或其它的第三方库【比如阿里镜像仓库地址】
  • mavenCentral():这是Maven的中央仓库,无需配置,直接声明就可以使用。
  • jcenter():JCenter中央仓库,实际也是用的maven搭建的,但相比Maven仓库更友好,通过CDN分发,并且支持https访问,在新版本中已经废弃了,替换为了mavenCentral()

总之,gradle可以通过指定仓库地址为本地maven仓库地址和远程仓库地址相结合的方式,避免每次都会去远程仓库下载依赖库,这种方式也有一定的问题,如果本地maven仓库有这个依赖,就会从直接加载本地依赖,如果本地仓库没有该依赖,那么还是会从远程下载·但是下载的jar不是存储在本地maven仓库中,而是放在自己的缓存目录中,默认在USER_HOME/.gradle/caches目录,当然如果我们配置过GRADLE_USER_HOME环境变量,则会放在GRADLE_USER_HOME/caches目录,那么可不可以将gradle caches指向maven repository。我们说这是不行的,caches下载文件不是按照maven仓库中存放的方式·

拓展三 阿里云仓库地址

参考仓库服务 (aliyun.com)

wrapper包装器

Gradle Wrapper实际上就是对Gradle的一层包装,用于解决实际开发中可能会遇到的不同的项目需要不同版本的 Gradle问题。例如: 把自己的代码共享给其他人使用,可能出现如下情况

1.对方电脑没有安装 gradle

2.对方电脑安装过 gradle,但是版本太旧了

这时候,我们就可以考虑使用Gradle Wrapper了。这也是官方建议使用Gradle Wrapper的原因。实际上有了Gradle Wrapper之后,我们本地是可以不配置Gradle的,下载Gradle项目后使用gradle项目自带的wrapper操作也是可以的。

那如何使用Gradle Wrapper呢?

项目中的gradlew、gradlew.cmd脚本用的就是wrapper中规定的gradle版本。参见源码

而我们上面提到的gradle指令用的是本地gradle,所以gradle指令和gradlew指令所使用的gradle版本有可能是不一样的。 gradlew、gradlew.cmd的使用方式与gradle使用方式完全一致,只不过把gradle指令换成了gradlew指令

image.png

image.png

当然,我们也可在终端执行 gradlew 指令时,指定指定一些参数,来控制Wrapper的生成,比如依赖的版本等,如下:

参数名说明
--gradle-version用于指定使用的Gradle版本
--gradle-distribution-url用于指定下载Gradle发行版的url地址
gradle wrapper --gradle-version=7.4.1

GradleWrapper的执行流程

  1. 当我们第一次执行./gradlew build 命令的时候,gradlew 会读取 gradle-wrapper.properties 文件的配置信息
  2. 准确的将指定版本的 gradle 下载并解压到指定的位置(GRADLE_USER_HOME目录下的wrapper/dists目录中)
  3. 并构建本地缓存(GRADLE_USER_HOME目录下的caches目录中),下载再使用相同版本的gradle就不用下载了
  4. 之后执行的 ./gradlew 所有命令都是使用指定的 gradle 版本·如下图所示 image.png gradle-wrapper.properties文件解读
字段名说明
distributionBase下载的Gradle压缩包解压后存储的主目录
distributionPath相对于distributionBase的解压后的Gradle压缩包的路径
zipStoreBase同distributionBase,只不过是存放zip压缩包的
zipStorePath同distributionPath,只不过是存放zip压缩包的
distributionUrlGradle发行版压缩包的下载地址

注意:前面提到的 GRALE_USER_HOME 环境变量用于这里的 Gradle Wrapper 下载的特定版本的 gradle存储目录。如果我们没有配置过 GRALE_USER_HOME 环境变量,默认在当前用户目录下的.gradle 文件夹中。

那什么时候选择使用 gradle wrapper、什么时候选择使用本地gradle?

下载别人的项目或者使用操作以前自己写的不同版本的gradle项目时:用Gradle wrapper,也即:gradlew什么时候使用本地gradle?新建一个项目时:使用gradle指令即可。

grovvy语法

在某种程度上,Groovy 可以被视为 Java 的一种脚本化改良版,Groovy 也是运行在JVM 上,它可以很好地与Java 代码及其相关库进行交互操作。它是一种成熟的面向对象编程语言,既可以面向对象编程,又可以用作纯粹的脚本语言。大多数有效的 Java 代码也可以转换为有效的 Groovy 代码,Groovy 和 Java 语言的主要区别是:完成同样的任务所需的Groovy 代码比 Java 代码更少。其特点为:

  • 功能强大,例如提供了动态类型转换、闭包和元编程(metaprogramming)支持
  • 支持函数式编程,不需要 main 函数
  • 默认导入常用的包
  • 类不支持 default 作用域,且默认作用域为 public。
  • Groovy 中基本类型也是对象,可以直接调用对象的方法。
  • 支持 DSL(Domain Specifc Languages 领域特定语言)和其它简洁的语法,让代码变得易于阅读和维护。
  • Groovy是基于 Java 语言的,所以完全兼容 Java 语法,所以对于 java程序员学习成本较低

详情了解官网The Apache Groovy programming language (groovy-lang.org)

7b88a17b868b16471c23000e7d90216.png 最新版Android studio已经支持kotlin

image.png Idea新建groovy项目

groovy语法此处暂不赘述 1.Groovy是基于Java语言的,所以完全兼容Java语法,可作为面向对象编程语言[定义类],也可作为脚本型语言[文件定义中不出现类] 作为脚本使用时

def username="zhangsang"
print(username)

编译后

public class Demo02 extends Script {
    public Demo02() {
    }

    public Demo02(Binding context) {
        super(context);
    }

    public static void main(String... args) {
        InvokerHelper.class.invoke<invokedynamic>(InvokerHelper.class, Demo02.class, args);
    }

    public Object run() {
        Object username = "zhangsang";
        return this.invoke<invokedynamic>(this, username);
    }
}

作为正常类使用时

class Actor {
    int age
}

编译后

public class Actor implements GroovyObject {
    @Generated
    public Actor() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    @Generated
    @Internal
    @Transient
    public MetaClass getMetaClass() {
        MetaClass var10000 = this.metaClass;
        if (var10000 != null) {
            return var10000;
        } else {
            this.metaClass = this.$getStaticMetaClass();
            return this.metaClass;
        }
    }

    @Generated
    @Internal
    public void setMetaClass(MetaClass var1) {
        this.metaClass = var1;
    }
}

2.在一个groovy文件中可以混合类的定义和脚本定义。此时不要再定义一个和文件同名的类。

def username = "zhangsang"
print(username)

public class Book {
    
}

3.groovy中使用def 定义变量、方法,不建议使用具体的数据类型

class Actor {
    int age

    public def getAge() {
        
    }
}

4.groovy中的注释:单行注释用 /,多行注释用: / * * /

class Actor {
    //单行注释
    int age

    /**
     * 多行注释
     */
    public def get Age() {

    }
}

类型转换:当需要时,类型之间会自动发生类型转换:字符串(Sting)、基本类型(如 int)和类型的包装类 (如 Integer)

类说明:如果在一个 groovy 文件中没有任何类定义,它将被当做 script 来处理,也就意味着这个文件将被透明的转换为一个Script 类型的类,这个自动转换得到的类将使用原始的 groovy 文件名作为类的名字。groovy 文件的内容被打包进run 方法,另外在新产生的类中被加入一个 main 方法以进行外部执行该脚本。

引号说明

def desc = "测试"
def str1 = '单引号,不支持变量引用,不支持换行操作${desc}'
def str2 = "双引号,支持变量引用,不支持换行操作${desc}"
def str3 = '''模板字符串,不支持变量引用,不支持换行
操作${desc}'''

println(str1)
println(str2)
println(str3)

println(str1.getClass().toString())
println(str2.getClass().toString())
println(str3.getClass().toString())

image.png groovy特有的数据类型:Trait():可以看成是带有方法的接口

拓展:Groovy 类与 Java 类之间的主要区别是:

  1. 没有可见性修饰符的类或方法自动是公共的(可以使用一个特殊的注释来实现包的私有可见性)·
  2. 没有可见性修饰符的字段将自动转换为属性,不需要显式的 getter 和 setter 方法。
  3. 如果属性声明为 final,则不会生成 setter。
  4. 一个源文件可能包含一个或多个类(但是如果一个文件不包含类定义的代码,则将其视为脚本)·脚本只是具有一些特殊约定的类,它们的名称与源文件相同(所以不要在脚本中包含与脚本源文件名相同的类定义)·

提示:可以把不同的基本类型添加到同一集合中! 类导入

import groovy.xml.MarkupBuilder

def sdf=new MarkupBuilder()

闭包

定义:是一个开放的、匿名的代码块,它可以接受参数、也可以有返回值。闭包可以引用其周围作用域中声明的变量。>】statements} 语法:

{
    [closureParameters->]
        statements
}

closureParameters 调用: 第一步:将闭包赋值给一个变量 第二步:变量名()、变量名.call() 闭包在实际开发中的使用:作为方法的参数使用

def calculate(Closure closure) {
    def num1 = 10
    def num2 = 2
    closure.call(num1, num2)
}

calculate { num1, num2 ->
    println("${num1+num2}")
}

groovy官网语法 The Apache Groovy programming language - Semantics (groovy-lang.org)

3. gradle进阶

3.1生命周期

Gradle 项目的生命周期分为三大阶段: Initialization ->Gonfiguration ->Execution.每个阶段都有自己的职责

Initialization 阶段主要目的是初始化构建,它又分为两个子过程,一个是执行 init script,另一个是执行 Setting Scriptinit.gradle 文件会在每个项目 build 之前被调用,用于做一些初始化的操作,它主要有如下作用:

配置内部的仓库信息(如公司的 maven 仓库信息);
配置一些全局属性
配置用户名及密码信息(如公司仓库的用户名和密码信息)

Setting Script 则更重要,它初始化了一次构建所参与的所有模块。

Configuration 阶段:这个阶段开始加载项目中所有模块的 Build script。所谓"加载”就是执行 build.gradle 中的语句根据脚本代码创建对应的 task,最终根据所有 task 生成由 Task 组成的有向无环图(Directed Acyclic Graphs),如下:

image.png 从而构成如下有向无环树:

image.png

Execution 阶段:这个阶段会根据上个阶段构建好的有向无环图,按着顺序执行 Task【Action 动作】。

3.2 settings文件

首先对 settings 文件的几点说明:

  1. 作用:主要是在项目初始化阶段确定一下引入哪些工程需要加入到项目构建中,为构建项目工程树做准备。
  2. 工程树:gradle中有工程树的概念,类似于 maven 中的 project 与 module。
classDiagram
root project --|> subproject01
root project --|> subproject02
root project --|> subproject03
class subproject01{
+subproject01
}
class subproject02{
+subproject02
}
class subproject03{
+subproject03
}
  1. 内容:里面主要定义了当前 gradle项目及子 project 的项目名称

  2. 位置:必须放在根工程目录下。

  3. 名字:为 settings.gradle 文件,不能发生变化

  4. 对应实例:与 org.gradle,api.initialization.Settings 实例是一一对应的关系。每个项目只有一个 setings 文件。

    image.png

    image.png

  5. 关注:作为开发者我们只需要关注该文件中的 include方法即可。使用相对路径(相对于根工程)【:】引入子工程。

  6. 一个子工程只有在 setting 文件中配置了才会被 gradle识别,这样在构建的时候才会被包含进去。

项目名称中":”代表项目的分隔符,类似路径中的"/".如果以":"开头则表示相对于 root proiect 。然后 Gradle 会为每个带有 build.gradle 脚本文件的工程构建一个与之对应的 Project 对象。

3.3 Task

项目实质上是 Task 对象的集合。一个 Task 表示一个逻辑上较为独立的执行过程,比如编译 Java 源代码,拷贝文件, 打包Jar 文件,甚至可以是执行一个系统命令。另外,一个 Task 可以读取和设置 Project 的 Property 以完成特定的操作。

image.png

参考官方文档:Using Tasks (gradle.org)

为任务指定组group('abc')

image.png

3.3.1任务入门

task task1{
    //任务的配置段:在配置阶段执行
    println"这是最简单的任务"
    //任务的行为:在执行阶段执行
    doFirst {
        println "task1 doFirst"
    }
    doLast {
        println "task1 doLast"
    }
}

在文件所在的目录执行命令: gradle -i task1

  • 提示1:task 的配置段是在配置阶段完成
  • 提示2:task 的 doFirst、doLast,方法是执行阶段完成,并目 doFirst .在 doLast,执行之前执行。
  • 提示 3:区分任务的配置段和任务的行为,任务的配置段在配置阶段执行,任务的行为在执行阶段执行

3.3.2任务的行为

doFirst、doLast,两个方法可以在任务内部定义,也可以在任务外部定义

def map = new HashMap<String, Object>()
map.put("action", { println "task one..." })

task(map, "task1") {
    //任务的配置段:在配置阶段执行
    println "这是最简单的任务"
    //任务的行为:在执行阶段执行
    doFirst {
        println "task1 doFirst"
    }
    doLast {
        println "task1 doLast"
    }
}

task1.doFirst {
    println "task1 doFirst out"
}

task1.doLast {
    println "task1 doLast out"
}

测试:gradle -i task1

输出结果:

task1 doFirst out
task1 doFirst
task one...
task1 doLast
task1 doLast out

底层原理分析:无论是定义任务自身的 action,还是添加的 doLast、doFirst 方法,其实底层都被放入到一个 Action 的 List中了,最初这个 action List 是空的,当我们设置了 action【任务自身的行为】,它先将action 添加到列表中,此时列表中只有一个action,后续执行 doFirst的时候 doFirst在 action 前面添加,执行 doLast 的时候 doLast 在 action后面添加。doFirs永远添加在 actions List 的第一位,保证添加的 Action 在现有的 action List 元素的最前面;doLast 永远都是在 action Lis末尾添加,保证其添加的 Action 在现有的 action List 元素的最后面。一个往前面添加,一个往后面添加,最后这个actionList 就按顺序形成了 doFirst、doSelf、doLast 三部分的 Actions,就达到 doFirst、doSelf、doLast 三部分的 Actions 顺序执行的目的。

提示 1:其中<<代表 doLast,在 gradle5.x版本之后就废弃,不能使用了,如下所示:

task hello <<{
    println 'Hello world!'
}

3.3.3任务的依赖方式

gradle C

参数方式依赖

task A {
    doLast {
        println "TaskA"
    }
}

task B {
    doLast {
        println "TaskB"
    }
}

//参数依赖:dependOn后面用冒号
task 'C'(dependsOn: ['A', 'B']) {
    doLast {
        println 'taskC'
    }
}

内部依赖

task 'C'() {
    dependsOn = ['A', 'B']
    doLast {
        println 'taskC'
    }
}

外部依赖

//引号可加可不加
C.dependsOn=[A,'B']

当然:task也支持跨项目依赖 在subproject01工程的build.gradle文件中定义

task A {
    doLast {
        println 'Task01A'
    }
}

在subproject02工程的build.gradle文件中定义

task B {
    dependsOn(":subject01:A")
    doLast {
        println 'Task02B'
    }
}

拓展 1:当一个 Task 依赖多个 Task 的时候,被依赖的 Task 之间如果没有依赖关系,那么它们的执行顺序是随机的,并无影响。

拓展 2:重复依赖的任务只会执行一次,比如:

A→B、C

B→C

任务A依赖任务B和任务C、任务B依赖C任务。执行任务A的时候,显然任务C被重复依赖了,C只会执行一次。

3.3.4任务执行

gradle任务执行语法:[taskName...] [--option-name...].

分类解释
常见的任务(*)gradle build: 构建项目:编译、测试、打包等操作
gradle run:运行一个服务,需要 application插件支持,并且指定了主启动类才能运行
gradle clean: 清理当前项目的 build 目录(不会作用于子project)
gradle init:初始化 gradle 项目使用
gradle wrapper:生成 wrqpper 文件夹的。
gradle wrapper 升级wrapper 版本号:gradle wrapper -gradle-version=4.4
gradle wrapper -gradle-version 5.2.1 -distribution-type all :关联源码用
项目报告相关任务gradle projects:列出所选项目及子项目列表,以层次结构的形式显示
gradle tasks:列出所选项目【当前 project,不包含父、子】的已分配给任务组的那些任务
gradle tasks --all :列出所选项目的所有任务。
gradle tasks --group="build setup":列出所选项目中指定分组中的任务。
gradle help --task someTask :显示某个任务的详细信息
gradle dependencies :查看整个项目的依赖信息,以依赖树的方式显示(jar包冲突默认高版本
gradle properties 列出所选项目的属性列表
调试相关选项-h,--help:查看帮助信息
-v,--version:打印 Gradle、Groovy、 Ant、JVM 和操作系统版本信息。
-S,--full-stacktace:打印出所有异常的完整(非常详细)堆栈跟踪信息。
-s,--stacktacs:打印出用户异常的堆栈跟踪(例如编译错误)
-Dorg.gradle,daemon.debug=true: 调试 Gradle 守护进程,
-Dorg.gradle,debug=true:调试 Gradle 客户端(非 daemon)进程,
-Dorg.gradle.debug.port=(port number):指定启用调试时要侦听的端口号。默认值为 5005.
性能选项:【备注:在gradle.properties 中指定这些选项中的许多选项,因此不需费命令行标志】--build--cache,-no-build-cache:尝试重用先前版本的输出。默认关闭(off)。
--max-workers:设置 Gradle 可以使用的 woker 数。默认值是处理器数。
-parallel,-no-parallel:并行执行项目。有关此选项的限制,请参阅并行项目执行。默认设置为关闭(off)
守护进程选项--daemon,--no-daemon:使用 Gradle 守护进程运行构建。默认是 on
-foreground:在前台进程中启动 Gradle 守护进程。
-Dorg.gradle.daemon.idletimeout=(number of milliseconds):
Gradle Daemon 将在这个空闲时间的毫秒数之后停止自己。默认值为 10s00000($ 小时)。
日志选项-Dorg.gradle.logging.level=(quiet,warn,lifecycle,info,debug):
通过 Gradle 属性设置日志记录级别。-q.-quiet: 只能记录错误信息!
-w,-warn:设置日志级别为 warn
-i,-info:将日志级别设置为 info
-d,-debug:登录调试模式(包括正常的堆栈跟踪)
其他(*)-x:-x等价于:-exclude-task: 常见 gradle-x tes clean build
--return-tasks: 强制执行任务,忽略 up-to-date ,常见 gradle build -rerun-tasks
--continue:忽略前面失败的任务,继续执行,而不是在遇到第一个失败时立即停止执行。每个遇到的故障都将在构建结束时报告,常见:gradle build-continue
gradle init -type pom :将 maven 项目转换为 gradle.项目(根目录执行
gradle[taskName]:执行自定义任务

参考官方文档 Command-Line Interface Reference (gradle.org)

拓展:gradle任务名是缩写:任务名支持驼峰式命名风格的任务名缩写,如:connectTask 简写为:cT,执行任务 gradle cT。

拓展 1:前面提到的 Gradle,指令本质:一个个的 task[任务],Gradle中所有操作都是基于任务完成的。

拓展 2:gradle 默认各指令之间相互的依赖关系:

classDiagram
build --|> check
build --|> assemble
assemble --|> jar
uploadArchives --|> jar
check --|> test
test --|> testClasses
testClasses --|> compileTestJava
testClasses --|> processTestResources
javadoc --|> classes
compileTestJava --|> classes
test --|> classes
jar --|> classes
classes --|> compileJava
classes --|> processResources
class check{
    可以对代码质量进行检查
}
class assemble{
    针对src/main目录操作
    startSctipts生成启动脚本
    将当前项目打成war,tar,zip包
}
class test{
    执行测试方法,生成测试报告
}
class testClasses{
    整合src/test目录
}
class uploadArchives{
    uploadArchives
}
class jar{
    打成jar包
}
class javadoc{
    javadoc
}
class compileTestJava{
    编译src/test/java
}
class processTestResources{
    编译src/test/resources
}
class classes{
    针对src/main目录下的java和resources目录
}
class compileJava{
    仅仅负责编译src/main/java目录下的java代码
}
class processResources{
    仅仅负责编译src/main/resources目录下的配置文件
}
class clean{
    clean
}

3.3.5任务定义方式

任务定义方式,总体分为两大类:一种是通过 Project 中的 task()方法,另一种是通过tasks对象的 create 或者 register 方法

tasks.register("A"){
    //register执行的是延迟创建,也即只有task被需要使用的时候才会被创建
    println("A")
}
tasks.create("B"){
    println("B")
}

当然:我们也可以在定义任务的同时指定任务的属性,具体属性有:

配置项描述默认值
type基于一个存在的Task 来创建,和我们类继承差不多DefaultTask
overwrite是否替换存在的 Task,这个和type 配合起来用false
dependOn用于配置任务的依赖[]
action添加到任务中的一个 Action 或者一个闭包null
description用于配置任务的描述null
group用于配置任务的分组null

在定义任务时也可以给任务分配属性:定义任务的时候可以直接指定任务属性,也可以给已有的任务动态分配属性:

def groupName = "FaceWallHan"
task(group: groupName, description: "this", "F") {

}

task("H") {
    group(groupName)
    description("this")
}
task y {

}
y.group = groupName

//给已有的clean任务重新指定组信息
clean.group(groupName)

3.3.6任务类型

前面我们定义的 task 都是 DefaultTask 类型的,如果要完成某些具体的操作完全需要我们自己去编写 grade 脚本,势必有些麻烦,那有没有一些现成的任务类型可以使用呢?有的,Gradle 官网给出了一些现成的任务类型帮助我们快速完成想要的任务,我们只需要在创建任务的时候,指定当前任务的类型即可,然后即可使用这种类型中的属性和 API方法了

常见任务类型该类型任务的使用
Delete删除文件或目录
Copy将文件复制到目标目录中。此任务还可以在复制时重命名和筛选文件。
CreateStartScripts创建启动脚本
Exec执行命令行进程
GenerateMavenPom生成 Maven 模块描述符(POM)文件
GradleBuild执行 Gradle 构建
Jar组装 JAR 归档文件
JavaCompile编译 Java 源文件
Javadoc为Java 类生成 HTML API 文档
PublishToMavenRepository将MavenPublication 发布到mavenartifactrepostal.
Tar组装 TAR 存档文件
Test执行 Jnit (3.s.x、4.x或 5.x)或 TestNG 测试。
Upload将 Confguration 的构件上传到一组存储库。
War组装 WAR 档案。
Zip组装 ZIP 归档文件。默认是压缩 ZIP 的内容。

提示 1:如果想看更详细的 gadle 自带 Task 类型,请参考官方文档:Gradle DSL Version 8.5

提示 2:官方文档在给出这些任务类型的时候,同时给出了案例代码,可以点进去上述官网地址中的某个类型中观看

当然除了 gradle自带的 task 类型,我们也可以自定义 task 类型,如下所示:

class CustomTask extends DefaultTask {
    //@TaskAction表示自身要执行的方法
    @TaskAction
    def doSelf() {
        println 'task in doSelf'
    }
}

def myTask = task MyDefinitionTask(type: CustomTask)
myTask.doFirst {
    println("task doFirst")
}

myTask.doLast {
    println("task doLast")
}

3.3.7任务的执行顺序

在 Gradle 中,有三种方式可以指定 Task 执行顺序:

  1. dependsOn 强依赖方式
  2. 通过 Task 输入输出
  3. 通过 API 指定执行顺序

3.3.8动态分配任务

详细请参考官网 Task - Gradle DSL Version 8.5

gradle的强大功能不仅仅用于定义任务的功能。例如,可以使用它在循环中注册同一类型的多个任务

一旦注册了任务,就可以通过 API访问它们。例如,您可以使用它在运行时动态地向任务添加依赖项。Ant 不允许这样的事情发生。

4.times { counter ->
    tasks.register("task${counter}") {
        doLast {
            println "I'm task number $counter"
        }
    }
}

tasks.named('task0') {
    dependsOn('task2', 'task3')
}

3.3.9任务的关闭与开启

每个任务都有一个 enabled 默认为的标志 true。将其设置为 false阻止执行任何任务动作,禁用的任务将标记为“跳过"。

task2.enabled=false

3.3.10任务的超时

每个任务都有一个 timeout 可用于限制其执行时间的属性。当任务达到超时时,其任务执行线程将被中断。该任务将被标记为失败。终结器任务仍将运行。如果**-continue** 使用,其他任务可以在此之后继续运行。不响应中断的任务无法超时。Gradle的所有内置任务均会及时响应超时

task3.timeout= Duration.ofMillis(500)
task a {
    doLast {
        Thread.sleep(1000)
        println "a execute"
    }
    timeout = Duration.ofMillis(500)
}
task b {
    doLast {
        println "b execute"
    }
}

在控制台使用:gradle a b测试会发现执行a的时候,由于a执行超时,抛出异常,所以没有继续往下执行【b 也没执行】。 然后在控制台使用: gradle a b-continue测试会发现a虽然执行失败,但是b还是执行了。

3.3.11任务的查找

task facewall {
    doLast {
        println "让天下没有难学的技术:尚硅谷"
    }
}
//根据任务名查找
tasks.findByName("facewall").doFirst { println "1:北京...." }
tasks.getByName("facewall").doFirst { println "2:深圳...." }
//根据任务路径查找【相对路径】
tasks.findByPath(":subject01:facewall").doFirst({ println "3:上海...." })
tasks.getByPath(":subject01:facewall").doFirst({ println "4:武汉...." })

3.3.12任务的规则

当我们执行、依赖一个不存在的任务时,Gradle.会执行失败,报错误信息。那我们能否对其进行改进,当执行一个不存在的任务时,不是报错而是打印提示信息呢?

task hello {
    doLast {
        println 'hello'
    }
}

tasks.addRule("facewall") { String taskName ->
    task(taskName) {
        println 'task not found.......................'
    }
}

测试:使用gradle abc hello进行测试,此时当abc,任务不存在时,也不会报异常【不中断执行】而是提示自定义的规则信息,继续执行hello 任务。此外,它还可以根据不同的规则动态创建需要的任务等情况。

3.3.13任务的onlyif断言

断言就是一个条件表达式。Task 有一个 onlyIf方法。它接受一个闭包作为参数,如果该闭包返回 tmue 则该任务执行否则跳过。这有很多用途,比如控制程序哪些情况下打什么包,什么时候执行单元测试,什么情况下执行单元测试的时候不执行网络测试等。具体案例如下所示:

tasks.register('hello') {
    doLast {
        println 'hello'
    }
}
hello.onlyIf {
    !project.hasProperty('world')
}

测试:通过 -P 为 Project 添加 world 属性 gradle hello -Pworld

3.3.14默认任务

Gradle 允许您定义一个或多个在没有指定其他任务时执行的默认任务。

defaultTasks 'myClean', 'myRun'
tasks.register('myClean') {
    println 'myClean'
}

tasks.register('myRun') {
    println 'myRun'
}

tasks.register('other') {
    println 'other'
}

3.4 gradle 中的文件操作

几种常见的文件操作方式

  • 本地文件
  • 文件集合
  • 文件树
  • 文件拷贝
  • 归档文件

各种文件操作类型的详细介绍如下所示:

3.4.1本地操作

使用 Project.file(java.long.Object)方法,通过指定文件的相对路径或绝对路径来对文件的操作,其中相对路径为相对当前project (根 project 或者子project) 的目录,其实使用 Project.file(java.lang.Object)方法创建的 File 对象就是 Java 中的 File对象,我们可以使用它就像在 Java中使用一样。示例代码如下:

//使用本地文件的操作方式:相对路径
def configFile = file('src/conf.xml')
configFile.createNewFile()
//使用本地文件的操作方式:绝对路径
configFile = file('D:\conf.xml')
configFile.createNewFile()
//使用本地文件的操作方式:new File
configFile = new File('faceWall.xml')
configFile.createNewFile()

3.4.2本地文件

文件集合就是一组文件的列表,在 Gradle中,文件集合用 FileCollection 接口表示。我们可以使用Project.files(java.lang.Object[]1)方法来获得一个文件集合对象,如下代码创建一个 FileCollection 实例:

def files = files('src/test1.xml', ['src/test2.xml', 'src/test3.xml'])
files.forEach {
    it.createNewFile()
    println it
}
for (final def f in (files as Set)) {
    println "set___${f}"
}

对于文件集合我们可以遍历它:也可以把它转换成java 类型:同时还能使用+来添加一个集合,或使用-来删除集合。

3.4.3文件树

文件树是有层级结构的文件集合,一个文件树它可以代表一个目录结构或ZIP 压缩包中的内容结构。文件树是从文件集合继承过来的,所以文件树具有文件集合所有的功能。 我们可以使用 Project.fileTree(java.util.Map)方法来创建文件树对象还可以使用过滤条件来包含或排除相关文件。示例代码如下:

def tree = fileTree('src/main')
tree.exclude('**/*.java')
tree.forEach {
    println(it)
}

3.4.4文件拷贝

我们可以使用 Copy 任务来拷贝文件,通过它可以过滤指定拷贝内容,还能对文件进行重命名操作等。Copy 任务必须指定一组需要拷贝的文件和拷贝到的目录,这里使用CopySpec.from(java.lang.Object[])方法指定原文件;使用CopySpec.into(java.lang.Object)方法指定目标目录。示例代码如下

tasks.register('copyTask', Copy) {
    //不存在的话会直接忽略
    //拷贝src/main/webapp目录下所有的文件
    from 'src/main/webapp'
    include '**/*.java'
    rename { String fileName ->
        fileName.replace('aa', 'faceWall')
    }
    //拷贝单独的一个文件
    from 'src/staging/index.html'
    //从Zip压缩文件中拷贝内容
    from zipTree('src/main/assets.zip')
    //拷贝到的目标目录
    into 'build/explodedWar'
}

3.4.5归档文件

通常一个项目会有很多的 Jar 包,我们希望把项目打包成一个 WAR,ZIP 或 TAR 包进行发布,这时我们就可以使用Zip,Tar,Jar,War 和 Ear 任务来实现,不过它们的用法都一样,所以在这里我只介绍 Zip, 任务的示例。首先,创建一个 Zip压缩文件,并指定压缩文件名称,如下代码所示:

tasks.register('myZip', Zip) {
    from 'src/main'
    //保存到build目录中
    into 'build'
//    baseName = 'myGame'
}

在这里,我们介绍了 Gradle 对本地文件、文件集合、文件树、文件拷贝和归档文件的操作方式。更详细的请参考官方 文档:Working With Files (gradle.org)

3.5 Dependencies

3.5.1依赖的方式

Gradle中的依赖分别为直接依赖项目依赖本地jar 依赖

方式原理实现
本地依赖依赖本地的某个jar包,具体可通过文件集合、文件树的方式指定//文件集合
implementation files('lib/retrofit.jar', 'lib/glide.jar')
//文件树
implementation fileTree('dir': 'lib', includes: ['*.jar'], excludes: [''])
项目依赖依赖某个project//被依赖的模块名一定要出现在settings.gradle文件中
implementation project(':subject01')
直接依赖依赖的类型'依赖的组名:依赖的名称:依赖的版本号'implementation 'com.github.bumptech.glide:glide:4.11.0'
//完整版
implementation group: 'com.github.bumptech.glide', name: 'glide', version: '4.11.0'

3.5.2 依赖的下载

当执行 build 命令时,gradle 就会去配置的依赖仓库中下载对应的 Jar,并应用到项目中。

3.5.2 依赖的类型

类似于 Maven 的 scope 标签,gradle 也提供了依赖的类型,具体如下所示:

compileOnly由java插件提供,曾短暂的叫provided,后续版本已经改成了compileOnly,适用于编译期需要而不需要打包的情况
runtimeOnly由 java 插件提供,只在运行期有效,编译时不需要,比如 mysql 驱动包。,取代老版本中被移除的 runtime
implementation由 java 插件提供,针对源码[src/main 目录],在编译、运行时都有效,取代老版本中被移除的 compile
testCompileOnly由java插件提供,用于编译测试的依赖项,运行时不需要
testRuntimeOnly由java插件提供,只在测试运行时需要,而不是在测试编译时需要,取代老版本中被移除的 testRuntime
testImplementation由java 插件提供,针对测试代码[src/test 目录]取代老版本中被移除的 testCompile
providedCompilewar 插件提供支持,编译、测试阶段代码需要依赖此类jar 包,而运行阶段容器已经提供了相应的支持,所以无需将这些文件打入到 war 包中了;例如 servlet-api.jar、jsp-api.jar
compile编译范围依赖在所有的 classpath,中可用,同时它们也会被打包。在 gradle7.0 已经移除
run timeruntime 依赖在运行和测试系统的时候需要,在编译的时候不需要,比如 mysgl驱动包。在gradle7.0 已经移除
apijava-library 插件提供支持,这些依赖项可以传递性地导出给使用者,用于编译时和运行时。取代老版本中被移除的 compile
compileOnlyApijava-library 插件提供支持,在声明模块和使用者在编译时需要的依赖项,但在运行时不需要。

官方文档参考:

The Java Library Plugin (gradle.org) 各个依赖范围的关系和说明

Upgrading your build from Gradle 6.x to 7.0 依赖范围升级和移除

The Java Library Plugin (gradle.org) API和implementation区别

The Java Plugin (gradle.org) 执行java 命令时都使用了哪些依赖范围的依赖。

3.5.4 api与implementation 区别

apiimplementation
编译时能进行依赖传递,底层变,全部都要变、编译速度慢不能进行依赖传递.底层变,不用全部都要变、编译速度快
运行时运行时会加载,所有模块的class都要被加载运行时会加载,所有模块的class都要被加载
应用场景适用于多模块依赖,避免重复依赖模块多数情况下使用implementation
如下所示
classDiagram
project_X --|> libA
project_X --|> libB
libA --|> libC
libB --|> libD

class libA{
    api
}
class libB{
implementation
}
class libC{
    api
}
class libD{
implementation
}

编译时:如果libc 的内容发生变化,由于使用的是 api依赖,依赖会传递,所以 libC、libA、projectX 都要发生变化,都需要重新编译,速度慢,运行时:libC、libA、projectx,中的 class 都要被加载。

编译时:如果 libD 的内容发生变化,由于使用的是 implemetation,依赖,依赖不会传递,只有 libD、libB 要变化并重新编译,速度快,运行时:libC、libA、projectx 中的 class 都要被加载。 拓展 3:api和implementation 案例分析

api 的适用场景是多 module 依赖,mpdueA 工程依赖了 module B,同时 module B 又需要依赖了 module C,modelA 工程也需要去依赖 moduie c,这个时候避免重复依赖 module,可以使用 module B api 依赖的方式去依赖 moduie C.modelA 工程只需要依赖 moduleB 即可。

有ABCD四个模块:

  • A implemetation B,B implemetation C,则A不能使用C.
  • A implemetation B,B api C,则A可以使用C。
  • A implemetation B,B implemetation C,C api D,则B可以使用D,但A不能使用D。
  • A implemetation B,B api C,C api D,这样A可以使用D
  • 不管ABCD在何处被添加到类路径都一样,在运行时这些模块中的class都是要被加载的。

总之,除非涉及到多模块依赖,为了避免重复依赖,咱们会使用 api,其它情况我们优先选择 implementation,拥有大量的 api.依赖项会显著增加构建时间。

3.5.5 依赖冲突及解决方案

依赖冲突是指 “在编译过程中,如果存在某个依赖的多个版本,构建系统应该选择哪个进行构建的问题",如下所示:

classDiagram
Animal --|> B
Animal --|> C
B--|> log4j_1_4_2
C--|> log4j2_2_4

class B{
b
}
class C{
c
}
class log4j_1_4_2{
log4j1.4.2
}
class log4j2_2_4{
log4j2.2.4
}

A、B、C 都是本地子项目module,log4j 是远程依赖。

编译时:B用 1.+.2 版本的 log4j,C 用 2.2.+ 版本的 log4i,B 和 C 之间没有冲突
打包时:只能有一个版本的代码最终打包进最终的A对应的jar | war包,对于 Gradle 来说这里就有冲突了

默认下,Gradle 会使用最新版本的jar 包【考虑到新版本的jr 包一般都是向下兼容的】,实际开发中,还是建议使用官方自带的这种解决方案。当然除此之外,Gradle 也为我们提供了一系列的解决依赖冲突的方法: exclude移除一个依赖,不允许依赖传递,强制使用某个版本。

exclude排除某个依赖

implementation('com.github.bumptech.glide:glide:4.11.0') {
    //依赖的组名
    exclude group: 'com.github.bumptech.glide'
}

不允许依赖传递

implementation('com.github.bumptech.glide:glide:4.11.0') {
    //依赖的名称
    exclude module: 'disklrucache'
}
implementation('com.github.bumptech.glide:glide:4.11.0') {
    //不建议使用这种方式
    transitive(false)
}

强制使用某个版本

//1.
implementation 'com.github.bumptech.glide:glide:4.11.0!!'
//2.
implementation('com.github.bumptech.glide:glide:4.11.0') {
    version {
        strictly("4.11.0")
    }
}

拓展:我们可以先查看当前项目中到底有哪些依赖冲突:

//下面配置,当gradle构建遇到依赖冲突时,就立即构建失败
configurations.all { Configuration configuration ->
    //遇到依赖冲突时,就立即构建失败
    configuration.resolutionStrategy.failOnVersionConflict()
}
//都代表最新版本
//动态版本说明,不建议这么用
implementation 'com.github.bumptech.glide:+'
implementation 'com.github.bumptech.glide:latest.integration'

3.6 gradle插件

3.6.1 使用插件的原因

简单的说,通过应用插件我们可以:

1.促进代码重用、减少功能类似代码编写、提升工作效率

2.促进项目更高程度的模块化、自动化、便捷化

3.可插拔式的的扩展项目的功能

3.6.2 插件的作用

在项目构建过程中做很多事情,把插件应用到项目中,通常可以完成:

  1. 可以添加任务【task】到项目中,从而帮助完成测试、编译、打包等
  2. 可以添加依赖配置到项目中。
  3. 可以向项目中拓展新的扩展属性、方法等。
  4. 可以对项目进行一些约定,如应用 Jva 插件后,约定 src/main/java 目录是我们的源代码存在位置,编译时编译这个目录下的 Java 源代码文件。

3.6.3 插件的分类和使用

classDiagram
gradle插件 --|> 脚本插件
gradle插件 --|> 二进制插件
二进制插件 --|> 内部插件
二进制插件 --|> 第三方插件
二进制插件 --|> 自定义插件
class 脚本插件{
脚本插件
}
class 二进制插件{
 (对象插件)
}
class 内部插件{
 内部插件
}
class 第三方插件{
 第三方插件
}
class 自定义插件{
 自定义插件
}

第一种:脚本插件

脚本插件的本质就是一个脚本文件,使用脚本插件时通过 apply from:将脚本加载进来就可以了,后面的脚本文件可以是本地的也可以是网络上的脚本文件,下面定义一段脚本,我们在 build.gradle 文件中使用它,具体如下:

Project与Task,Gradle插件

//version.gradle
ext {
    company = 'google'
    cfgs = [
            compileSdkVersion: JavaVersion.VERSION_1_8
    ]
    spring = [
            version: '5.0.0'
    ]
}

下面将将在构建文件中使用这个脚本文件,具体如下:

//build.gradle文件
apply from: "version.gradle"
tasks.register('taskVersion') {
    doLast {
        println "公司名称为:${company},JDK版本是${cfgs.compileSdkVersion},版本号是${spring.version}"
    }
}

意义:脚本文件模块化的基础,可按功能把我们的脚本进行拆分一个个公用、职责分明的文件,然后在主脚本文件引用.比如:将很多共有的库版本号一起管理、应用构建版本一起管理等。

第二种:对象插件之内部插件[核心插件]

二进制插件[对象插件]就是实现了 org.gradle.api.Plugin 接口的插件,每个 Java Gradle 插件都有一个 plugin id。

classDiagram
二进制插件 --|> 内部插件
二进制插件 --|> 第三方插件
二进制插件 --|> 自定义插件
内部插件 --|> apply方式
内部插件 --|> pluginsDSL1
apply方式 --|> apply_map具名参数
apply方式 --|> app_闭包
传统使用方式 --|> 1先引入依赖
传统使用方式 --|> 2通过apply应用
第三方插件 --|> 传统使用方式
第三方插件 --|> pluginsDSL2
自定义插件 --|> 构建过程中默认执行
class 二进制插件{
 (对象插件)
}
class 内部插件{
 内部插件
}
class 第三方插件{
 第三方插件
}
class 自定义插件{
 自定义插件
}
class 构建过程中默认执行{
 构建过程中默认执行
}
class apply方式{
 apply方式
}
class pluginsDSL1{
 pluginsDSL1
}
class apply_map具名参数{
 apply(map具名参数)
}
class app_闭包{
 apply(闭包)
}
class 传统使用方式{
 传统使用方式
}
class 1先引入依赖{
 1先引入依赖
}
class 2通过apply应用{
 2通过apply应用
}
class pluginsDSL2{
 pluginsDSL2
}

pluginsDSL

可通过如下方式使用一个 Java 插件:

apply plugin :'java'//map具名参数方式

gradle提供的二进制插件【核心插件】:Gradle Plugin Reference

或者:

通过上述代码就将 Java 插件应用到我们的项目中了,对于 Gradle 自带的核心插件都有唯一的 plugin id,其中 java 是Java 插件的 plugin id,这个 plugin id 必须是唯一的,可使用应用包名来保证 plugin id 的唯一性。这里的 java 对应的具体类型是 org.gradle.api.plugins.JavaPlugin,所以可以使用如下方式使用 Java 插件:

plugins {
    id 'java'
    id 'application'
}

apply(map具名参数)

apply: map具名参数方式
key: plugin(固定key值) value:插件id、插件的全类名、插件的简类名(前提:包已经默认导入过)

//插件ID
apply plugins: 'java'
//插件的全类名
apply plugins: 'org.gradle.api.plugins.JavaPlugin'
//插件的简类名
apply plugins: 'JavaPlugin'

apply(闭包)

//也可以使用闭包作为project.apply方法的一个参数
apply {
    plugins: 'java'
}

第二种:对象插件之第三方插件 如果是使用第三方发布的二进制插件,一般需要配置对应的仓库和类路径

//使用传统的应用方式
buildscript {
    ext {
        springBootVersion = "2.3.3.RELEASE"
    }
    repositories {
        mavenLocal()
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
        jcenter()
    }
    // 此处先引入插件
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
//再应用插件
apply plugin: 'org.springframework.boot' //社区插件,需要事先引入,不必写版本号

但是如果是第三方插件已经被托管在 plugins.gradle.org/ 网站上,就可以不用在 buildscript 里配置 classpath 依赖了,直接使用新出的 plugins DSL 的方式引用,案例如下:

使 用 plugins DSL 方 式

plugins {
    id 'org.springframework.boot' version '2.4.1'
}

注意

  1. 如果使用老式插件方式buildscript{}要放在build.gradle 文件的最前面,而新式plugins{}没有该限制。
  2. 托管在网站gradle 插件官网的第三方插件有两种使用方式,一是传统的buildscript 方式,一种是 plugins DSL 方式 。

第二种:对象插件之用户自定义插件

interface GreetingPluginExtension {
    Property<String> getMessage()

    Property<String> getGreeter()
}

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        project.task('hello') {
            doLast {
                println "${extension.message.get()} from ${extension.greeter.get()}"
            }
        }
    }
}

apply plugin: GreetingPlugin

// Configure the extension using a DSL block 
greeting {
    message = 'Hi'
    greeter = 'Gradle'
}

参考地址:docs.gradle.org/current/use…

我们直接执行 hello 任务./gradle hello 即可,这种方式实现的插件我们一般不使用,因为这种方式局限性太强,只能本

Project,而其他的Project 不能使用。

3.6.4 buildSrc项目

buildSrc 是Gradle 默认的插件目录,编译 Gradle 的时候会自动识别这个目录,将其中的代码编译为插件。

  1. 首先先建立一个名为 buildSrc 的 java Module,将 buildSrc 从 included modules 移除,重新构建,然后只保留 build.gradle和src/main 目录,其他全部删掉,注意名字一定是 buildSrc,不然会找不到插件。
  2. 然后修改Gradle 中的内容
apply plugin: 'groovy' // 必 须
apply plugin: 'maven-publish'

dependencies {
    implementation gradleApi() //必须
    implementation localGroovy() //必须
}

repositories {
    google()
    jcenter()
    mavenCentral() //必须
}

//把项目入口设置为src/main/groovy
sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }
    }
}
  1. 创建入口目录,在 src/main 下创建代码入口目录,如下:image.png
  2. 然后实现插件代码Text.groovy,注意文件后缀为groovy,文件要引入package com.facewall
package com.facewall


import org.gradle.api.Plugin
import org.gradle.api.Project

class Text implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.register("face") {
            doLast {
                println('自定义插件')
            }
        }
    }
}
 
  1. 接下来在 main 目录下创建 resources 目录,在 resources 目录下创建 META-INF 目录,在 META-INF 目录下创建gradle-plugins 目录,在gradle-plugins 目录下创建properties 文件image.png
  2. properties 文件可以自己命名,但是要以 .properties 结尾,比如com.facewall.plugin.properties,其 com.facewall.plugin 就是定义的包名路径.
  3. 最后需要在properties 文件中指明我们实现插件的全类名implementation-class=com.facewall.Text image.png

到目前为止我们的插件项目已经写完了,在 module 引入我们写的插件apply plugin:'com.facewall.plugin',然后执行插件的Task./gradle face

这种形式的写法,在我们整个工程的module 都可以使用,但也只是限制在本工程,其他工程不能使用。

改进

第二种写插件的方式他只能在本工程中使用,而其他的项目工程不能使用,有时候我们需要一个插件在多个工程中使用, 这时候我们就需要把插件上传maven 中。

第一步: 首先将上述buildSrc 目录复制一份,修改文件夹名,然后在settings.gradle 文件中使用include 引入

第二步:修改build.gradle 文件,发布到maven 仓库中

apply plugin: 'groovy' // 必 须
apply plugin: 'maven-publish'
dependencies {
    implementation gradleApi() //必须
    implementation localGroovy() //必须
}
repositories {
    google()
    jcenter()
    mavenCentral() //必须
}
//把项目入口设置为src/main/groovy
sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }
    }
}

publishing {
    publications {
        myLibrary(MavenPublication) {
            groupId = 'com.atguigu.plugin' //指定GAV坐标信息
            artifactId = 'library'
            version = '1.1'
            from components.java//发布jar包
            //from components.web///引入war插件,发布war包
        }
    }
    repositories {
        maven {
            url "$rootDir/lib/release"
        }
        //发布项目到私服中
//        maven {
//            name = 'myRepo' //name属性可选,表示仓库名称,url必填
//            //发布地址:可以是本地仓库或者maven私服
//            //url = layout.buildDirectory.dir("repo")
//            url = 'http://my.org/repo'
//            // change URLs to point to your repos, e.g. http://my.org/repo
//            //认证信息:用户名和密码
//            credentials {
//                username = 'joe'
//                password = 'secret'
//            }
//        }
    }
}

第三步:执行publish 指令,发布到根 project 或者maven 私服仓库。

第四步:使用插件,在项目级 build.gradle 文件中将插件添加到 classpath:

buildscript {
    repositories {
        maven { url "$rootDir/lib/release" }
    }
    dependencies {
        classpath "com.han.plugin:library:1.1"
    }
}
//是在 atguiguplugin  中定义的插件 ID
apply plugin: 'com.facewall.plugin'

apply plugin: 'com.facewall.plugin'对应配置文件的文件名 image.png

第五步:执行 gradle build 指令就会在控制台看到自定义插件的输出,说明自定义插件就已经生效了。

最后,至于如何写一个插件,能帮助项目更加自动化或者便捷化,是值得大家未来需要长期思考、关注、努力的点。

3.6.5 插件的关注点

第一点: 插件的引用

apply plugin: '插件名'

第二点:主要的功能[任务]

当我们在工程中引入插件后,插件会自动的为我们的工程添加一些额外的任务来完成相应的功能。以Java 插件为例,当我们加入java 插件之后,就加入了如下功能:

classDiagram
build --|> check
build --|> assemble
assemble --|> jar
uploadArchives --|> jar
check --|> test
test --|> testClasses
testClasses --|> compileTestJava
testClasses --|> processTestResources
javadoc --|> classes
compileTestJava --|> classes
test --|> classes
jar --|> classes
classes --|> compileJava
classes --|> processResources
class check{
    可以对代码质量进行检查
}
class assemble{
    针对src/main目录操作
    startSctipts生成启动脚本
    将当前项目打成war,tar,zip包
}
class test{
    执行测试方法,生成测试报告
}
class testClasses{
    整合src/test目录
}
class uploadArchives{
    uploadArchives
}
class jar{
    打成jar包
}
class javadoc{
    javadoc
}
class compileTestJava{
    编译src/test/java
}
class processTestResources{
    编译src/test/resources
}
class classes{
    针对src/main目录下的java和resources目录
}
class compileJava{
    仅仅负责编译src/main/java目录下的java代码
}
class processResources{
    仅仅负责编译src/main/resources目录下的配置文件
}
class clean{
    clean
}

具体大家可通过gradle tasks 查看加入某个插件前后的区别。

说明:Gradle 中的任务依赖关系是很重要的,它们之间的依赖关系就形成了构建的基本流程。

第三点:工程目录结构

一些插件对工程目结构有约定,所以我们一般遵循它的约定结构来创建工程,这也是 Gradle 的“约定优于配置”原则。例如java 插件规定的项目源集目录结构如下所示:

--src
   |--main
   |   |--java
   |   |--resources
   |--test
       |--java
       |--resources

如果要使用某个插件就应该按照它约定的目录结构设置,这样能大大提高我们的效率,当然各目录结构也可以自己定义。

第四点:依赖管理

比如前面我们提到的 依赖的类型[依赖管理]部分,不同的插件提供了不同的依赖管理。

第五点:常用的属性

例如:Java 插件会为工程添加一些常用的属性,我们可以直接在编译脚本中直接使用。

属性名称类型默认值描述
reportsDirNameStringreports生成报告的目录名称
reportsDirFile(只读)buildDir/reportsDirName生成报告的目录
testResultsDirNameStringtest-results生成测试result.xml文件的目录名称
testResultsDirFile(只读)reportsDir/testReportDirName生成测试报告的目录
libsDirNameStringlibs生成lib 库的目录名称
libsDirFile(只读)buildDir/libsDirName生成lib 库的目录
distsDirNameStringdistributions生成发布文件的目录名称
distsDirFile(只读)buildDir/distsDirName生成发布文件的目录
docsDirNameStringdocs生成帮助文档的目录名称
docsDirFile(只读)buildDir/docsDirName生成帮助文档的目录
dependencyCacheDirNameStringdependency-cache存储缓存资源依赖信息的目录名称
dependencyCacheDirFile(只读)buildDir/dependencyCacheDirName存储缓存资源依赖信息的目录

当然,这里还有一些其它属性

属性名称类型默认值描述
sourceSetsSourceSetContainer (只读)Not null包含工程的资源集合(sourcesets.)
sourceCompatibilityJavaVersion,也可以使用字符串或数字,比 如 '1.5' 或 者 1.5根据使用的JVM 定编译java 文件时指定使用的java版本
targetCompatibilityJavaVersion,也可以使用字符串或数字,比 如 '1.5' 或 者 1.5sourceCompatibility生成classes 的java 版本
archivesBaseNameStringprojectName作为归档文件的默认名称,如JAR 或者ZIP 文件的名称

3.6.6 Java插件分析

参考官网:docs.gradle.org/current/use…,以Java 插件为例,讲解需要关注的几点:

第一点:我们要关注插件使用

plugins {
    id 'java'
}

第二点:我们要关注插件的功能

我们可通过官方文档介绍了解某个插件功能或者百度、再或者大家可以通过 gradle tasks 查看加入java 插件前后的区别。

第三点:项目布局

一般加入一个插件之后,插件也会提供相应的目录结构,例如:java 插件的目录结构

--src
   |--main
   |   |--java
   |   |--resources
   |--test
       |--java
       |--resources

当然这个默认的目录结构也是可以改动的例如:

sourceSets {
    main {
        java {
            srcDirs = ['src/java']
        }
        resources {
            srcDirs = ['src/resources']
        }
    }
}

第四点:依赖管理:以java 插件为例,提供了很多依赖管理项

image.png

源集依赖关系配置

image.png

第五点:额外的属性和方法:

可参考官方文档: sourceCompatibility(JavaVersion.VERSION_1_8)

3.7 build.gradle 文件

  • build.gradle 是一个gradle 的构建脚本文件,支持**java、groovy (kotlin)**等语言。
  • 每个project 都会有一个build.gradle 文件,该文件是项目构建的入口,可配置版本、插件、依赖库等信息。
  • 每个build 文件都有一个对应的 Project 实例,对build.gradle 文件配置,本质就是设置Project 实例的属性和方法。
  • 由于每个 project 都会有一个build 文件,那么Root Project 也不列外.Root Project 可以获取到所有 Child Project,所以在Root Project 的 build 文件中我们可以对Child Project 统一配置,比如应用的插件、依赖的maven 中心仓库等。
  • build 文件中常见的属性和方法如下所示:

3.7.1 常见属性代码

代码参考:

//指定使用什么版本的JDK语法编译源代码,跟编译环境有关,在有java插件时才能用
sourceCompatibility = 1.8
//指定生成特定于某个JDK版本的class文件:跟运行环境有关,在有java插件时才能用
targetCompatibility = 1.8
//业务编码字符集,注意这是指定源码解码的字符集[编译器]
compileJava.options.encoding "UTF-8"
//测试编码字符集,注意这是指定源码解码的字符集[编译器]
compileTestJava.options.encoding "UTF-8"
//编译JAVA文件时采用UTF-8:注意这是指定源码编码的字符集【源文件】
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
//编译JAVA文件时采用UTF-8:注意这是指定文档编码的字符集【源文件】
tasks.withType(Javadoc) {
    options.encoding = "UTF-8"
}

提示 1:group+name+version 类似于 maven 的group+artifactId+version

提示 2:encoding 解决业务代码与测试代码中文乱码问题

3.7.2 Repositories

repositories {
    //gradle中会按着仓库配置的顺序,从上往下依次去对应的仓库中找所需要的jar包:
    //如果找到,则停止向下搜索,如果找不到,继续在下面的仓库中查找
    //指定去本地某个磁盘目录中查找:使用本地file文件协议:一般不用这种方式
    maven {
        url 'file:///D:/repos/mavenrepos3.5.4'
    }
    maven {
        url "$rootDir/lib/release"
    }
    //指定去maven的本地仓库查找
    mavenLocal()
    //指定去maven的私服或者第三方镜像仓库查找
    maven {
        name "Alibaba";
        url "https://maven.aliyun.com/repository/public"
    }
    maven {
        name "Bstek";
        url "https://nexus.bsdn.org/content/groups/public/"
    }
    //指定去maven的远程仓库查找:即 https://repo.maven.apache.org/maven2/
    mavenCentral()
    //去google仓库查找google()
}

因为 Gradle 没有自己的远程仓库,而是使用 Maven、jcenter、jvy、google 这些远程仓库。

3.7.3 Subprojects 与 Allprojects

allprojects 是对所有project (包括Root Project+ child Project[当前工程和所有子工程]) 的进行统一配置,而subprojects是对所有Child Project 的进行统一配置。测试如下:

allprojects {
    tasks.create('hello') {
        doLast { task ->
                println "project name is $task.project.name"
        }
    }
}
subprojects {
    hello.doLast { task ->
        println "here is subprojects $task.project.name"
    }
}

通常在 subprojects 和allprojects 中:

allprojects() {
    //本质Project中的allprojects方法,传递一个闭包作为参数。
    apply plugin: 'java'
    ext {
        junitVersion = '4.10'
    }
    task allTask {

    }
    repositories {

    }
    dependencies {

    }
}
subprojects() {
    //同上面allprojects中的方法。
}

拓展 1: 如果是直接在根project 配置 repositories 和 dependencies 则只针对根工程有效。

拓展 2 我们也可以在对单个 Project 进行单独配置:

project('subject01') {
    task subject01 {
        doLast {
            println 'for subject01'
        }
    }
}
project('subject01') {
    apply plugin: 'java'
    dependencies {
        testImplementation 'org.junit.jupiter:junit-jupiter'
    }
}

执行gradle build 指令即可查看测试效果。

3.7.4 ext 用户自定义属性

Project 和 Task 都允许用户添加额外的自定义属性,要添加额外的属性,通过应用所属对象的ext 属性即可实现。添加之后可以通过 ext 属性对自定义属性读取和设置,如果要同时添加多个自定义属性,可以通过 ext 代码块:

//自定义一个Project的属性
ext.age = 18
//通过代码块同时自定义多个属性
ext {
    phone = 19292883833
    address = "北京尚硅谷"
}
task extCustomProperty {
//在task中自定义属性
    ext {
        desc = "奥利给"
    }
    doLast {
        println " 年 龄 是 :${age}"
        println "电话是:${phone}"
        println "地址是:${address}"
        println "尚硅谷:${desc}"
    }
}

测试:通过gradle extCustomProperty

拓展 1: ext 配置的是用户自定义属性,而gradle.properties 中一般定义 系统属性、环境变量、项目属性、JVM 相关配置信息

例如gradle.properties 文件案例:加快构建速度的,gradle.properties 文件中的属性会自动在项目运行时加载。

## 设置此参数主要是编译下载包会占用大量的内存,可能会内存溢出
org.gradle.jvmargs=-Xms4096m -Xmx8192m
## 开启gradle缓存
org.gradle.caching=true
#开启并行编译
org.gradle.parallel=true
#启用新的孵化模式
org.gradle.configureondemand=true
#开启守护进程
org.gradle.daemon=true

详细请参考:docs.gradle.org/current/use…

3.7.5 Buildscript

buildscript 里是gradle 脚本执行所需依赖,分别是对应的 maven 库和插件。案例如下

import org.apache.commons.codec.binary.Base64

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}
tasks.register('encode') {
    doLast {
        def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
        println new String(encodedString)
    }
}

需要注意的是:

  1. buildscript{}必须在 build.gradle 文件的最前端。
  2. 对于多项目构建,项目的 buildscript ()方法声明的依赖关系可用于其所有子项目的构建脚本。
  3. 构建脚本依赖可能是 Gradle 插件。

案例如下所示:

//老式apply插件的引用方式,使用apply+buildscript
buildscript {
    ext {
        springBootVersion = "2.3.3.RELEASE"
    }
    repositories {
        mavenLocal()
        maven {
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
        jcenter()
    }
//此处引入插件
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}
apply plugin: 'java' //核心插件,无需事先引入
apply plugin: 'org.springframework.boot' //社区插件,需要事先引入,才能应用,不必写版本号

3.8 publishing 项目发布

接下来,将咱们写好的模块发布发布到公司的私服以供别人使用,如下所示:

image.png

3.8.1 引入maven 发布的插件

plugins {
    id 'java-library' //如果发布war包,需要war插件,java-library支持带源码、文档发布
    id 'maven-publish'
}

3.8.2 设置发布代码

//带源码和javadoc的发布:需要'java-library'插件支持:它是java的升级版,java插件的功能java-library都有
//javadoc.options.encoding = "UTF-8"
//java {
//    withJavadocJar()
//    withSourcesJar()
//}
publishing {
    publications {
        myLibrary(MavenPublication) {
            groupId = 'org.gradle.sample' //指定GAV坐标信息
            artifactId = 'library'
            version = '1.1'
            from components.java//发布jar包
            //from components.web///引入war插件,发布war包
        }
    }
    repositories {
        //本地仓库位于USER_HOME/.m2/repository
        //mavenLocal()
        //发布项目到私服中
        maven {
            name = 'myRepo' //name属性可选,表示仓库名称,url必填
            //发布地址:可以是本地仓库或者maven私服
            //url = layout.buildDirectory.dir("repo")
            // change URLs to point to your repos, e.g. http://my.org/repo
            def releasesRepoUrl = layout.buildDirectory.dir('repos/releases')
            def snapshotsRepoUrl = layout.buildDirectory.dir('repos/snapshots')
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
            //认证信息:用户名和密码
            //credentials {
            //    username = 'joe'
            //    password = 'secret'
            //}
        }
    }
}

3.8.3 执行发布指令

执行发布命令,将项目发布到本地仓库或者远程仓库。常见的发布指令有:

  • generatePomFileForPubNamePublication: 生成 pom 文件
  • publishPubNamePublicationToRepoNameRepository:发布项目到指定仓库,如果没有仓库名,默认为maven
  • publishPubNamePublicationToMavenLocal: 将 PubName 发布复制到本地 Maven 仓库中包括POM 文件和其他元数据。

publish: 发布到 repositories 中指定的仓库(为比如 Maven 私服)

publishToMavenLocal: 执行所有发布任务中的操作发布到本地 maven 仓库【默认在用户家目录下的.m2/repository】。

3.9 生命周期中Hook

生命周期中的这些钩子函数都是由 gradle 自动回调完成的,利用这些钩子函数可以帮助我们实现一些我们想要的功能。

image.png

Gradle 在生命周期各个阶段都提供了用于回调的钩子函数:

Gradle 初始化阶段:

  • 在 settings.gradle 执行完后,会回调 Gradle 对象的 settingsEvaluated 方法
  • 在构建所有工程 build.gradle 对应的Project 对象后,也既初始化阶段完毕,会回调 Gradle 对象的projectsLoaded 方法

Gradle 配置阶段:

  • Gradle 会循环执行每个工程的 build.gradle 脚本文件
  • 在执行当前工程build.gradle 前,会回调Gradle 对象的 beforeProject 方法和当前Project 对象的 beforeEvaluate 方法, 虽然 beforeEvalute 属于 project 的生命周期, 但是此时 build script 尚未被加载, 所以 beforeEvaluate 的设置依 然要在 init script 或 setting script 中进行,不要在 build script 中使用 project.beforeEvaluate 方法。
  • 在执行当前工程 build.gradle 后,会回调 Gradle 对象的afterProject 方法和当前Project 对象的 afterEvaluate 方法
  • 在所有工程的 build.gradle 执行完毕后,会回调 Gradle 对象的 projectsEvaluated 方法
  • 在构建 Task 依赖有向无环图后,也就是配置阶段完毕,会回调TaskExecutionGraph 对象的 whenReady 方法

Gradle 执行阶段:

  • Gradle 会循环执行Task 及其依赖的 Task
  • 在当前 Task 执行之前,会回调 TaskExecutionGraph 对象的 beforeTask 方法
  • 在当前 Task 执行之后,会回调 TaskExecutionGraph 对象的 afterTask 方法当所有的 Task 执行完毕后,会回调 Gradle 对象的 buildFinish 方法。

提示: Gradle 执行脚本文件的时候会生成对应的实例,主要有如下几种对象:

  1. Gradle 对象:在项目初始化时构建,全局单例存在,只有这一个对象
  2. Project 对象:每一个build.gradle文件 都会转换成一个 Project 对象,类似于maven中的pom.xml文件
  3. Settings 对象:settings.gradle 会转变成一个 settings 对象,和整个项目是一对一的关系,一般只用到include方法
  4. Task对象: 从前面的有向无环图中,我们也可以看出,gradle最终是基于Task的,一个项目可以有一个或者多个Task

钩子函数代码演示:项目目录结构如下:

image.png

在root project 的settings.gradle 文件中添加:

task A {
    println "root taskA" doFirst() {
        println "root taskA doFirst"
    }
    doLast() {
        println "root taskA doLast"
    }
}

在subject01 的 build.gradle 文件中添加:

task B {
    println "SubProject01 taskB" doFirst() {
        println "SubProject01 taskB doFirst"
    }
    doLast() {
        println "SubProject01 taskB doLast"
    }
}

在subject02 的 build.gradle 文件中添加:

//task C 在上面
task C {
// 依赖task D
    dependsOn 'D'
    println "SubProject02 taskC"
    doFirst() {
        println "SubProject02 taskC doFirst"
    }
    doLast() {
        println "SubProject02 taskC doLast"
    }
}
//task D 在下面
task D {
    println "SubProject02 taskD"
    doFirst() {
        println "SubProject02 taskD doFirst"
    }
    doLast() {
        println "SubProject02 taskD doLast"
    }
}