Gradle 分享(20180917)

一、Gradle定义的规则

Gradle是项目自动构建工具,更是一个框架;它作为框架,定义了流程和规则.

  1. 它在构建过程中会产生许多的对象,如图1


    图1

    2.具体的构建工作是由task来完成,它提供了大量不同类型的task


    图2
    图3

    至于一个项目下来要怎么用task去实现构建,它没有具体的介绍,好在有人帮我们用这些task实现了(也就是插件),也就是说它只负责定义流程和规则(这个规则是你要写个插件那你要通过实现它的接口Plugin,如:public class BinaryRepositoryVersionPlugin implements Plugin<Project> {

    public void apply(Project project) { }
    },觉得它提供的task没有你想要的类型,你也可以自己定义,通过继承它的类DefaultTask,如:public class Generate extends DefaultTask {}),在我们用gradle去构建项目的时候没有特殊要求,我们可以不用写task,因为插件里有许多的task,已经用这些task实现了我们的构建,现在市面上已经有许多不同的插件,比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件.

3.上面提到的构建过程中产生的对象都有详细的API:官网

二、构建脚本基础

1、构建块

每个Gradle构建都包括三个基本的构建块:项目(projects)、任务(tasks)和属性(properties),每个构建至少包括一个项目,项目包括一个或者多个任务,项目和任务都有很多个属性来控制构建过程。

  • 构建块---project

在Gradle术语里项目表示你想构建的一个组件(比如一个JAR文件),或者你想完成的一个目标(比如打包app),如果你以前使用过Maven,你应该听过类似的概念。与Maven pom.xml相对应的是build.gradle文件,每个Gradle脚本至少定义了一个项目。当开始构建过程后,Gradle基于你的配置实例化org.gradle.api.Project这个类以及让这个项目通过project变量来隐式的获得。下图列出了API接口和最重要的方法。
图4

一个项目可以创建新任务、添加依赖和配置、应用插件和其他脚本,许多属性比如name和description都是可以通过getter和setter方法来访问。
当访问你项目的属性和方法时你并不需要显式的使用project变量(Gradle会假定你的意思是Project实例调用这个方法或者属性),看看下面这个例子:

//在build.gradle脚本中
//没有使用project变量来设置项目的描述
setDescription("myProject")
//使用Grovvy语法来访问名字和描述
println "Description of project $name: " + project.description
  • 构建块---Task

先了解两个概念:任务动作(actions)和任务依赖,一个动作就是任务执行的时候一个原子的工作,这可以简单到打印hello world,也可以复杂到编译源代码。很多时候一个任务需要在另一个任务之后执行,尤其是当一个任务的输入依赖于另一个任务的输出时,比如项目打包成JAR文件之前先要编译成class文件,让我们来看看Gradle API中任务的表示:org.gradle.api.Task 接口。


图5
  • 构建块---Property

每个Project和Task实例都提供了setter和getter方法来访问属性,属性可以是任务的描述或者项目的版本号,也可以是你自己要定义的属性,比如,你想定义一个变量来引用你在构建脚本中多次使用的一个文件,Gradle允许你通过外部属性来定义自己的变量。

  • 1、外部属性

外部属性一般存储在键值对中,要添加一个属性,你需要使用ext命名空间,看一个例子:

//build.gradle
ext.myProp = 'myValue'
ext {
   someOtherProp = 123
}

相似的,外部属性可以定义在一个属性文件中: 通过在/.gradle路径或者项目根目录下的 gradle.properties文件来定义属性可以直接注入到你的项目中,他们可以通过 project实例来访问,注意/.gradle目录下只能有一个 Gradle属性文件即使你有多个项目,在属性文件中定义的属性可以被所有的项目访问,假设你在你的gradle.properties文件中定义了下面的属性:

exampleProp = myValue
someOtherProp = 455

你可以在项目中访问这两个变量:

assert project.exampleProp == 'myValue'

task printGradleProperty << {
   println "Second property: $someOtherProp"
}

2、管理任务

每个新创建的任务都是 org.gradle.api.DefaultTask 类型, org.gradle.api.Task 的标准实现,DefaultTask 所有的域都是私有的,意味着他们只能通过 setter 和 getter 方法来访问,庆幸的是Groovy提供了一些语法来允许你通过变量名来使用。

  • 声明任务的动作(actions)

动作就是在你的任务中放置构建逻辑的地方,Task接口提供了两个方法来声明任务的动作: doFirst和doLast,当任务执行的时候,定义在闭包里的动作逻辑就按顺序执行。
接下来我们会写一个简单的任务printVersion,任务的作用就是打印项目的版本号,在任务 的最后一个动作定义这个逻辑。

version = '0.1-SNAPSHOT'

task printVersion {
    doLast {
        println "Version: $version"
    }
}

task printVersion <<{
      println "Version: $version"
}
  • 1、给已经存在的任务添加动作

