Worker API提供了将任务动作的执行分解为离散的工作单元,然后同时并异步执行该工作的功能. 这使Gradle可以充分利用可用资源并更快地完成构建. 本指南将引导您完成将现有自定义任务转换为使用Worker API的过程.

本指南假定您了解编写Gradle自定义任务的基础. 请考虑首先完成编写Gradle任务 .

What you’ll create

您将首先创建一个自定义任务类,该类为可配置文件集生成MD5哈希值. 然后,您将转换此自定义任务以使用Worker API. 然后,我们将探索以不同级别的隔离度运行任务. 在此过程中,您将了解Worker API的基础知识及其提供的功能.

What you’ll need

  • About

  • 文本编辑器或IDE

  • Java开发套件(JDK),版本1.8或更高版本

  • Gradle发行版5.6或更高版本

Create a custom task class

首先,您需要创建一个自定义任务,该任务会生成一组可配置文件的MD5哈希值.

在新目录中,创建一个buildSrc/build.gradle文件.

buildSrc/build.gradle
repositories {
    jcenter()
}

dependencies {
    implementation "commons-io:commons-io:2.5"
    implementation "commons-codec:commons-codec:1.9" (1)
}
1 您的自定义任务类将使用Apache Commons Codec生成MD5哈希.
如果您不熟悉buildSrc ,那么这是一个特殊目录,允许您定义和构建自定义类,这些类应可用于构建脚本. 有关更多信息,请参见用户手册 .

现在,在buildSrc/src/main/java目录中创建一个自定义任务类. 您应该将此类命名为CreateMD5 .

buildSrc/src/main/java/CreateMD5.java
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.workers.WorkerExecutor;

