gradle中的增量构建浅析

文章目录 简介增量构建自定义inputs和outputs运行时API隐式依赖输入校验自定义缓存方法输入归一化其他使用技巧

简介

在我们使用的各种工具中,为了提升工作效率,总会使用到各种各样的缓存技术,比如说docker中的layer就是缓存了之前构建的image。在gradle中这种以task组合起来的构建工具也不例外,在gradle中,这种技术叫做增量构建。

增量构建

gradle为了提升构建的效率,提出了增量构建的概念,为了实现增量构建,gradle将每一个task都分成了三部分,分别是input输入,任务本身和output输出。下图是一个典型的java编译的task。

以上图为例,input就是目标jdk的版本,源代码等,output就是编译出来的class文件。

增量构建的原理就是监控input的变化,只有input发送变化了,才重新执行task任务,否则gradle认为可以重用之前的执行结果。

所以在编写gradle的task的时候,需要指定task的输入和输出。

并且要注意只有会对输出结果产生变化的才能被称为输入,如果你定义了对初始结果完全无关的变量作为输入,则这些变量的变化会导致gradle重新执行task,导致了不必要的性能的损耗。

还要注意不确定执行结果的任务,比如说同样的输入可能会得到不同的输出结果,那么这样的任务将不能够被配置为增量构建任务。

自定义inputs和outputs

既然task中的input和output在增量编译中这么重要,本章将会给大家讲解一下怎么才能够在task中定义input和output。

如果我们自定义一个task类型,那么满足下面两点就可以使用上增量构建了:

第一点,需要为task中的inputs和outputs添加必要的getter方法。

第二点,为getter方法添加对应的注解。

gradle支持三种主要的inputs和outputs类型:

  1. 简单类型:简单类型就是所有实现了Serializable接口的类型,比如说string和数字。
  2. 文件类型:文件类型就是 File 或者 FileCollection 的衍生类型,或者其他可以作为参数传递给 Project.file(java.lang.Object) 和 Project.files(java.lang.Object…) 的类型。
  3. 嵌套类型:有些自定义类型,本身不属于前面的1,2两种类型,但是它内部含有嵌套的inputs和outputs属性,这样的类型叫做嵌套类型。

接下来,我们来举个例子,假如我们有一个类似于FreeMarker和Velocity这样的模板引擎,负责将模板源文件,要传递的数据最后生成对应的填充文件,我们考虑一下他的输入和输出是什么。

输入:模板源文件,模型数据和模板引擎。

输出:要输出的文件。

如果我们要编写一个适用于模板转换的task,我们可以这样写:

import java.io.File;
import java.util.HashMap;
import org.gradle.api.*;
import org.gradle.api.file.*;
import org.gradle.api.tasks.*;

public class ProcessTemplates extends DefaultTask {
  private TemplateEngineType templateEngine;
  private FileCollection sourceFiles;
  private TemplateData templateData;
  private File outputDir;

  @Input
  public TemplateEngineType getTemplateEngine() {
    return this.templateEngine;
  }

  @InputFiles
  public FileCollection getSourceFiles() {
    return this.sourceFiles;
  }

  @Nested
  public TemplateData getTemplateData() {
    return this.templateData;
  }

  @OutputDirectory
  public File getOutputDir() { return this.outputDir; }

  // 上面四个属性的setter方法

  @TaskAction
  public void processTemplates() {
    // ...
  }
}

上面的例子中,我们定义了4个属性,分别是TemplateEngineType,FileCollection,TemplateData和File。前面三个属性是输入,后面一个属性是输出。

除了getter和setter方法之外,我们还需要在getter方法中添加相应的注释: @Input , @InputFiles ,@Nested 和 @OutputDirectory, 除此之外,我们还定义了一个 @TaskAction 表示这个task要做的工作。

TemplateEngineType表示的是模板引擎的类型,比如FreeMarker或者Velocity等。我们也可以用String来表示模板引擎的名字。但是为了安全起见,这里我们自定义了一个枚举类型,在枚举类型内部我们可以安全的定义各种支持的模板引擎类型。

因为enum默认是实现Serializable的,所以这里可以作为@Input使用。

sourceFiles使用的是FileCollection,表示的是一系列文件的集合,所以可以使用@InputFiles。

为什么TemplateData是@Nested类型的呢?TemplateData表示的是我们要填充的数据,我们看下它的实现:

import java.util.HashMap;
import java.util.Map;
import org.gradle.api.tasks.Input;

public class TemplateData {
  private String name;
  private Map<String, String> variables;

  public TemplateData(String name, Map<String, String> variables) {
    this.name = name;
    this.variables = new HashMap<>(variables);
  }

  @Input
  public String getName() { return this.name; }

  @Input
  public Map<String, String> getVariables() {
    return this.variables;
  }
}

可以看到,虽然TemplateData本身不是File或者简单类型,但是它内部的属性是简单类型的,所以TemplateData本身可以看做是@Nested的。

outputDir表示的是一个输出文件目录,所以使用的是@OutputDirectory。

使用了这些注解之后,gradle在构建的时候就会检测和上一次构建相比,这些属性有没有发送变化,如果没有发送变化,那么gradle将会直接使用上一次构建生成的缓存。

注意,上面的例子中我们使用了FileCollection作为输入的文件集合,考虑一种情况,假如只有文件集合中的某一个文件发送变化,那么gradle是会重新构建所有的文件,还是只重构这个被修改的文件呢?
留给大家讨论

除了上讲到的4个注解之外,gradle还提供了其他的几个有用的注解:

@InputFile: 相当于File,表示单个input文件。

@InputDirectory: 相当于File,表示单个input目录。

@Classpath: 相当于Iterable,表示的是类路径上的文件,对于类路径上的文件需要考虑文件的顺序。如果类路径上的文件是jar的话,jar中的文件创建时间戳的修改,并不会影响input。

@CompileClasspath:相当于Iterable,表示的是类路径上的java文件,会忽略类路径上的非java文件。

@OutputFile: 相当于File,表示输出文件。

@OutputFiles: 相当于Map<String, File> 或者 Iterable,表示输出文件。

@OutputDirectories: 相当于Map<String, File> 或者 Iterable,表示输出文件。

@Destroys: 相当于File 或者 Iterable,表示这个task将会删除的文件。

@LocalState: 相当于File 或者 Iterable,表示task的本地状态。

@Console: 表示属性不是input也不是output,但是会影响console的输出。

@Internal: 内部属性,不是input也不是output。

@ReplacedBy: 属性被其他的属性替换了,不能算在input和output中。

@SkipWhenEmpty: 和@InputFiles 跟 @InputDirectory一起使用,如果相应的文件或者目录为空的话,将会跳过task的执行。

@Incremental: 和@InputFiles 跟 @InputDirectory一起使用,用来跟踪文件的变化。

@Optional: 忽略属性的验证。

@PathSensitive: 表示需要考虑paths中的哪一部分作为增量的依据。

运行时API

自定义task当然是一个非常好的办法来使用增量构建。但是自定义task类型需要我们编写新的class文件。有没有什么办法可以不用修改task的源代码,就可以使用增量构建呢?

答案是使用Runtime API。

gradle提供了三个API,用来对input,output和Destroyables进行获取:

  • Task.getInputs() of type TaskInputs
  • Task.getOutputs() of type TaskOutputs
  • Task.getDestroyables() of type TaskDestroyables

获取到input和output之后,我们就是可以其进行操作了,我们看下怎么用runtime API来实现之前的自定义task:

task processTemplatesAdHoc {
  inputs.property("engine", TemplateEngineType.FREEMARKER)
  inputs.files(fileTree("src/templates"))
    .withPropertyName("sourceFiles")
    .withPathSensitivity(PathSensitivity.RELATIVE)
  inputs.property("templateData.name", "docs")
  inputs.property("templateData.variables", [year: 2013])
  outputs.dir("$buildDir/genOutput2")
    .withPropertyName("outputDir")

  doLast {
    // Process the templates here
  }
}

上面例子中,inputs.property() 相当于 @Input ,而outputs.dir() 相当于@OutputDirectory。

Runtime API还可以和自定义类型一起使用:

task processTemplatesWithExtraInputs(type: ProcessTemplates) {
  // ...

  inputs.file("src/headers/headers.txt")
    .withPropertyName("headers")
    .withPathSensitivity(PathSensitivity.NONE)
}

上面的例子为ProcessTemplates添加了一个input。

隐式依赖

除了直接使用dependsOn之外,我们还可以使用隐式依赖:

task packageFiles(type: Zip) {
  from processTemplates.outputs
}