到目前为止,你只是给printVersion这个任务添加了单个动作,要么是第一个或者最后一个,对于每个任务可以有多个动作,实际上,当任务创建的时候你可以添加任意多个动作,每一个任务都有一个动作清单,他们在运行的时候是执行的,接下来我们来修改之前的例子:

task printVersion {
//任务的初始声明可以添加first和last动作
    doFirst {
        println "Before reading the project version"
    }

    doLast {
        println "Version: $version"
    }
}
//你可以在任务的动作列表的最前面添加其他任务,比如:
printVersion.doFirst { println "First action" }

由此可知,我们可以添加额外的动作给已经存在的任务,当你想添加动作的那个任务不是你自己写的时候这会非常有用,你可以添加一些自定义的逻辑,比如你可以添加doFirst动作到compile-Java任务来检查项目是否包含至少一个source文件。

  • 2、访问任务属性

来改善一下输出版本号的方法,Gradle提供一个基于SLF4J库的日志实现,除了实现了基本的日志级别(DEBUG, ERROR, INFO, TRACE, WARN))外,还添加了额外的级别,日志实例可以通过任务的方法来直接访问,接下来,你将用QUIET级别打印项目的版本号:

task printVersion << {
    logger.quiet "Version: $version"
}

展示两个其他的属性,group和description,两个都是documentation任务的一部分,description属性简短的表示任务的目的,group表示任务的逻辑分组。

task printVersion(
    group: 'versioning',
    description: 'Prints project version.') << {
    logger.quiet "Version: $version"
}
  • 3、定义任务依赖

dependsOn方法用来声明一个任务依赖于一个或者多个任务,接下来通过一个例子来讲解运用不同的方法来应用依赖:

task first << { println "first" }
task second << { println "second" }

//声明多个依赖
task printVersion(dependsOn: [second, first]) << {
    logger.quiet "Version: $version"
}

task third << { println "third" }
//通过任务名称来声明依赖
third.dependsOn('printVersion')

你可以通过命令行调用third任务来执行这个任务依赖链:

$ gradle -q third
first
second
Version: 0.1-SNAPSHOT
third

仔细看这个执行顺序,你有没用发现printVersion声明了对second和first任务的依赖,但是first在second任务前执行了,Gradle里面任务的执行顺序并不是确定的。
任务依赖执行顺序
Gradle并不保证依赖的任务能够按顺序执行,dependsOn方法只是定义这些任务应该在这个任务之前执行,但是这些依赖的任务具体怎么执行它并不关心,如果你习惯用命令式的构建工具来定义依赖(比如ant)这可能会难以理解。在Gradle里面,执行顺序是由任务的输入输出特性决定的,这样做有很多优点,比如你想修改构建逻辑的时候你不需要去了解整个任务依赖链,另一方面,因为任务不是顺序执行的,就可以并发的执行来提高性能。

  • 4、终结者任务

可以用finalizedBy方法来结束一个指定的任务:

task first << { println "first" }
task second << { println "second" }
//声明first结束后执行second任务
first.finalizedBy second

//当我们执行任务first ,你会发现任务first结束后自动触发任务second:
$ gradle -q first
first
second
  • 添加随意的代码

在任务里没有定义动作或者没有使用左移操作符,在Gradle里称之为任务配置块(task configuration)。
在我们执行调用其他任务命令,你会发现任务配置块在执行其他任务之前就执行了,我们没有声明依赖,也没有在命令行中调用它,任务是怎么调用的。

这是因为任务配置块总是在任务动作之前执行的 ,理解这个行为的关键就是Gradle的构建生命周期,我们来看下Gradle的构建阶段:
Gradle的构建生命周期
无论你什么时候执行一个gradle build,都会经过三个不同的阶段:初始化、配置和执行。
在初始化阶段,Gradle给你的项目创建一个Project实例,你的构建脚本只定义了单个项目,在多项目构建的上下文环境中,构建的阶段更为重要。根据你正在执行的项目,Gradle找出这个项目的依赖。
下一个阶段就是配置阶段,Gradle构建一些在构建过程中需要的一些模型数据,当你的项目或者指定的任务需要一些配置的时候这个阶段很有帮助。
记住不管你执行哪个build哪怕是gradle tasks配置代码都会执行
在执行阶段任务按顺序执行,执行顺序是通过依赖关系决定的,标记为up-to-date的任务会跳过,比如任务B依赖于任务A,当你运行gradle B的时候执行顺序将是A->B。

  • 编写自定义的任务

makeReleaseVersion的逻辑比较简单,你可能不用考虑代码维护的问题,随着构建逻辑越来越复杂,你添加了越来越多的简单的任务,这时候你就有需要用类和方法来结构化你的代码,你可以把你编写源代码的那一套代码实践搬过来。

  • 1、 编写自定义任务类

之前提到过,Gradle会给每一个任务创建一个DefaultTask类型的实例,当你要创建一个自定义的任务时,你需要创建一个继承自DefaultTask的类,看看下面这个例子:

class ReleaseVersionTask extends DefaultTask {
    //通过注解声明任务的输入和输出    
    @Input Boolean release
    @OutputFile File destFile