import javax.inject.Inject;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class CreateMD5 extends SourceTask { (1)
    private final DirectoryProperty destinationDirectory; (2)
    private final ConfigurableFileCollection codecClasspath;

    @Inject
    public CreateMD5() {
        super();
        this.destinationDirectory = getProject().getObjects().directoryProperty();
        this.codecClasspath = getProject().getObjects().fileCollection();
    }

    @OutputDirectory
    public DirectoryProperty getDestinationDirectory() {
        return destinationDirectory;
    }

    @InputFiles
    public ConfigurableFileCollection getCodecClasspath() {
        return codecClasspath;
    }

    @TaskAction
    public void createHashes() {
        for (File sourceFile : getSource().getFiles()) { (3)
            try {
                InputStream stream = new FileInputStream(sourceFile);
                System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
                // Artificially make this task slower.
                Thread.sleep(3000); (4)
                Provider<RegularFile> md5File = destinationDirectory.file(sourceFile.getName() + ".md5");  (5)
                FileUtils.writeStringToFile(md5File.get().getAsFile(), DigestUtils.md5Hex(stream), (String) null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}
1 SourceTask是一种便利类型,用于对一组源文件进行操作的任务.
2 任务的输出将进入已配置的目录.
3 该任务遍历定义为"源文件"的所有文件,并为每个文件创建MD5哈希.
4 插入人工睡眠以模拟对大型文件进行哈希处理(示例文件不会那么大).
5 每个文件的MD5哈希将写入输出目录中,扩展名为" md5".

接下来,创建一个build.gradle来实现新的CreateMD5任务.

build.gradle
plugins { id 'base' } (1)

task md5(type: CreateMD5) {
    destinationDirectory = project.layout.buildDirectory.dir("md5") (2)
    source file("src") (3)
}
1 应用base插件,以便您可以执行clean任务来删除输出.
2 MD5哈希文件将被写入build/md5 .
3 此任务将为src目录中的每个文件生成MD5哈希文件.

现在,您将需要一些源来生成MD5哈希. 在src目录中创建3个文件:

src/einstein.txt
Intellectual growth should commence at birth and cease only at death.
src/feynman.txt
I was born not knowing and have had only a little time to change that here and there.
src/oppenheimer.txt
No man should escape our universities without knowing how little he knows.

此时,您可以尝试一下任务:

$ gradle md5

您应该看到类似于以下内容的输出:

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for oppenheimer.txt...


BUILD SUCCESSFUL in 12s

build/md5目录中,您现在应该看到带有md5扩展名的相应文件,其中包含src目录中文件的MD5哈希值. 请注意,该任务至少需要9秒钟才能运行,因为它一次将每个文件散列一次(即,每个文件大约3秒散列3个文件).

Converting to the Worker API

尽管此任务按顺序处理每个文件,但是每个文件的处理都独立于任何其他文件. 如果这项工作并行完成,并且可以利用多个处理器,那就太好了. 这是Worker API可以提供帮助的地方.

首先,您需要定义一个接口,该接口代表每个工作单元的参数并扩展org.gradle.workers.WorkParameters . 为了生成MD5散列文件,工作单元将需要两个参数:要散列的文件和向其写入散列的文件. 但是,无需创建具体的实现,因为Gradle将在运行时为我们生成一个实现.

buildSrc/src/main/java/MD5WorkParameters.java
import org.gradle.api.file.RegularFileProperty;
import org.gradle.workers.WorkParameters;

public interface MD5WorkParameters extends WorkParameters {
    RegularFileProperty getSourceFile(); (1)
    RegularFileProperty getMD5File();
}
1 Use Property objects to represent the source and MD5 hash files.

其次,您需要将自定义任务的一部分重构为一个单独的类,该部分将为每个文件进行工作. 此类是您的"工作单元"实现,它应该是扩展org.gradle.workers.WorkAction的抽象类.

buildSrc/src/main/java/GenerateMD5.java
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.gradle.workers.WorkAction;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public abstract class GenerateMD5 implements WorkAction<MD5WorkParameters> { (1)
    @Override
    public void execute() {
        try {
            File sourceFile = getParameters().getSourceFile().getAsFile().get();
            File md5File = getParameters().getMD5File().getAsFile().get();
            InputStream stream = new FileInputStream(sourceFile);
            System.out.println("Generating MD5 for " + sourceFile.getName() + "...");
            // Artificially make this task slower.
            Thread.sleep(3000);
            FileUtils.writeStringToFile(md5File, DigestUtils.md5Hex(stream), (String) null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
1 不要实现getParameters()方法-Gradle将在运行时注入它.

现在,您应该更改自定义任务类,以将工作提交给WorkerExecutor,而不是自己完成工作.

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.workers.*;
import org.gradle.api.file.DirectoryProperty;

import javax.inject.Inject;
import java.io.File;

public class CreateMD5 extends SourceTask {
    private final WorkerExecutor workerExecutor; (1)
    private final DirectoryProperty destinationDirectory;

    @Inject
    public CreateMD5(WorkerExecutor workerExecutor) { (2)
        super();
        this.workerExecutor = workerExecutor;
        this.destinationDirectory = getProject().getObjects().directoryProperty();
    }

    @OutputDirectory
    public DirectoryProperty getDestinationDirectory() {
        return destinationDirectory;
    }

    @TaskAction
    public void createHashes() {
        WorkQueue workQueue = workerExecutor.noIsolation(); (3)

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = destinationDirectory.file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> { (4)
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 您需要具有WorkerExecutor服务才能提交您的工作.
2 要获得WorkerExecutor ,请创建一个带有javax.inject.Inject注释的构造函数. 创建任务时,Gradle将在运行时注入WorkerExecutor .
3 在提交工作之前,您需要获取具有所需隔离模式的WorkQueue对象. 稍后我们将详细讨论隔离模式.
4 提交工作单元时,请指定工作单元实现,在本例中为GenerateMD5并配置其参数.

此时,您应该可以再次尝试执行任务.

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for oppenheimer.txt...


BUILD SUCCESSFUL in 4s

尽管工作单元是并行执行的,但MD5哈希文件的生成顺序可能不同,结果应与以前相同. 但是,您应该注意的一件事是任务运行得快得多. 这是因为Worker API对每个文件并行而不是按顺序执行MD5计算.

Changing the isolation mode

隔离模式控制Gradle将工作项彼此隔离以及与Gradle运行时的其余部分隔离的强烈程度. WorkerExecutor上有三种方法可以控制此方法: noIsolation()classLoaderIsolation()processIsolation() . noIsolation()模式是最低的隔离级别,它将阻止工作单元更改项目状态. 这是最快的隔离模式,因为它需要最少的开销来设置要执行的工作项,因此您可能希望在简单的情况下使用此模式. 但是,它将对所有工作单元使用单个共享的类加载器. 这意味着每个工作单元都可能通过静态类状态相互影响. 这也意味着每个工作单元都使用buildscript类路径上相同版本的库. 如果希望用户能够将任务配置为与其他(但兼容)版本的Apache Commons Codec库一起运行,则需要使用其他隔离模式.

首先,您需要将buildSrc/build.gradle的依赖项更改为compileOnly . 这告诉Gradle在构建类时应使用此依赖关系,但不应将其放在构建脚本的类路径中.

buildSrc/build.gradle
repositories {
    jcenter()
}

dependencies {
    implementation "commons-io:commons-io:2.5"
    compileOnly "commons-codec:commons-codec:1.9"
}

接下来,您将需要更改CreateMD5任务,以允许用户配置他们要使用的编解码器库的版本. 它将在运行时解析适当的库版本,并配置工作程序以使用该版本. classLoaderIsolation()方法告诉Gradle在具有隔离类加载器的线程中运行此工作.

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;

import javax.inject.Inject;
import java.io.File;
import java.util.Set;

public class CreateMD5 extends SourceTask {
    private final WorkerExecutor workerExecutor;
    private final DirectoryProperty destinationDirectory;
    private final ConfigurableFileCollection codecClasspath; (1)

    @Inject
    public CreateMD5(WorkerExecutor workerExecutor) {
        super();
        this.workerExecutor = workerExecutor;
        this.destinationDirectory = getProject().getObjects().directoryProperty();
        this.codecClasspath = getProject().getObjects().fileCollection();
    }

    @OutputDirectory
    public DirectoryProperty getDestinationDirectory() {
        return destinationDirectory;
    }

    @InputFiles
    public ConfigurableFileCollection getCodecClasspath() {
        return codecClasspath;
    }

    @TaskAction
    public void createHashes() {
        WorkQueue workQueue = workerExecutor.classLoaderIsolation(workerSpec -> {
            workerSpec.getClasspath().from(codecClasspath); (2)
        });

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = destinationDirectory.file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> {
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 公开编解码器库类路径的输入属性.
2 创建工作队列时,请在ClassLoaderWorkerSpec上配置类路径.

接下来,您将需要配置您的构建,以使其具有用于在任务执行时查找编解码器版本的存储库. 我们还将创建一个依赖项来从此存储库解析我们的编解码器库.

build.gradle
plugins { id 'base' }

repositories {
    jcenter() (1)
}

configurations {
    codec (2)
}

dependencies {
    codec "commons-codec:commons-codec:1.10" (3)
}

task md5(type: CreateMD5) {
    source file("src")
    codecClasspath.from(configurations.codec) (4)
    destinationDirectory = project.layout.buildDirectory.dir("md5")
}
1 添加一个存储库以解析编解码器库-该存储库可以与用于构建CreateMD5任务类的存储库不同.
2 添加配置以保存我们的编解码器库版本.
3 配置Apache Commons Codec的备用兼容版本.
4 配置md5任务以将该配置用作其类路径. 请注意,在实际执行任务之前,不会解析该配置.

现在,如果您运行任务,则可以使用编解码器库的配置版本按预期工作:

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for oppenheimer.txt...


BUILD SUCCESSFUL in 9s

Creating a Worker Daemon

有时,在执行工作项时需要进一步隔离. 例如,外部库可能依赖于要设置的某些系统属性,这些属性可能在工作项之间发生冲突. 或者库可能与Gradle所运行的JDK版本不兼容,并且可能需要与其他版本一起运行. Worker API可以使用processIsolation()方法来解决此问题,该方法使工作在单独的" worker守护程序"中执行. 这些工作程序守护程序进程将在构建之间持久存在,并且可以在后续构建中重用. 但是,如果系统资源不足,Gradle将停止所有未使用的工作程序守护程序.

要利用辅助守护程序,只需在创建WorkQueue时使用processIsolation()方法. 您可能还需要为新过程配置自定义设置.

buildSrc/src/main/java/CreateMD5.java
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFile;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.*;
import org.gradle.process.JavaForkOptions;
import org.gradle.workers.*;

import javax.inject.Inject;
import java.io.File;
import java.util.Set;

public class CreateMD5 extends SourceTask {
    private final WorkerExecutor workerExecutor;
    private final DirectoryProperty destinationDirectory;
    private final ConfigurableFileCollection codecClasspath;

    @Inject
    public CreateMD5(WorkerExecutor workerExecutor) {
        super();
        this.workerExecutor = workerExecutor;
        this.destinationDirectory = getProject().getObjects().directoryProperty();
        this.codecClasspath = getProject().getObjects().fileCollection();
    }

    @OutputDirectory
    public DirectoryProperty getDestinationDirectory() {
        return destinationDirectory;
    }

    @InputFiles
    public ConfigurableFileCollection getCodecClasspath() {
        return codecClasspath;
    }

    @TaskAction
    public void createHashes() {
        (1)
        WorkQueue workQueue = workerExecutor.processIsolation(workerSpec -> {
            workerSpec.getClasspath().from(codecClasspath);
            workerSpec.forkOptions(options -> {
                options.setMaxHeapSize("64m"); (2)
            });
        });

        for (File sourceFile : getSource().getFiles()) {
            Provider<RegularFile> md5File = destinationDirectory.file(sourceFile.getName() + ".md5");
            workQueue.submit(GenerateMD5.class, parameters -> {
                parameters.getSourceFile().set(sourceFile);
                parameters.getMD5File().set(md5File);
            });
        }
    }
}
1 将隔离模式更改为PROCESS .
2 为新过程设置JavaForkOptions .

现在,您应该能够运行您的任务,它将按预期运行,但可以使用辅助守护程序:

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for oppenheimer.txt...


BUILD SUCCESSFUL in 10s

注意执行时间可能会有些长. 这是因为Gradle必须为每个辅助守护程序启动一个新进程,这很昂贵. 但是,如果再次运行任务,则会看到它运行得更快. 这是因为在初始构建期间启动的辅助守护程序已经保留,并且可以在后续构建期间立即使用.

$ gradle clean md5

> Task :md5
Generating MD5 for einstein.txt...
Generating MD5 for feynman.txt...
Generating MD5 for oppenheimer.txt...


BUILD SUCCESSFUL in 5s

Summary

在本指南中,您学习了如何:

  • Introduce the Worker API to an existing custom task to execute work in parallel with minimum isolation

  • 使用Worker API类加载器隔离模式来使用隔离的类路径执行工作

  • 配置任务以在单独的"工作程序守护程序"过程中进一步隔离工作

Help improve this guide

有意见或问题吗? 找到错字了? 像所有Gradle指南一样,帮助只是GitHub问题而已. 请在gradle-guides / using-the-worker-api中添加问题或请求请求,我们将尽快与您联系.