上面的例子中,packageFiles 使用了from,隐式依赖了processTemplates的outputs。

gradle足够智能,可以检测到这种依赖关系。

上面的例子还可以简写为:

task packageFiles2(type: Zip) {
  from processTemplates
}

我们看一个错误的隐式依赖的例子:

plugins {
  id 'java'
}

task badInstrumentClasses(type: Instrument) {
  classFiles = fileTree(compileJava.destinationDir)
  destinationDir = file("$buildDir/instrumented")
}

这个例子的本意是执行compileJava任务,然后将其输出的destinationDir作为classFiles的值。

但是因为fileTree本身并不包含依赖关系,所以上面的执行的结果并不会执行compileJava任务。

我们可以这样改写:

task instrumentClasses(type: Instrument) {
  classFiles = compileJava.outputs.files
  destinationDir = file("$buildDir/instrumented")
}

或者使用layout:

task instrumentClasses2(type: Instrument) {
  classFiles = layout.files(compileJava)
  destinationDir = file("$buildDir/instrumented")
}

或者使用buildBy:

task instrumentClassesBuiltBy(type: Instrument) {
  classFiles = fileTree(compileJava.destinationDir) {
    builtBy compileJava
  }
  destinationDir = file("$buildDir/instrumented")
}

输入校验

gradle会默认对@InputFile ,@InputDirectory 和 @OutputDirectory 进行参数校验。

如果你觉得这些参数是可选的,那么可以使用@Optional。

自定义缓存方法

上面的例子中,我们使用from来进行增量构建,但是from并没有添加@InputFiles, 那么它的增量缓存是怎么实现的呢?

我们看一个例子:

public class ProcessTemplates extends DefaultTask {
  // ...
  private FileCollection sourceFiles = getProject().getLayout().files();

  @SkipWhenEmpty
  @InputFiles
  @PathSensitive(PathSensitivity.NONE)
  public FileCollection getSourceFiles() {
    return this.sourceFiles;
  }

  public void sources(FileCollection sourceFiles) {
    this.sourceFiles = this.sourceFiles.plus(sourceFiles);
  }

  // ...
}

上面的例子中,我们将sourceFiles定义为可缓存的input,然后又定义了一个sources方法,可以将新的文件加入到sourceFiles中,从而改变sourceFile input,也就达到了自定义修改input缓存的目的。

我们看下怎么使用:

task processTemplates(type: ProcessTemplates) {
  templateEngine = TemplateEngineType.FREEMARKER
  templateData = new TemplateData("test", [year: 2012])
  outputDir = file("$buildDir/genOutput")

  sources fileTree("src/templates")
}

我们还可以使用project.layout.files()将一个task的输出作为输入,可以这样做:

 public void sources(Task inputTask) {
    this.sourceFiles = this.sourceFiles.plus(getProject().getLayout().files(inputTask));
  }

这个方法传入一个task,然后使用project.layout.files()将task的输出作为输入。

看下怎么使用:

task copyTemplates(type: Copy) {
  into "$buildDir/tmp"
  from "src/templates"
}

task processTemplates2(type: ProcessTemplates) {
  // ...
  sources copyTemplates
}

非常的方便。

如果你不想使用gradle的缓存功能,那么可以使用upToDateWhen()来手动控制:

task alwaysInstrumentClasses(type: Instrument) {
  classFiles = layout.files(compileJava)
  destinationDir = file("$buildDir/instrumented")
  outputs.upToDateWhen { false }
}

上面使用false,表示alwaysInstrumentClasses这个task将会一直被执行,并不会使用到缓存。

输入归一化

要想比较gradle的输入是否是一样的,gradle需要对input进行归一化处理,然后才进行比较。

我们可以自定义gradle的runtime classpath 。

normalization {
  runtimeClasspath {
    ignore 'build-info.properties'
  }
}

上面的例子中,我们忽略了classpath中的一个文件。

我们还可以忽略META-INF中的manifest文件的属性:

normalization {
  runtimeClasspath {
    metaInf {
      ignoreAttribute("Implementation-Version")
    }
  }
}

忽略META-INF/MANIFEST.MF :

normalization {
  runtimeClasspath {
    metaInf {
      ignoreManifest()
    }
  }
}

忽略META-INF中所有的文件和目录:

normalization {
  runtimeClasspath {
    metaInf {
      ignoreCompletely()
    }
  }
}