    ReleaseVersionTask() {
        //在构造器里设置任务的分组和描述
        group = 'versioning'
        description = 'Makes project a release version.'
    }
    //通过注解声明要执行的任务
    @TaskAction
    void start() {
        project.version.release = true
        ant.propertyfile(file: destFile) {
            entry(key: 'release', type: 'string', operation: '=', value: 'true')
            }
    }
}

通过注解来表达输入和输出
任务输入和输出注解给你的实现添加了语法糖,他们和调用TasksInputs和TaskOutputs方法是一样的效果,你一眼就知道任务期望什么样的输入数据以及会产生什么输出。我们使用@Input注解来声明输入属性release,用@OutputFile来定义输出文件。

  • 2、使用自定义的任务

上面我们实现了自定义的动作方法,但是我们怎么使用这个方法,你需要在build脚本中创建一个ReleaseVersionTask类型的任务,通过给属性赋值来设定输入和输出:

//定义一个ReleaseVersionTask类型的任务
task makeReleaseVersion(type: ReleaseVersionTask) {
    //设定任务属性
    release = version.release
    destFile = versionFile
}
  • 3、复用自定义的任务

假设你在另一个项目中想使用前面这个自定义的任务,在另一个项目中需求又不太一样,用来表示版本的POGO有不同的域,比如下面这个:

class ProjectVersion {
    Integer min
    Integer maj
    Boolean prodReady

    @Override
    String toString() {
        "$maj.$min${prodReady? '' : '-SNAPSHOT'}"
    }
}

此外,你还想把版本文件名改为project-version.properties,需要怎么做才能复用上面那个自定义的任务呢?

task makeReleaseVersion(type: ReleaseVersionTask) {
    release = version.prodReady
    //不同的版本文件
    destFile = file('project-version.properties')
}
  • Gradle自带的任务类型

Gradle自带的任务类型继承自DefaultTask,Gradle提供了很多自带的任务类型,这里我只介绍两个,Zip和copy用在发布项目中。

//eg.使用任务类型来备份发布版本
task createDistribution(type: Zip, dependsOn:     makeReleaseVersion) {
    //引用war任务的输出
    from war.outputs.files
    //把所有文件放进ZIP文件的src目录
    from(sourceSets*.allSource) {
        into 'src'
    }
    //添加版本文件
    from(rootDir) {
        include versionFile.name
    }
}

task backupReleaseDistribution(type: Copy) {
    //引用createDistribution的输出
    from createDistribution.outputs.files
    into "$buildDir/backup"
}

task release(dependsOn: backupReleaseDistribution) << {
    logger.quiet 'Releasing the project...'
}

-- sdflskjf --
sdjfskjafja

3、掌握构建生命周期

作为一个构建脚本的开发者,你不应该局限于编写任务动作或者配置逻辑,有时候你想在指定的生命周期事件发生的时候执行一段代码。生命周期事件可以在指定的生命周期之前、之中或者之后发生,在执行阶段之后发生的生命周期事件就该是构建的完成了。

假设你希望在构建失败时能够在开发阶段尽早得到反馈,给构建生命周期事件添加回调有两种方法:一是通过闭包,二是实现 Gradle API 的一个监听接口,Gradle并没有要求你监听生命周期事件,这完全决定于你,通过监听器实现的优势就是可以给你的类写单元测试,看看下面这幅图会有一点直观的印象:

图7

在配置阶段,Gradle决定在任务在执行阶段的执行顺序,依赖关系的内部结构是通过直接的无环图(DAG)来表示的,图中的每一个任务称为一个节点,每一个节点通过边来连接,你很有可能通过dependsOn或者隐式的依赖推导来创建依赖关系。记住DAG图从来不会有环,就是说一个已经执行的任务不会再次执行,下面这幅图将要的展示了这个过程:

图8

回想一下之前我们实现的makeReleaseVersion任务是在release任务之前执行的,我们可以编写一个生命周期回调方法来取代之前写一个任务来执行版本修改任务。构建系统准确知道在执行之前应该运行哪些任务,你可以查询任务图来查看它是否存在,下面这幅图展示了访问任务执行图的相关接口:

图9

接下来我们来添加相应的监听方法,下面这段代码通过调用whenReady方法来注册回调接口,当任务图创建的时候这个回调会自动执行,你知道这个逻辑会在任何任务之前执行,所以你可以移除makeReleaseVersion任务。

gradle.taskGraph.whenReady { TaskExecutionGraph taskGraph ->
   //检查任务图是否包括release任务
    if(taskGraph.hasTask(release)) {

        if(!version.release) {

            version.release = true

            ant.propertyfile(file: versionFile) {
                entry(key: 'release', type: 'string', operation: '=',
                value: 'true')
            }
        }
    }
}

你也可以实现一个监听器来实现同样的效果,首先在构建脚本中编写一个实现指定监听器的类,然后在构建中注册这个实现,监听任务执行图的接口是TaskExecutionGraphListener,编写的时候你只需要实现graphPopulate(TaskExecutionGraph)方法,下图表示了这个过程:

