实现二进制插件
二进制插件指的是编译并作为 JAR 文件分发的插件。这些插件通常用 Java 或 Kotlin 编写,并为 Gradle 构建提供自定义功能或 task。
使用 Plugin Development 插件
可以使用 Gradle Plugin Development 插件 来辅助开发 Gradle 插件。
此插件将自动应用 Java Plugin,将 gradleApi()
依赖添加到 api
配置,在生成的 JAR 文件中生成所需的插件描述符,并配置 Plugin Marker Artifact 以在发布时使用。
要应用和配置插件,请将以下代码添加到您的构建文件
plugins {
`java-gradle-plugin`
}
gradlePlugin {
plugins {
create("simplePlugin") {
id = "org.example.greeting"
implementationClass = "org.example.GreetingPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
simplePlugin {
id = 'org.example.greeting'
implementationClass = 'org.example.GreetingPlugin'
}
}
}
在开发插件时,建议编写和使用 自定义 task 类型,因为它能自动受益于 增量构建。将插件应用到您的项目的另一个好处是,validatePlugins
task 会自动检查自定义 task 类型实现中定义的每个公共属性是否都存在输入/输出注解。
创建插件 ID
插件 ID 旨在全局唯一,类似于 Java 包名(即,反向域名)。这种格式有助于防止命名冲突,并允许对具有相似所有权的插件进行分组。
显式的插件标识符简化了将插件应用到项目的过程。您的插件 ID 应组合反映命名空间(指向您或您的组织的合理指针)和其提供的插件名称的组件。例如,如果您的 Github 帐户名为 foo
,而您的插件名为 bar
,则合适的插件 ID 可能是 com.github.foo.bar
。同样,如果插件是在 baz
组织开发的,则插件 ID 可能是 org.baz.bar
。
插件 ID 应遵循以下准则
-
可以包含任何字母数字字符、'.' 和 '-'。
-
必须包含至少一个 '.' 字符,将命名空间与插件名称分隔开。
-
按照惯例,命名空间使用小写反向域名约定。
-
按照惯例,名称中仅使用小写字符。
-
不得使用
org.gradle
、com.gradle
和com.gradleware
命名空间。 -
不能以 '.' 字符开头或结尾。
-
不能包含连续的 '.' 字符(即 '..')。
标识所有权和名称的命名空间对于插件 ID 来说就足够了。
当在单个 JAR 构件中捆绑多个插件时,建议遵循相同的命名约定。这种做法有助于逻辑地分组相关插件。
在单个项目中可以定义和注册(通过不同的标识符)的插件数量没有限制。
以类形式编写的插件的标识符应在包含插件类的项目的构建脚本中定义。为此,需要应用 java-gradle-plugin
plugins {
id("java-gradle-plugin")
}
gradlePlugin {
plugins {
create("androidApplicationPlugin") {
id = "com.android.application"
implementationClass = "com.android.AndroidApplicationPlugin"
}
create("androidLibraryPlugin") {
id = "com.android.library"
implementationClass = "com.android.AndroidLibraryPlugin"
}
}
}
plugins {
id 'java-gradle-plugin'
}
gradlePlugin {
plugins {
androidApplicationPlugin {
id = 'com.android.application'
implementationClass = 'com.android.AndroidApplicationPlugin'
}
androidLibraryPlugin {
id = 'com.android.library'
implementationClass = 'com.android.AndroidLibraryPlugin'
}
}
}
文件操作
在开发插件时,对于接受文件位置的输入配置,保持灵活性是一个好主意。
建议使用 Gradle 的 managed properties 和 project.layout
来选择文件或目录位置。这将启用延迟配置,以便仅在需要文件时才解析实际位置,并且可以在构建配置期间随时重新配置。
此 Gradle 构建文件定义了一个 task GreetingToFileTask
,它将问候语写入文件。它还注册了两个 task:greet
,它创建包含问候语的文件;以及 sayGreeting
,它打印文件内容。greetingFile
属性用于指定问候语的文件路径
abstract class GreetingToFileTask : DefaultTask() {
@get:OutputFile
abstract val destination: RegularFileProperty
@TaskAction
fun greet() {
val file = destination.get().asFile
file.parentFile.mkdirs()
file.writeText("Hello!")
}
}
val greetingFile = objects.fileProperty()
tasks.register<GreetingToFileTask>("greet") {
destination = greetingFile
}
tasks.register("sayGreeting") {
dependsOn("greet")
val greetingFile = greetingFile
doLast {
val file = greetingFile.get().asFile
println("${file.readText()} (file: ${file.name})")
}
}
greetingFile = layout.buildDirectory.file("hello.txt")
abstract class GreetingToFileTask extends DefaultTask {
@OutputFile
abstract RegularFileProperty getDestination()
@TaskAction
def greet() {
def file = getDestination().get().asFile
file.parentFile.mkdirs()
file.write 'Hello!'
}
}
def greetingFile = objects.fileProperty()
tasks.register('greet', GreetingToFileTask) {
destination = greetingFile
}
tasks.register('sayGreeting') {
dependsOn greet
doLast {
def file = greetingFile.get().asFile
println "${file.text} (file: ${file.name})"
}
}
greetingFile = layout.buildDirectory.file('hello.txt')
$ gradle -q sayGreeting Hello! (file: hello.txt)
在此示例中,我们将 greet
task 的 destination
属性配置为闭包/provider,它使用 Project.file(java.lang.Object) 方法进行评估,以在最后一刻将闭包/provider 的返回值转换为 File
对象。请注意,我们在 task 配置之后指定了 greetingFile
属性值。这种延迟评估是接受任何值来设置文件属性,然后在读取属性时解析该值的关键好处。
您可以在 文件操作 中了解有关延迟文件操作的更多信息。
使用扩展使插件可配置
大多数插件为构建脚本和其他插件提供配置选项,以自定义插件的工作方式。插件使用扩展对象来做到这一点。
Project 有一个关联的 ExtensionContainer 对象,其中包含已应用于项目的插件的所有设置和属性。您可以通过将扩展对象添加到此容器来为您的插件提供配置。
扩展对象只是一个具有 Java Bean 属性的对象,表示配置。
让我们向项目添加一个 greeting
扩展对象,它允许您配置问候语
interface GreetingPluginExtension {
val message: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Add the 'greeting' extension object
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
// Add a task that uses configuration from the extension object
project.task("hello") {
doLast {
println(extension.message.get())
}
}
}
}
apply<GreetingPlugin>()
// Configure the extension
the<GreetingPluginExtension>().message = "Hi from Gradle"
interface GreetingPluginExtension {
Property<String> getMessage()
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
def extension = project.extensions.create('greeting', GreetingPluginExtension)
// Add a task that uses configuration from the extension object
project.task('hello') {
doLast {
println extension.message.get()
}
}
}
}
apply plugin: GreetingPlugin
// Configure the extension
greeting.message = 'Hi from Gradle'
$ gradle -q hello Hi from Gradle
在此示例中,GreetingPluginExtension
是一个具有名为 message
的属性的对象。扩展对象以名称 greeting
添加到项目中。此对象作为项目属性可用,其名称与扩展对象相同。the<GreetingPluginExtension>()
等同于 project.extensions.getByType(GreetingPluginExtension::class.java)
。
通常,您需要在单个插件上指定几个相关的属性。Gradle 为每个扩展对象添加一个配置块,因此您可以对设置进行分组
interface GreetingPluginExtension {
val message: Property<String>
val greeter: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
project.task("hello") {
doLast {
println("${extension.message.get()} from ${extension.greeter.get()}")
}
}
}
}
apply<GreetingPlugin>()
// Configure the extension using a DSL block
configure<GreetingPluginExtension> {
message = "Hi"
greeter = "Gradle"
}
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'
}
$ gradle -q hello Hi from Gradle
在此示例中,多个设置可以在 configure<GreetingPluginExtension>
代码块中分组。 configure
函数用于配置扩展对象。它提供了一种方便的方式来设置属性或将配置应用于这些对象。构建脚本的 configure
函数 (GreetingPluginExtension
) 中使用的类型必须与扩展类型匹配。然后,当代码块执行时,代码块的接收者是扩展。
在此示例中,多个设置可以在 greeting
闭包中分组。构建脚本中闭包代码块的名称 (greeting
) 必须与扩展对象名称匹配。然后,当闭包执行时,扩展对象上的字段将基于标准的 Groovy 闭包委托特性映射到闭包中的变量。
声明 DSL 配置容器
使用扩展对象扩展了 Gradle DSL,为插件添加了项目属性和 DSL 代码块。由于扩展对象是常规对象,您可以通过向扩展对象添加属性和方法,在插件代码块内部提供您自己的嵌套 DSL。
让我们考虑以下构建脚本以进行说明。
plugins {
id("org.myorg.server-env")
}
environments {
create("dev") {
url = "https://127.0.0.1:8080"
}
create("staging") {
url = "http://staging.enterprise.com"
}
create("production") {
url = "http://prod.enterprise.com"
}
}
plugins {
id 'org.myorg.server-env'
}
environments {
dev {
url = 'https://127.0.0.1:8080'
}
staging {
url = 'http://staging.enterprise.com'
}
production {
url = 'http://prod.enterprise.com'
}
}
插件公开的 DSL 公开了一个容器,用于定义一组环境。用户配置的每个环境都有一个任意但声明性的名称,并用其自己的 DSL 配置代码块表示。上面的示例实例化了开发、暂存和生产环境,包括其各自的 URL。
每个环境都必须在代码中具有数据表示形式以捕获值。环境的名称是不可变的,可以作为构造函数参数传入。目前,数据对象存储的唯一其他参数是 URL。
以下 ServerEnvironment
对象满足这些要求
abstract public class ServerEnvironment {
private final String name;
@javax.inject.Inject
public ServerEnvironment(String name) {
this.name = name;
}
public String getName() {
return name;
}
abstract public Property<String> getUrl();
}
Gradle 公开了工厂方法 ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory) 来创建数据对象的容器。该方法接受的参数是表示数据的类。可以通过将其添加到具有特定名称的扩展容器中,将创建的 NamedDomainObjectContainer 类型的实例公开给最终用户。
插件通常会在插件实现中后处理捕获的值,例如,配置 task
public class ServerEnvironmentPlugin implements Plugin<Project> {
@Override
public void apply(final Project project) {
ObjectFactory objects = project.getObjects();
NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer =
objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
project.getExtensions().add("environments", serverEnvironmentContainer);
serverEnvironmentContainer.all(serverEnvironment -> {
String env = serverEnvironment.getName();
String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
String taskName = "deployTo" + capitalizedServerEnv;
project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(serverEnvironment.getUrl()));
});
}
}
在上面的示例中,为每个用户配置的环境动态创建了一个部署 task。
您可以在 开发自定义 Gradle 类型 中找到有关实现项目扩展的更多信息。
建模类似 DSL 的 API
插件公开的 DSL 应该是可读且易于理解的。
例如,让我们考虑插件提供的以下扩展。在其当前形式中,它提供了一个“扁平”的属性列表,用于配置网站的创建
plugins {
id("org.myorg.site")
}
site {
outputDir = layout.buildDirectory.file("mysite")
websiteUrl = "https://gradle.org.cn"
vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
plugins {
id 'org.myorg.site'
}
site {
outputDir = layout.buildDirectory.file("mysite")
websiteUrl = 'https://gradle.org.cn'
vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}
随着公开属性数量的增加,您应该引入嵌套的、更具表现力的结构。
以下代码片段添加了一个名为 siteInfo
的新配置块,作为扩展的一部分。这更强烈地表明了这些属性的含义
plugins {
id("org.myorg.site")
}
site {
outputDir = layout.buildDirectory.file("mysite")
siteInfo {
websiteUrl = "https://gradle.org.cn"
vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
}
plugins {
id 'org.myorg.site'
}
site {
outputDir = layout.buildDirectory.file("mysite")
siteInfo {
websiteUrl = 'https://gradle.org.cn'
vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}
}
为此类扩展实现后备对象很简单。首先,引入一个新的数据对象来管理属性 websiteUrl
和 vcsUrl
abstract public class SiteInfo {
abstract public Property<String> getWebsiteUrl();
abstract public Property<String> getVcsUrl();
}
在扩展中,创建 siteInfo
类的实例和一个将捕获的值委托给数据实例的方法。
要配置底层数据对象,请定义 Action 类型的参数。
以下示例演示了在扩展定义中使用 Action
abstract public class SiteExtension {
abstract public RegularFileProperty getOutputDir();
@Nested
abstract public SiteInfo getSiteInfo();
public void siteInfo(Action<? super SiteInfo> action) {
action.execute(getSiteInfo());
}
}
将扩展属性映射到 task 属性
插件通常使用扩展来捕获来自构建脚本的用户输入,并将其映射到自定义 task 的输入/输出属性。构建脚本作者与扩展的 DSL 交互,而插件实现处理底层逻辑
// Extension class to capture user input
class MyExtension {
@Input
var inputParameter: String? = null
}
// Custom task that uses the input from the extension
class MyCustomTask : org.gradle.api.DefaultTask() {
@Input
var inputParameter: String? = null
@TaskAction
fun executeTask() {
println("Input parameter: $inputParameter")
}
}
// Plugin class that configures the extension and task
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create and configure the extension
val extension = project.extensions.create("myExtension", MyExtension::class.java)
// Create and configure the custom task
project.tasks.register("myTask", MyCustomTask::class.java) {
group = "custom"
inputParameter = extension.inputParameter
}
}
}
// Extension class to capture user input
class MyExtension {
@Input
String inputParameter = null
}
// Custom task that uses the input from the extension
class MyCustomTask extends DefaultTask {
@Input
String inputParameter = null
@TaskAction
def executeTask() {
println("Input parameter: $inputParameter")
}
}
// Plugin class that configures the extension and task
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
// Create and configure the extension
def extension = project.extensions.create("myExtension", MyExtension)
// Create and configure the custom task
project.tasks.register("myTask", MyCustomTask) {
group = "custom"
inputParameter = extension.inputParameter
}
}
}
在此示例中,MyExtension
类定义了一个可以在构建脚本中设置的 inputParameter
属性。MyPlugin
类配置此扩展并使用其 inputParameter
值来配置 MyCustomTask
task。然后,MyCustomTask
task 在其逻辑中使用此输入参数。
您可以在 延迟配置 中了解有关可以在 task 实现和扩展中使用的类型的更多信息。
使用约定添加默认配置
插件应在特定上下文中提供合理的默认值和标准,从而减少用户需要做出的决策数量。使用 project
对象,您可以定义默认值。这些被称为约定。
约定是使用默认值初始化的属性,用户可以在其构建脚本中覆盖这些属性。例如
interface GreetingPluginExtension {
val message: Property<String>
}
class GreetingPlugin : Plugin<Project> {
override fun apply(project: Project) {
// Add the 'greeting' extension object
val extension = project.extensions.create<GreetingPluginExtension>("greeting")
extension.message.convention("Hello from GreetingPlugin")
// Add a task that uses configuration from the extension object
project.task("hello") {
doLast {
println(extension.message.get())
}
}
}
}
apply<GreetingPlugin>()
interface GreetingPluginExtension {
Property<String> getMessage()
}
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
def extension = project.extensions.create('greeting', GreetingPluginExtension)
extension.message.convention('Hello from GreetingPlugin')
// Add a task that uses configuration from the extension object
project.task('hello') {
doLast {
println extension.message.get()
}
}
}
}
apply plugin: GreetingPlugin
$ gradle -q hello Hello from GreetingPlugin
在此示例中,GreetingPluginExtension
是一个表示约定的类。message 属性是约定属性,默认值为 'Hello from GreetingPlugin'。
用户可以在其构建脚本中覆盖此值
GreetingPluginExtension {
message = "Custom message"
}
GreetingPluginExtension {
message = 'Custom message'
}
$ gradle -q hello
Custom message
将能力与约定分离
在插件中将能力与约定分离允许用户选择要应用哪些 task 和约定。
例如,Java Base 插件提供非倾向性(即,通用)功能,如 SourceSets
,而 Java 插件添加 Java 开发人员熟悉的 task 和约定,如 classes
、jar
或 javadoc
。
在设计您自己的插件时,请考虑开发两个插件 - 一个用于能力,另一个用于约定 - 以向用户提供灵活性。
在下面的示例中,MyPlugin
包含约定,而 MyBasePlugin
定义能力。然后,MyPlugin
应用 MyBasePlugin
,这被称为插件组合。要从另一个插件应用插件
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyBasePlugin implements Plugin<Project> {
public void apply(Project project) {
// define capabilities
}
}
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class MyPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getPluginManager().apply(MyBasePlugin.class);
// define conventions
}
}
对插件做出反应
Gradle 插件实现中的一种常见模式是配置构建中现有插件和 task 的运行时行为。
例如,插件可以假设它应用于基于 Java 的项目,并自动重新配置标准源目录
public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
public void apply(Project project) {
// Careful! Eagerly appyling plugins has downsides, and is not always recommended.
project.getPluginManager().apply(JavaPlugin.class);
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
main.getJava().setSrcDirs(Arrays.asList("src"));
}
}
这种方法的缺点是它会自动强制项目应用 Java 插件,对其施加强烈的倾向性(即,降低灵活性和通用性)。在实践中,应用插件的项目甚至可能不处理 Java 代码。
插件可以对使用项目应用 Java 插件这一事实做出反应,而不是自动应用 Java 插件。只有在这种情况下,才会应用某些配置
public class InhouseConventionJavaPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getPluginManager().withPlugin("java", javaPlugin -> {
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
main.getJava().setSrcDirs(Arrays.asList("src"));
});
}
}
如果没有充分的理由假设使用项目具有预期的设置,则对插件做出反应优于应用插件。
相同的概念适用于 task 类型
public class InhouseConventionWarPlugin implements Plugin<Project> {
public void apply(Project project) {
project.getTasks().withType(War.class).configureEach(war ->
war.setWebXml(project.file("src/someWeb.xml")));
}
}
对构建特性做出反应
插件可以访问构建中构建特性的状态。Build Features API 允许检查用户是否请求了特定的 Gradle 特性,以及它是否在当前构建中处于活动状态。构建特性的一个例子是 配置缓存。
主要有两种用例
-
在报告或统计信息中使用构建特性的状态。
-
通过禁用不兼容的插件功能,逐步采用实验性的 Gradle 特性。
下面是一个插件示例,它利用了这两种情况。
public abstract class MyPlugin implements Plugin<Project> {
@Inject
protected abstract BuildFeatures getBuildFeatures(); (1)
@Override
public void apply(Project p) {
BuildFeatures buildFeatures = getBuildFeatures();
Boolean configCacheRequested = buildFeatures.getConfigurationCache().getRequested() (2)
.getOrNull(); // could be null if user did not opt in nor opt out
String configCacheUsage = describeFeatureUsage(configCacheRequested);
MyReport myReport = new MyReport();
myReport.setConfigurationCacheUsage(configCacheUsage);
boolean isolatedProjectsActive = buildFeatures.getIsolatedProjects().getActive() (3)
.get(); // the active state is always defined
if (!isolatedProjectsActive) {
myOptionalPluginLogicIncompatibleWithIsolatedProjects();
}
}
private String describeFeatureUsage(Boolean requested) {
return requested == null ? "no preference" : requested ? "opt-in" : "opt-out";
}
private void myOptionalPluginLogicIncompatibleWithIsolatedProjects() {
}
}
1 | BuildFeatures 服务可以注入到插件、task 和其他托管类型中。 |
2 | 访问特性的 requested 状态以进行报告。 |
3 | 使用特性的 active 状态来禁用不兼容的功能。 |
构建特性属性
BuildFeature
状态属性用 Provider<Boolean>
类型表示。
构建特性的 BuildFeature.getRequested()
状态确定用户是否请求启用或禁用该特性。
当 requested
provider 值为
-
true
— 用户选择使用该特性 -
false
— 用户选择不使用该特性 -
undefined
— 用户既没有选择使用也没有选择不使用该特性
构建特性的 BuildFeature.getActive()
状态始终是已定义的。它表示特性在构建中的有效状态。
当 active
provider 值为
-
true
— 该特性可能会以特定于该特性的方式影响构建行为 -
false
— 该特性不会影响构建行为
请注意,active
状态不依赖于 requested
状态。即使用户请求了特性,由于构建中使用的其他构建选项,它也可能仍然不活动。即使用户未指定首选项,Gradle 也可以默认激活特性。
使用自定义 dependencies
代码块
插件可以在自定义代码块中提供依赖声明,从而允许用户以类型安全和上下文感知的方式声明依赖。
例如,自定义 dependencies
代码块允许插件选择一个有意义的名称,该名称可以一致地使用,而不是用户需要知道和使用底层的 Configuration
名称来添加依赖。
添加自定义 dependencies
代码块
要添加自定义 dependencies
代码块,您需要创建一个新的 类型,它将表示用户可用的依赖范围集。新类型需要可以从插件的一部分(从域对象或扩展)访问。最后,依赖范围需要连接回将在依赖解析期间使用的底层 Configuration
对象。
有关如何在 Gradle 核心插件中使用此功能的示例,请参阅 JvmComponentDependencies 和 JvmTestSuite。
1. 创建一个扩展 Dependencies
的接口
您还可以扩展 GradleDependencies 以访问 Gradle 提供的依赖,如 gradleApi() 。 |
/**
* Custom dependencies block for the example plugin.
*/
public interface ExampleDependencies extends Dependencies {
2. 为依赖范围添加访问器
对于您的插件要支持的每个依赖范围,添加一个返回 DependencyCollector
的 getter 方法。
/**
* Dependency scope called "implementation"
*/
DependencyCollector getImplementation();
3. 为自定义 dependencies
代码块添加访问器
为了使自定义 dependencies
代码块可配置,插件需要添加一个返回上面新类型的 getDependencies
方法和一个名为 dependencies
的可配置代码块方法。
按照惯例,您的自定义 dependencies
代码块的访问器应称为 getDependencies()
/dependencies(Action)
。此方法可以命名为其他名称,但用户需要知道不同的代码块可以像 dependencies
代码块一样工作。
/**
* Custom dependencies for this extension.
*/
@Nested
ExampleDependencies getDependencies();
/**
* Configurable block
*/
default void dependencies(Action<? super ExampleDependencies> action) {
action.execute(getDependencies());
}
4. 将依赖范围连接到 Configuration
最后,插件需要将自定义 dependencies
代码块连接到一些底层的 Configuration
对象。如果未完成此操作,则自定义代码块中声明的任何依赖都将不可用于依赖解析。
project.getConfigurations().dependencyScope("exampleImplementation", conf -> {
conf.fromDependencyCollector(example.getDependencies().getImplementation());
});
在此示例中,用户将用于添加依赖的名称是 “implementation”,但底层的 Configuration 被命名为 exampleImplementation 。 |
example {
dependencies {
implementation("junit:junit:4.13")
}
}
example {
dependencies {
implementation("junit:junit:4.13")
}
}
自定义 dependencies
代码块与顶级 dependencies
代码块之间的差异
每个依赖范围都返回一个 DependencyCollector
,它提供强类型方法来添加和配置依赖。
还有一个 DependencyFactory
,它具有工厂方法,可从不同的表示法创建新依赖。可以使用这些工厂方法延迟创建依赖,如下所示。
自定义 dependencies
代码块与顶级 dependencies
代码块的不同之处在于以下几点
-
必须使用
String
、Dependency
的实例、FileCollection
、Dependency
的Provider
或MinimalExternalModuleDependency
的ProviderConvertible
来声明依赖。 -
在 Gradle 构建脚本之外,您必须显式调用
DependencyCollector
的 getter 和add
。-
dependencies.add(\"implementation\", x)
变为getImplementation().add(x)
-
-
您不能使用 Kotlin 和 Java 中的
Map
表示法声明依赖。请在 Kotlin 和 Java 中改用多参数方法。-
Kotlin:
compileOnly(mapOf(\"group\" to \"foo\", \"name\" to \"bar\"))
变为compileOnly(module(group = \"foo\", name = \"bar\"))
-
Java:
compileOnly(Map.of(\"group\", \"foo\", \"name\", \"bar\"))
变为getCompileOnly().add(module(\"foo\", \"bar\", null))
-
-
您不能使用
Project
的实例添加依赖。您必须先将其转换为ProjectDependency
。 -
您不能直接添加版本目录 bundle。请改为在每个配置上使用
bundle
方法。-
Kotlin 和 Groovy:
implementation(libs.bundles.testing)
变为implementation.bundle(libs.bundles.testing)
-
-
您不能直接为非
Dependency
类型使用 providers。而是使用DependencyFactory
将它们映射到Dependency
。-
Kotlin 和 Groovy:
implementation(myStringProvider)
变为implementation(myStringProvider.map { dependencyFactory.create(it) })
-
Java:
implementation(myStringProvider)
变为getImplementation().add(myStringProvider.map(getDependencyFactory()::create)
-
-
与顶层
dependencies
代码块不同,constraints 不在单独的代码块中。-
而是通过使用
constraint(…)
装饰依赖项来添加 constraints,例如implementation(constraint("org:foo:1.0"))
。
-
请记住,dependencies
代码块可能无法提供与 顶层 dependencies
代码块 相同的方法。
插件应首选通过它们自己的 dependencies 代码块添加依赖项。 |
提供默认依赖项
插件的实现有时需要使用外部依赖项。
您可能希望使用 Gradle 的依赖管理机制自动下载一个 artifact,然后在插件中声明的任务类型的 action 中使用它。理想情况下,插件实现不需要用户询问该依赖项的坐标 - 它可以简单地预定义一个合理的默认版本。
让我们看一个插件示例,该插件下载包含数据的文件以进行进一步处理。插件实现声明了一个自定义配置,该配置允许使用依赖项坐标分配这些外部依赖项
public class DataProcessingPlugin implements Plugin<Project> {
public void apply(Project project) {
Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
c.setVisible(false);
c.setCanBeConsumed(false);
c.setCanBeResolved(true);
c.setDescription("The data artifacts to be processed for this plugin.");
c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
});
project.getTasks().withType(DataProcessing.class).configureEach(
dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
}
}
abstract public class DataProcessing extends DefaultTask {
@InputFiles
abstract public ConfigurableFileCollection getDataFiles();
@TaskAction
public void process() {
System.out.println(getDataFiles().getFiles());
}
}
这种方法对于最终用户来说很方便,因为无需主动声明依赖项。插件已经提供了有关此实现的所有详细信息。
但是,如果用户想要重新定义默认依赖项怎么办?
没问题。插件还公开了自定义配置,该配置可用于分配不同的依赖项。实际上,默认依赖项被覆盖了
plugins {
id("org.myorg.data-processing")
}
dependencies {
dataFiles("org.myorg:more-data:2.6")
}
plugins {
id 'org.myorg.data-processing'
}
dependencies {
dataFiles 'org.myorg:more-data:2.6'
}
您会发现这种模式非常适用于需要在执行任务的 action 时使用外部依赖项的任务。您可以更进一步,通过公开扩展属性(例如 JaCoCo 插件 中的 toolVersion
)来抽象用于外部依赖项的版本。
最大限度地减少外部库的使用
在您的 Gradle 项目中使用外部库可以带来极大的便利,但请注意,它们可能会引入复杂的依赖关系图。Gradle 的 buildEnvironment
任务可以帮助您可视化这些依赖关系,包括插件的依赖关系。请记住,插件共享同一个类加载器,因此同一库的不同版本可能会发生冲突。
为了演示,我们假设以下构建脚本
plugins {
id("org.asciidoctor.jvm.convert") version "4.0.2"
}
plugins {
id 'org.asciidoctor.jvm.convert' version '4.0.2'
}
任务的输出清楚地表明了 classpath
配置的类路径
$ gradle buildEnvironment > Task :buildEnvironment ------------------------------------------------------------ Root project 'external-libraries' ------------------------------------------------------------ classpath \--- org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:4.0.2 \--- org.asciidoctor:asciidoctor-gradle-jvm:4.0.2 +--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 | \--- org.tukaani:xz:1.6 +--- org.ysb33r.gradle:grolifant-herd:3.0.0 | +--- org.tukaani:xz:1.6 | +--- org.ysb33r.gradle:grolifant40:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.apache.commons:commons-collections4:4.4 | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 | | | +--- org.tukaani:xz:1.6 | | | +--- org.apache.commons:commons-collections4:4.4 | | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant50:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant40-legacy-api:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.apache.commons:commons-collections4:4.4 | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant60:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant70:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant60:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant80:3.0.0 | | +--- org.tukaani:xz:1.6 | | +--- org.ysb33r.gradle:grolifant40:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant50:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant60:3.0.0 (*) | | +--- org.ysb33r.gradle:grolifant70:3.0.0 (*) | | \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*) | \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*) +--- org.asciidoctor:asciidoctor-gradle-base:4.0.2 | \--- org.ysb33r.gradle:grolifant-herd:3.0.0 (*) \--- org.asciidoctor:asciidoctorj-api:2.5.7 (*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation. A web-based, searchable dependency report is available by adding the --scan option. BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed
Gradle 插件不在其自己的隔离类加载器中运行,因此您必须考虑是否真的需要库,或者更简单的解决方案是否足够。
对于作为任务执行一部分执行的逻辑,请使用 Worker API,它允许您隔离库。
提供插件的多个变体
插件的变体指的是插件的不同风格或配置,这些风格或配置是为特定需求或用例量身定制的。这些变体可以包括基本插件的不同实现、扩展或配置。
配置其他插件变体最方便的方法是使用 feature variants,这是所有应用 Java 插件之一的 Gradle 项目中都可用的概念
dependencies {
implementation 'com.google.guava:guava:30.1-jre' // Regular dependency
featureVariant 'com.google.guava:guava-gwt:30.1-jre' // Feature variant dependency
}
在以下示例中,每个插件变体都是独立开发的。为每个变体编译单独的 source set 并将其打包到单独的 jar 中。
以下示例演示了如何添加与 Gradle 7.0+ 兼容的变体,而“main”变体与旧版本兼容
val gradle7 = sourceSets.create("gradle7")
java {
registerFeature(gradle7.name) {
usingSourceSet(gradle7)
capability(project.group.toString(), project.name, project.version.toString()) (1)
}
}
configurations.configureEach {
if (isCanBeConsumed && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
objects.named("7.0"))
}
}
}
tasks.named<Copy>(gradle7.processResourcesTaskName) { (3)
val copyPluginDescriptors = rootSpec.addChild()
copyPluginDescriptors.into("META-INF/gradle-plugins")
copyPluginDescriptors.from(tasks.pluginDescriptors)
}
dependencies {
"gradle7CompileOnly"(gradleApi()) (4)
}
def gradle7 = sourceSets.create('gradle7')
java {
registerFeature(gradle7.name) {
usingSourceSet(gradle7)
capability(project.group.toString(), project.name, project.version.toString()) (1)
}
}
configurations.configureEach {
if (canBeConsumed && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
objects.named(GradlePluginApiVersion, '7.0'))
}
}
}
tasks.named(gradle7.processResourcesTaskName) { (3)
def copyPluginDescriptors = rootSpec.addChild()
copyPluginDescriptors.into('META-INF/gradle-plugins')
copyPluginDescriptors.from(tasks.pluginDescriptors)
}
dependencies {
gradle7CompileOnly(gradleApi()) (4)
}
只有 Gradle 7 或更高版本才能显式地被变体作为目标,因为对此的支持仅在 Gradle 7 中添加。 |
首先,我们为我们的 Gradle 7 插件变体声明一个单独的 source set 和一个 feature variant。然后,我们进行一些特定的连接,将该 feature 变成一个合适的 Gradle 插件变体
1 | 将 与组件 GAV 相对应的隐式 capability 分配给该变体。 |
2 | 将 Gradle API 版本属性 分配给我们 Gradle7 变体的所有 可消费的配置。Gradle 使用此信息来确定在插件解析期间选择哪个变体。 |
3 | 配置 processGradle7Resources 任务以确保插件描述符文件被添加到 Gradle7 变体 Jar 中。 |
4 | 为我们的新变体添加对 gradleApi() 的依赖,以便在编译时 API 可见。 |
请注意,目前没有方便的方法可以访问您正在构建插件所用的 Gradle 版本的 API。理想情况下,每个变体都应该能够声明对其支持的最低 Gradle 版本的 API 的依赖。这将在未来得到改进。
上面的代码片段假设您的插件的所有变体都具有相同位置的插件类。也就是说,如果您的插件类是 org.example.GreetingPlugin
,您需要在 src/gradle7/java/org/example
中创建该类的第二个变体。
使用多变体插件的版本特定变体
给定对多变体插件的依赖,当 Gradle 解析以下任何内容时,它将自动选择最匹配当前 Gradle 版本的变体:
-
在
plugins {}
代码块 中指定的插件; -
buildscript
类路径依赖项; -
出现在编译或运行时类路径上的 build source (
buildSrc
) 的根项目中的依赖项; -
在应用了 Java Gradle Plugin Development plugin 或 Kotlin DSL plugin 的项目中,出现在编译或运行时类路径上的依赖项。
最佳匹配变体是目标 Gradle API 版本最高且不超过当前构建的 Gradle 版本的变体。
在所有其他情况下,如果存在不指定支持的 Gradle API 版本的插件变体,则首选该变体。
在使用插件作为依赖项的项目中,可以请求插件依赖项的支持不同 Gradle 版本的变体。这允许依赖于其他插件的多变体插件访问其 API,这些 API 专门在其版本特定的变体中提供。
此代码片段使 上面定义的插件变体 gradle7
使用其对其他多变体插件的依赖项的匹配变体
configurations.configureEach {
if (isCanBeResolved && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
objects.named("7.0"))
}
}
}
configurations.configureEach {
if (canBeResolved && name.startsWith(gradle7.name)) {
attributes {
attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
objects.named(GradlePluginApiVersion, '7.0'))
}
}
}