其他使用技巧

如果你的gradle因为某种原因暂停了,你可以送 --continuous 或者 -t 参数,来重用之前的缓存,继续构建gradle项目。

你还可以使用 --parallel 来并行执行task。

到此这篇关于gradle中的增量构建的文章就介绍到这了,更多相关gradle增量构建内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • android studio更新gradle错误构建项目失败的解决方法

    一.版本错误 对应版本,修改gradle version,和plusing version两个地方修改gradle version,和plusing version的方法有两种,一种是在 project.build.这里plusing version的设置 buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.0' } } allprojects

  • 详解IDEA下Gradle多模块(项目)的构建

    我们在新起一个项目的时候,一般都会建多个子项目(IDEA里面称之为Module模块).通过Gradle构建,多个Module之间需要将公用的配置抽取到全局,子项目中只写差异化的配置,以便于维护. 多模块项目的Gradle目录结构 示例:我的示例项目demo,我需要有一个common模块用于公用代码,一个rest模块用于提供rest接口,rest依赖common,如果用gradle构建,目录树会是这样: demo ├── build.gradle -- 全局配置 ├── settings.grad

  • 利用Gradle如何构建scala多模块工程的步骤详解

    前言 Scala是一门强大的语言,受到很多人的喜爱,我也曾经尝试学习过.不过Scala官网专用的构建工具SBT就不那么好用了.我曾经想将SBT的软件包保存路径设置到D盘,还想设置网络代理,不过最后都没搞明白怎么回事.相信也有很多同学想学习Scala,但是却被SBT挡在了门外.偶然之下我发现现在Gradle增加了scala插件,可以完美支持Scala项目. 前段时间终于无法忍受sbt慢如龟速的编译打包速度了.稍稍调研了一下,就果断切换到了gradle.由于调研得比较匆忙,在使用过程中遇到了各种问题

  • Gradle构建多模块项目的方法步骤

    通常我在使用Maven构建项目的时候是将应用项目划分为多个更小的模块. Gradle 项目也拥有多于一个组件,我们也将其称之为多项目构建(multi-project build). 我们首先创建一个多项目构建: mkdir cmdGradleProj && cd cmdGradleProj gradle init 这时候 D:\cmdGradleProj> 目录下执行:tree /f 的项目结构如下: │ build.gradle │ gradlew │ gradlew.bat │

  • springboot+gradle 构建多模块项目的步骤

    springboot用以进行web项目开发的便捷性,本文不再赘述,主要是想将工作中基于springboot与gradle的多模块项目的构建经验进行总结与归纳. 1.创建项目 首先安装java和gradle,本文选用的java版本为1.8.0_40,gradle版本为2.10,安装过程本文不再赘述. 其次创建项目,名称为 springboot-mm: mkdir springboot-mm cd springboot-mm gradle init 此时的项目结构如下: 接下来,创建多个模块,这里以

  • 解决Android studio3.6安装后gradle Download失败(构建不成功)

    因为课程需要,昨天好多同学在安装Android studio3.6.1后,无法构建,不知道什么原因,我的电脑上使用的是之前3.4版本的,可以正常使用,所以没太关心.但晚上我想到3.6版本应该有一些新功能,所以我就想升级一下,升级完之后,发现之内的设计视图是不显示的,需要该工程成功构建之后才能正常使用,于是我就build一下,结果就凉凉了 gradle Download十几分钟,然后失败 两次之后我想到可能是因为跨版本更新可能导致很多东西报错.于是卸载重新安装新版本的,之后构建情况一模一样, gr

  • gradle中的增量构建浅析

    文章目录 简介增量构建自定义inputs和outputs运行时API隐式依赖输入校验自定义缓存方法输入归一化其他使用技巧 简介 在我们使用的各种工具中,为了提升工作效率,总会使用到各种各样的缓存技术,比如说docker中的layer就是缓存了之前构建的image.在gradle中这种以task组合起来的构建工具也不例外,在gradle中,这种技术叫做增量构建. 增量构建 gradle为了提升构建的效率,提出了增量构建的概念,为了实现增量构建,gradle将每一个task都分成了三部分,分别是in

  • Android如何在Gradle中更改APK文件名详解

    前言 本文主要给大家介绍了关于Android在Gradle中更改APK文件名的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 默认情况下,Android Studio中的Gradle构建命名为.apk文件app-release.apk.对应用程序build.gradle文件进行了一些小的更改,可以将.apk名称更改为<app name>-release-<version>.apk. 需要在app的目录下面修改build.gradle文件. 第一步是将pro

  • JavaScript中的 attribute 和 jQuery中的 attr 方法浅析

    根据大体上的意思我感觉js setAttribute与jquery中attr工作是完全一样的,只是jquery中简写了并且工能更强大了,下面我来分别介绍一下他们的用法. attribute 是原生js dom 对象上的一个属性,这个属性有很多子属性,比如 isId(判断属性是否是Id) , name (获取属性名称) , value (获取属性值),attributes 用来获取dom元素 的所有属性集合. 话不多说,上例子了: <input type="text" name=&

  • Kotlin之在Gradle中无参(no-arg)编译器插件的使用详解

    1.前言 最近在用Kotlin+Spring Boot写一个后端项目,实体类习惯性地用了Kotlin中的data class,但是Spring要求要有一个无参的构造函数,否则可能会抛出java.sql.SQLDataException.要使data class能够生成一个无参的构造函数,有两种方法可以做到: 1.给data class的构造函数中的每一个参数都赋上默认值.比如: data class User( @TableId(value = "id", type = IdType.

  • Android Studio新建工程默认在build.gradle中加入maven阿里源的问题

    背景: 在安卓开发时,我们时常会因为gradle时间漫长感到烦恼.通常情况下我们会在build.gradle(Project:MyApplication)中的repositories里添加阿里源,如下图所示. 而每次新建工程依然是google()和jcenter(),而且新建后自动帮你Sync,如果网速不佳则是等了好久才编译好一个新的工程. 解决方案: Android Studio新建工程自动加入maven阿里源 版本:笔者用的是AndroidStudio 3.4,其他版本略同 找到Androi

  • gradle中的properties文件详解

    本文侧重于了解自定义properties文件,以及properties中的数据格式,在gradle中如何使用等知识点. 一.概述 在Android Studio 创建一个项目的时候,rootProject下面会生成gradle.properties和local.properties文件,如下图: 其中,gradle.properties中的内容不需要显示调用就可以直接在build.gradle中进行使用(local.properties暂时无法直接验证). properties其实是Java项目

  • python中count函数知识点浅析

    python中,count函数的作用是进行python中的数量计算.count函数用于统计字符串.列表或元祖中某个字符出现的次数,是一个很好用的统计函数.具体介绍请看本文. 1.count函数 统计列表ls中value元素出现的次数 2.语法 str.count("char", start,end) 或 str.count("char") -> int 返回整数 3.参数 str -- 为要统计的字符(可以是单字符,也可以是多字符). star -- 为索引字

  • python中xlutils库用法浅析

    不少小伙伴认为,直接去操作excel,比我们利用各种代码数据去处理,直接又简单,不那么花里胡哨,但是在代码上,处理数据,直接的软件操作是行不通的,需要我们去利用代码去处理,其实解决麻烦的办法非常简单,只需要我们调用专业的处理数据的模块,就可以轻松处理了,比如excel处理中的xlutils库,下面详细为大家介绍使用. 简单介绍: 最常见的使用在excel中的复制. 安装方式: pip install xlutils 注意点: 虽然可以进行excel的复制.但是只能提供写操作,不能够复制格式. 使

  • python中uuid模块实例浅析

    很多人不明白,学习这些冷门的函数基本上都用不到,或者说是什么多大用处,事实上,有是有很多用处的,比如今天给大家介绍的uuid模块,就能够生成一个真正的随机数,还可以给数据库生成唯一ID,很多地方都可以用到的,感兴趣的小伙伴,可以跟着小编,一起来认真的学习了解下啦. 模块安装: pip install uuid 模块导入: import uuid 使用场景: 1.能够生成时间戳 2.可以生成随机数 使用方法: import uuid uuid.uuid3(uuid.NAMESPACE_DNS, '

  • vs2019中使用MFC构建简单windows窗口程序

    微软基础类库(英语: Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量.其中包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类. vs 2019 最新版,在设计上又有了很大的变化,并且其所有的服务,模块都是自定义搭建的,所以在一开始安装的时候,没有勾选mfc模块的话,是无法快速构成mfc应用的. vs2019下MFC模块的安装 首

随机推荐