图10

下面是编程实现:

class ReleaseVersionListener implements TaskExecutionGraphListener  {
    final static String releaseTaskPath = ':release'

    @Override

    void graphPopulated(TaskExecutionGraph taskGraph) {
        //查看是否包含release任务
        if(taskGraph.hasTask(releaseTaskPath)) {
            List<Task> allTasks = taskGraph.allTasks
            //查找release任务
            Task releaseTask = allTasks.find {it.path == releaseTaskPath }
            Project project = releaseTask.project

            if(!project.version.release) {

                project.version.release = true
                project.ant.propertyfile(file: project.versionFile) {
                entry(key: 'release', type: 'string', operation: '=',
                    value: 'true')
            }
        }
     }
    }
}
def releaseVersionListener = new ReleaseVersionListener()
//注册监听器
gradle.taskGraph.addTaskExecutionGraphListener(releaseVersionListener)

三、依赖管理

Gradle有自己的依赖管理实现,为了避免其他依赖管理软件比如Ivy和Maven的缺点,Gradle关心的是性能、可靠性和复用性。

1、 简要概述依赖管理

几乎所有基于JVM的项目都会或多或少依赖其他库,假设你在开发一个基于web的项目,你很可能会依赖很受欢迎的开源框架比如Spring MVC来提高效率。Java的第三方库一般以JAR文件的形式存在,一般用库名加版本号来标识。随着开发的进行依赖的第三方库增多小的项目变的越来越大,组织和管理你的JAR文件就很关键。

2、不算完美的依赖管理技术

由于Java语言并没提供依赖管理的工具,所以你的团队需要自己开发一套存储和检索依赖的想法。你可能会采取以下几种常见的方法:

  • 手动复制JAR文件到目标机器,这是最原始的很容易出错的方法。
  • 使用一个共享的存储介质来存储JAR文件(比如共享的网盘),你可以加载网络硬盘或者通过FTP检索二进制文件。这种方法需要开发者事先建立好与仓库的连接,手动添加新的依赖到仓库中。
  • 把依赖的JAR文件同源代码都添加到版本控制系统中。这种方法不需要任何额外的步骤,你的同伴在拷贝仓库的时候就能检索依赖的改变。另一方面,这些JAR文件占用了不必要的空间,当你的项目存在相互之间依赖的时候你需要频繁的check-in的检查源代码是否发生了改变。

3、自动管理依赖的重要性

尽管上面的方法都能用,但是这距离理想的解决方案差远了,因为他们没有提供一个标准化的方法来命名和管理JAR文件。至少你得需要开发库的准确版本和它依赖的库(传递依赖),这个为什么这么重要?

  • 准确知道依赖的版本

如果在项目中你没有准确声明依赖的版本这将会是一个噩梦,如果没有文档你根本无法知道这个库支持哪些特性,是否升级一个库到新的版本就变成了一个猜谜游戏因为你不知道你的当前版本。

  • 管理传递依赖

在项目的早期开发阶段传递依赖就会是一个隐患,这些库是第一层的依赖需要的,比如一个比较常见的开发方案是将Spring和Hibernate结合起来这会引入超过20个其他的开发库,一个库需要很多其他库来正常工作。下图展示了Hibernate核心库的依赖图:

图11

如果没有正确的管理依赖,你可以会遇到没想到过的编译期错误和运行期类加载问题。我们可以总结到我们需要一个更好的方式来管理依赖,一般来讲你想在项目元数据中声明你的依赖和它的版本号。作为一个项目自动化的过程,这个版本的库会自动从中央仓库下载、安装到你的项目中,我们来看几个现有的开源解决方案。

  • 使用自动化的依赖管理

在Java领域里支持声明的自动依赖管理的有两个项目:Apache Ivy(Ant项目用的比较多的依赖管理器)和Maven(在构建框架中包含一个依赖管理器),我不再详细介绍这两个的细节而是解释自动依赖管理的概念和机制。

Ivy和Maven是通过XML描述文件来表达依赖配置,配置包含两部分:依赖的标识加版本号和中央仓库的位置(可以是一个HTTP链接),依赖管理器根据这个信息自动定位到需要下载的仓库然后下载到你的机器中。库可以定义传递依赖,依赖管理器足够聪明分析这个信息然后解析下载传递依赖。如果出现了依赖冲突比如上面的Hibernate core的例子,依赖管理器会试着解决。库一旦被下载就会存储在本地的缓存中,构建系统先检查本地缓存中是否存在需要的库然后再从远程仓库中下载。下图显示了依赖管理的关键元素:

图12

Gradle通过DSL来描述依赖配置,实现了上面描述的架构。

4、自动依赖管理面临的挑战

虽然依赖管理器简化了手工的操作,但有时也会遇到问题。你会发现你的依赖图中会依赖同个库的不同版本,使用日志框架经常会遇到这个问题,依赖管理器基于一个特定的解决方案只选择其中一个版本来避免版本冲突。如果你想知道某个库引入了什么版本的传递依赖,Gradle提供了一个非常有用的依赖报告来回答这个问题。

5、 声明依赖

DSL配置block dependencies用来给配置添加一个或多个依赖,你的项目不仅可以添加外部依赖,下面这张表显示了Gradle支持的各种不同类型的依赖。

图13

这一章只介绍外部模块依赖和文件依赖,我们来看看Gradle APi是怎么表示依赖的。

  • 理解依赖的API表示

每个Gradle项目都有一个DependencyHandler的实例,你可以通过getDependencies()方法来获取依赖处理器的引用,上表中每一种依赖类型在依赖处理器中都有一个相对应的方法。每一个依赖都是Dependency的一个实例,group, name, version, 和classifier这几个属性用来标识一个依赖,下图清晰的表示了项目(Project)、依赖处理器(DependencyHandler)和依赖三者之间的关系:

图14

6、外部模块依赖

在Gradle的术语里,外部库通常是以JAR文件的形式存在,称之为外部模块依赖,代表项目层次外的一个模块,这种类型的依赖是通过属性来唯一的标识,接下来我们来介绍每个属性的作用。

  • 依赖属性

当依赖管理器从仓库中查找依赖时,需要通过属性的结合来定位,最少需要提供一个name。

  • group: 这个属性用来标识一个组织、公司或者项目,可以用点号分隔,Hibernate的group是org.hibernate。

  • name: name属性唯一的描述了这个依赖,hibernate的核心库名称是hibernate-core。

  • version: 一个库可以有很多个版本,通常会包含一个主版本号和次版本号,比如Hibernate核心库3.6.3-Final。

  • classifier: 有时候需要另外一个属性来进一步的说明,比如说明运行时的环境,Hibernate核心库没有提供classifier。

  • 依赖的写法

你可以使用下面的语法在项目中声明依赖:

dependencies {
    configurationName dependencyNotation1,     dependencyNotation2, ...
}

你先声明你要给哪个配置添加依赖,然后添加依赖列表,你可以用map的形式来注明,你也可以直接用冒号来分隔属性,比如这样的:

图15
//声明外部属性
ext.cargoGroup = 'org.codehaus.cargo'
ext.cargoVersion = '1.3.1'

dependencies {
    //使用映射声明依赖
    implementation group: cargoGroup, name: 'cargo-core-uberjar', version: cargoVersion
    //用快捷方式来声明,引用了前面定义的外部属性
    cargo "$cargoGroup:cargo-ant:$cargoVersion"
}

如果你项目中依赖比较多,你把一些共同的依赖属性定义成外部属性可以简化build脚本。

到目前为止还没讲到怎么配置不同类型的仓库,比如你想使用MavenCentral仓库,添加下面的配置代码到你的build脚本中:

repositories {
    mavenCentral()
}

  • 检查依赖报告

当你运行dependencies任务时,这个依赖树会打印出来,依赖树显示了你build脚本声明的顶级依赖和它们的传递依赖:

图16
图17

仔细观察你会发现有些传递依赖标注了*号,表示这个依赖被忽略了,这是因为其他顶级依赖中也依赖了这个传递的依赖,Gradle会自动分析下载最合适的依赖。

  • 排除传递依赖

Gradle允许你完全控制传递依赖,你可以选择排除全部的传递依赖也可以排除指定的依赖,假设你不想使用UberJar传递的xml-api的版本而想声明一个不同版本,你可以使用exclude方法来排除它:

dependencies {
    cargo('org.codehaus.cargo:cargo-ant:1.3.1') {
        exclude group: 'xml-apis', module: 'xml-apis'
    }
    cargo 'xml-apis:xml-apis:2.0.2'
}

exclude属性值和正常的依赖声明不太一样,你只需要声明group和(或)module,Gradle不允许你只排除指定版本的依赖。

有时候仓库中找不到项目依赖的传递依赖,这会导致构建失败,Gradle允许你使用transitive属性来排除所有的传递依赖:

dependencies {
    cargo('org.codehaus.cargo:cargo-ant:1.3.1') {
    transitive = false
    }
    // 选择性的声明一些需要的库
}

  • 动态版本声明

如果你想使用一个依赖的最新版本,你可以使用latest.integration,比如声明 Cargo Ant tasks的最新版本,你可以这样写 org.codehaus .cargo:cargo-ant:latest-integration,你也可以用一个+号来动态的声明:

dependencies {
    //依赖最新的1.x版本
    cargo 'org.codehaus.cargo:cargo-ant:1.+'
}

使用动态版权声明后,如果你不知道它确切使用哪个版本,可以执行gradle –q dependencies命令行去瞧瞧,Gradle可以清晰的看到选择了哪个版本。

7、文件依赖

如果你没有使用自动的依赖管理工具,你可能会把外部库作为源代码的一部分或者保存在本地文件系统中,当你想把项目迁移到Gradle的时候,你不想去重构,Gradle很简单就能配置文件依赖。下面这段代码复制从Maven中央仓库解析的依赖到libs/cargo目录。

task copyDependenciesToLocalDir(type: Copy) {
    //Gradle提供的语法糖
    from configurations.cargo.asFileTree
    into "${System.properties['user.home']}/libs/cargo"
}

运行这个任务之后你就可以在依赖中声明Cargo库了,下面这段代码展示了怎么给cargo配置添加JAR文件依赖:

dependencies {
    cargo fileTree(dir: "${System.properties['user.home']}/libs/cargo",include: '*.jar')
}

8、 配置远程仓库

Gradle支持下面三种不同类型的仓库:

图18

下图是配置不同仓库对应的Gradle API:

图19

下面以Maven仓库来介绍,Maven仓库是Java项目中使用最为广泛的一个仓库,库文件一般是以JAR文件的形式存在,用XML(POM文件)来来描述库的元数据和它的传递依赖。所有的库文件都存储在仓库的指定位置,当你在构建脚本中声明了依赖时,这些属性用来找到库文件在仓库中的准确位置。group属性标识了Maven仓库中的一个子目录,下图展示了Cargo依赖属性是怎么对应到仓库中的文件的:

图20

RepositoryHandler接口提供了两个方法来定义Maven仓库,mavenCentral方法添加一个指向仓库列表的引用,mavenLocal方法引用你文件系统中的本地Maven仓库。

  • 添加Maven仓库

要使用Maven仓库你只需要调用mavenCentral方法,如下所示:

repositories {
    mavenCentral()
}

  • 添加本地仓库

本地仓库默认在 <user_home style="box-sizing: border-box; -webkit-tap-highlight-color: transparent; -webkit-font-smoothing: antialiased; font-size: inherit;">/.m2/repository目录下,只需要添加如下脚本来引用它:</user_home>

repositories {
    mavenLocal()
}

  • 添加自定义Maven仓库

如果指定的依赖不存在与Maven仓库或者你想通过建立自己的企业仓库来确保可靠性,你可以使用自定义的仓库。仓库管理器允许你使用Maven布局来配置一个仓库,这意味着你要遵守artifact的存储模式。你也可以添加验证凭证来提供访问权限,Gradle的API提供两种方法配置自定义的仓库:maven()和mavenRepo()。下面这段代码添加了一个自定义的仓库,如果Maven仓库中不存在相应的库会从自定义仓库中查找:

repositories {
    mavenCentral()
    maven {
        name 'Custom Maven Repository',
        url 'http://repository.forge.cloudbees.com/release/')
    }
}

四、构建多项目

1、理解settings 的API表示

在Gradle开始执行构建之前,它创建一个Settings类型的实例,Settings接口直接用来表示settings文件,主要目的是通过代码来动态添加项目参与到多项目构建中,下图是你可以访问的Gradle API。

图21

之前我们介绍过Gradle有三个生命周期,实例化阶段->配置阶段->执行阶段,Settings处于实例化阶段,Gradle自动找出一个子项目是否处在一个多项目构建中。

2、设置文件解析

Gradle允许你从根目录或者任何子目录中运行构建任务,只要它包含一个build脚本,Gradle怎么知道一个子项目是不是一个多项目构建的一部分呢?他需要查找settings文件,Gradle通过两步来查找设置文件。

    1. Gradle查找和当前目录具有相同嵌套级别的master目录下的设置文件
    1. 如果第一步没有找到设置文件,Gradle从当前目录开始逐步查找父目录

如果找到了settings文件,项目包含在另一个项目中,这个项目就当成是多项目构建的一部分。整个过程如下所示:

图22
图23

3、 配置子项目

我们可以把项目根据功能拆分成多个模块,可以用之前的方法来定义构建逻辑,下面有几点需要主要:

  • 根目录和子目录使用相同的group和version属性值
  • 所有的子目录都是Java项目需要Java插件来正常工作,所以你只需要在子项目中应用Java插件
  • web子项目是唯一一个依赖外部库的项目,它需要打包成WAR而不是JAR
  • 子项目之间可以定义模块依赖

接下来你将学习如何定义特定的和共有的构建逻辑,怎么样去避免重复的配置。有些子项目可能依赖其他项目的源代码,比如repository项目依赖model项目,通过声明项目依赖可以避免拷贝源代码。

4、理解项目的API表示

之前我介绍过项目Project可能会用到的一些API,接下来还有一些API用在多项目构建中。project方法用于声明指定项目的构建代码,需要提供项目的路径,比如:model。有时候你想给所有的项目或者只有子项目定义一些逻辑,你可以使用allprojects和subprojects方法,比如你想给所有的子项目添加Java插件,你需要使用subprojects方法。

图24

5、定义项目特有的行为

指定项目的行为通过project方法来定义,为了给三个子项目model、repository、web定义构建逻辑,你需要给它们分别创建一个配置块。下面build.gradle文件:

ext.projectIds = ['group': 'com.manning.gia', 'version': '0.1']

group = projectIds.group
version = projectIds.version

project(':model') {
    group = projectIds.group
    version = projectIds.version
    apply plugin: 'java'
}

project(':repository') {
    group = projectIds.group
    version = projectIds.version
    apply plugin: 'java'
}

project(':web') {
    group = projectIds.group
    version = projectIds.version
    apply plugin: 'java'
    apply plugin: 'war'
    apply plugin: 'jetty'

    repositories {
        mavenCentral()
    }

    dependencies {
        providedCompile 'javax.servlet:servlet-api:2.5'
        runtime 'javax.servlet:jstl:1.1.2'
    }

}

从整个项目的根目录那里,你可以执行子项目的任务,需要记住项目路径是通过冒号分隔,比如你想执行model子项目的build任务,在命令行中执行gradle :modle:build就可以。

6、声明项目依赖

声明项目依赖和声明外部依赖非常类似,只需要在dependencies配置块中声明,如下所示:

project(':model') {
...
}

project(':repository') {
    ...

    dependencies {
        //声明编译期依赖项目model
        compile project(':model')

    }

}

project(':web') {
    ...

    dependencies {
        //声明编译期依赖项目repository
        compile project(':repository')
        providedCompile 'javax.servlet:servlet-api:2.5'
        runtime 'javax.servlet:jstl:1.1.2'
    }

}

这样就定义了我们的项目依赖,注意当一个项目依赖于另一个项目时,另一个项目的项目依赖和外部依赖同样会被添加进来,在构建周期的初始化阶段,Gradle决定项目的执行顺序。

有时候你并不需要重新构建那些并未改变的项目,Gradle提供了部分构建partial builds的选项,通过命令行选项-a或者--no-rebuild。比如你只改变了repository项目不想重新构建model子项目,你可以这样做:gralde :repository:build -a.

7、 定义共同的行为

上面你在所有的子项目中添加了Java插件,给所有的项目添加了一个外部属性ProjectIds,当你的子项目变得比较多的时候这样子做可能是个问题,接下来你可以通过allprojects和subprojects方法来改善你的构建代码。

图25

这是什么意思?这意味着你可以用allprojects方法给所有的项目添加group和version属性,由于根项目不需要Java插件,你可以使用subprojects给所有子项目添加Java插件,如下所示:

allprojects {
    group = 'com.manning.gia'
    version = '0.1'
}

subprojects {
    apply plugin: 'java'
}

project(':repository') {
    dependencies {
        compile project(':model')
    }
}

project(':web') {
    apply plugin: 'war'
    apply plugin: 'jetty'

    repositories {
        mavenCentral()
    }

    dependencies {
        compile project(':repository')
        providedCompile 'javax.servlet:servlet-api:2.5'
        runtime 'javax.servlet:jstl:1.1.2'
    }
}

8、 拆分项目文件

到目前为止我们自定义了一个build.gradle和settings.gradle文件,随着你添加越来越多的子项目和任务到build.gradle中,代码的维护性将会下降。通过给每个子项目建立一个单独的build.gradle文件可以解决这个问题。

接下来我们在每个子项目的目录下创建一个build.gradle文件,目录如下:

图25

现在你可以把构建逻辑从原先的build脚本中拆分开来放到合适的位置。

  • 定义根项目的构建代码

移除了那些与特定子项目相关的代码后,根项目的内容变得非常简单,只需要保留allprojects和subprojects配置块,如下所示:

allprojects {
    group = 'com.manning.gia'
    version = '0.1'
}

subprojects {
    apply plugin: 'java'
}
  • 定义子项目的构建代码

接下来你只需要把与特定项目相关的构建代码移到相应的build.gradle文件里就可以了,如下所示:
repository子项目的构建代码:

dependencies {
    compile project(':model')
}

web子项目的构建代码:

apply plugin: 'war'
apply plugin: 'jetty'

repositories {
    mavenCentral()
}

dependencies {
    compile project(':repository')
    providedCompile 'javax.servlet:servlet-api:2.5'
    runtime 'javax.servlet:jstl:1.1.2'
}

运行这个多项目构建和之前单独的一个build脚本的结果完全一样,当时你该上了构建代码的可读性和维护性。

五、用Gradle构建你的JAVA项目

从打包部署成可执行的应用,我们需要编译源代码,生成的class文件需要打包到JAR文件中。JDK提供了javac 和jar工具帮助你实现这些任务,但是你也不想每次源代码发生变化时你都手动去执行这些任务吧。
Gradle插件能够自动化完成这些任务,插件引入了一些领域特有的观念,其中一个Gradle插件就是java插件,Java插件不仅仅只有编译和打包的功能,它给你的项目安排了一个标准布局,并确保你所有的任务都是按序执行,现在该应用java插件来构建你的build脚本了。

1、 使用java插件

每个Gradle项目都会创建一个build.gradle文件,如果你想使用java插件只需要添加下面这行代码:

apply plugin: 'java'

这一行代码足以构建你的项目,但是Gradle怎么知道你的源代码放在哪个位置呢?java插件的一个约定就是源代码的位置,默认情况下插件搜索src/main/java路径下的文件,你的包名com.manning.gia.todo会转换成源代码根目录下的子目录,创建build脚本之后你的项目结构应该是这样的:

图26

2、 构建项目

现在你可以构建你的项目了,java插件添加了一个build任务到你项目中,build任务编译你的代码、运行测试然后打包成jar文件,所有都是按序执行的。运行gradle build之后你的输出应该是类似这样的:

$ gradle build
:compileJava
:processResources UP-TO-DATE
:classes
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
:check
:build

输出的每一行都表示一个可执行的任务,你可能注意到有一些任务标记为 UP-TO-DATE,这意味着这些任务被跳过了,gradle能够自动检查哪些部分没有发生改变,就把这部分标记下来,省的重复执行。在大型的企业项目中可以节省不少时间。执行完gradle build之后项目结构应该是类似这样的:

图27
图28

在项目的根目录你可以找到一个build目录,这里包含了所有的输出,包含class文件,测试报告,打包的jar文件,以及一些用来归档的临时文件。如果你之前使用过maven,它的标准输出是target,这两个结构应该很类似。jar文件目录build/libs下可以直接运行,jar文件的名称直接由项目名称得来的,这里是todo-app。

3、 运行项目

你只需要使用JDK的java命令就可以执行这个应用了:

$ java -cp build/classes/main com.manning.gia.todo.ToDoApp
--- To Do Application ---
Please make a choice:
(a)ll items
(f)ind a specific item
(i)nsert a new item
(u)pdate an existing item
(d)elete an existing item
(e)xit
>

接下来我们会学习如何自定义项目结构。

4、自定义你的项目

Java插件是一个非常固执的框架,对于项目很多的方面它都假定有默认值,比如项目布局,如果你看待世界的方法是不一样的,Gradle给你提供了一个自定义约定的选项。想知道哪些东西是可以配置的?可以参考这个手册:http://www.gradle.org/docs/current/dsl/,之前提到过,运行命令行gradle properties可以列出可配置的标准和插件属性以及他们的默认值。

  • 修改你的项目和插件属性

接下来你将学习如何指定项目的版本号、Java源代码的兼容级别,前面你用的java命令来运行应用程序,你需要通过命令行选项-cp build/classes/main指定class文件的位置给Java运行时。但是要从JAR文件中启动应用,你需要在manifest文件MANIFEST.MF中包含首部Main-Class。看下面的脚本你就明白怎么操作了:

//Identifies project’sversion through a number scheme
version = 0.1

//Sets Java version compilation compatibility to 1.6
sourceCompatibility = 1.6

//Adds Main-Class header to JAR file’s manifest

jar {
manifest {
    attributes 'Main-Class': 'com.manning.gia.todo.ToDoApp'
}
}

打包成JAR之后,你会发现JAR文件的名称变成了todo-app-0.1.jar,这个jar包含了main-class首部,你就可以通过命令java -jar build/libs/todo-app-0.1.jar运行了。

接下来学习如何改变项目的默认布局:

//Replaces conventional source code directory with list of different directories

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
    }
//Replaces conventional test source code directory with list of different directories    

    test {
        java {
            srcDirs = ['test']
            }
        }
}

//Changes project output property to directory out

buildDir = 'out'

  • 配置和使用外部依赖

在Java世界里,依赖是分布的以JAR文件的形式存在,许多库都从仓库里获得,比如一个文件系统或中央服务器。Gradle需要你指定至少一个仓库作为依赖下载的地方,比如mavenCentral: //Shortcut notation for configuring Maven Central 2 repository accessible under http://repo1.maven.org/maven2

repositories {
    mavenCentral()
}

  • 定义依赖

接下来就是定义依赖,依赖通过group标识,name和version来确定,比如下面这个:

dependencies {
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.1'
}

Gradle是通过配置来给依赖分组,Java插件引入的一个配置是compile,你可以很容易区分这个配置定义的依赖是用来编译源代码的。

  • 解析依赖

Gradle能够自动检测并下载项目定义的依赖:

$ gradle build
:compileJava
Download http://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.1/commons-lang3-3.1.pom

Download http://repo1.maven.org/maven2/org/apache/commons/commons-parent/22/commons-parent-22.pom

Download http://repo1.maven.org/maven2/org/apache/apache/9/apache-9.pom
Download http://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.1/commons-lang3-3.1.jar

:processResources UP-TO-DATE
...
:build
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,748评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,504评论 25 707
  • 星期五,8至10度,雾霾天,西安. 早上7:00被早起的曹语轩折腾,非要让我送他去上学,在他各种捏鼻子揉脸扯被...
    45度向阳阅读 119评论 0 1
  • 【与萌共长】20171016学习力践行D6 和爸爸一起接萌跳舞下课,给萌萌买小助教礼物,让她自己选。她选了一个蛋糕...
    艳萍和萌宝阅读 271评论 0 0