实现二进制插件
二进制插件(Binary plugins)是指被编译并打包为 JAR 文件的插件。这些插件通常使用 Java 或 Kotlin 编写,并为 Gradle 构建提供自定义功能或任务。
使用插件开发插件
Gradle 插件开发插件 可用于辅助开发 Gradle 插件。
此插件将自动应用 Java 插件,将 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'
}
}
}
创建插件 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 构建文件定义了一个任务 GreetingToFileTask
,它将问候语写入文件。它还注册了两个任务: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
任务的 destination
属性配置为一个闭包/提供者(closure/provider),它使用 Project.file(java.lang.Object) 方法进行评估,以便在最后一刻将闭包/提供者的返回值转换为 File
对象。请注意,我们在任务配置之后指定了 greetingFile
属性值。这种延迟评估是在设置文件属性时接受任何值并在读取属性时解析该值的一个关键优势。
您可以在处理文件中了解更多关于延迟处理文件的信息。
使用扩展使插件可配置
大多数插件为构建脚本和其他插件提供配置选项,以自定义插件的工作方式。插件通过使用 扩展对象(extension objects) 来实现这一点。
一个 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 = "http://localhost:8080"
}
create("staging") {
url = "http://staging.enterprise.com"
}
create("production") {
url = "http://prod.enterprise.com"
}
}
plugins {
id 'org.myorg.server-env'
}
environments {
dev {
url = 'http://localhost:8080'
}
staging {
url = 'http://staging.enterprise.com'
}
production {
url = 'http://prod.enterprise.com'
}
}
插件公开的 DSL 提供了一个容器,用于定义一组环境。用户配置的每个环境都有一个任意但具声明性的名称,并由其自己的 DSL 配置块表示。上面的示例实例化了开发、staging 和生产环境,包括它们各自的 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 类型的创建实例可以通过指定名称添加到扩展容器中,从而暴露给最终用户。
插件实现中对捕获的值进行后处理是很常见的,例如,配置任务
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()));
});
}
}
在上面的示例中,为每个用户配置的环境动态创建了一个部署任务。
您可以在开发自定义 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());
}
}
将扩展属性映射到任务属性
插件通常使用扩展来捕获构建脚本中的用户输入,并将其映射到自定义任务的输入/输出属性。构建脚本作者与扩展的 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
任务。然后 MyCustomTask
任务在其逻辑中使用此输入参数。
您可以在延迟配置中了解更多关于可以在任务实现和扩展中使用的类型的信息。
使用约定添加默认配置
插件应在特定上下文中提供合理的默认值和标准,减少用户需要做出的决策数量。使用 project
对象,您可以定义默认值。这些被称为 约定(conventions)。
约定是使用默认值初始化的属性,并且可以在用户的构建脚本中被覆盖。例如
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
分离能力和约定
在插件中分离能力和约定,允许用户选择应用哪些任务和约定。
例如,Java Base 插件提供无偏好(即,通用)的功能,例如 SourceSets
,而 Java 插件添加了 Java 开发人员熟悉的人物和约定,例如 classes
、jar
或 javadoc
。
设计您自己的插件时,考虑开发两个插件——一个用于能力,另一个用于约定——以为用户提供灵活性。
在下面的示例中,MyPlugin
包含约定,而 MyBasePlugin
定义能力。然后,MyPlugin
应用 MyBasePlugin
,这称为插件组合(plugin composition)。要在另一个插件中应用插件
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 插件实现中的一个常见模式是配置构建中现有插件和任务的运行时行为。
例如,一个插件可能会假设它被应用到基于 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"));
});
}
}
如果无法合理地假设消费项目具有预期的设置,那么响应插件优于应用插件。
同样的概念适用于任务类型
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 服务可以注入到插件、任务和其他托管类型中。 |
2 | 访问特性的 requested 状态用于报告。 |
3 | 使用特性的 active 状态来禁用不兼容的功能。 |
构建特性属性
BuildFeature
状态属性由 Provider<Boolean>
类型表示。
构建特性的 BuildFeature.getRequested()
状态决定了用户是否请求启用或禁用该特性。
当 requested
提供者(provider)的值是
-
true
— 用户选择启用该特性 -
false
— 用户选择禁用该特性 -
undefined
— 用户既未选择启用也未选择禁用该特性
构建特性的 BuildFeature.getActive()
状态总是已定义的。它代表了该特性在构建中的实际状态。
当 active
提供者(provider)的值是
-
true
— 该特性可能以特定于该特性的方式影响构建行为 -
false
— 该特性不会影响构建行为
请注意,active
状态不依赖于 requested
状态。即使用户请求某项特性,由于构建中使用了其他构建选项,该特性仍可能不会处于活动状态。即使用户未指定偏好,Gradle 也可以默认启用某项特性。
使用自定义的 dependencies
代码块
插件可以在自定义代码块中提供依赖声明,这样用户就可以以类型安全和上下文感知的方式声明依赖项。
例如,用户无需知道并使用底层 Configuration
名称来添加依赖项,自定义的 dependencies
代码块允许插件选择一个可以一致使用的有意义的名称。
添加自定义的 dependencies
代码块
要添加自定义的 dependencies
代码块,您需要创建一种新的类型,该类型将表示可供用户使用的一组依赖范围(dependency scopes)。该新类型需要能从插件的一部分(域对象或扩展)访问到。最后,需要将依赖范围映射回在依赖解析期间使用的底层 Configuration
对象。
请参阅 JvmComponentDependencies 和 JvmTestSuite,了解此功能在 Gradle 核心插件中的使用示例。
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
代码块不同,约束不在单独的代码块中。-
相反,通过使用
constraint(…)
来装饰依赖项来添加约束,例如implementation(constraint("org:foo:1.0"))
。
-
请记住,此 dependencies
代码块可能无法访问与顶级 dependencies
代码块相同的方法。
插件应优先通过其自己的 dependencies 代码块来添加依赖项。 |
提供默认依赖项
插件的实现有时需要使用外部依赖项。
您可能希望使用 Gradle 的依赖管理机制自动下载制品,并在稍后在插件中声明的任务类型的操作中使用它。理想情况下,插件实现无需询问用户该依赖项的坐标——它可以简单地预定义一个合理的默认版本。
让我们来看一个插件示例,该插件下载包含数据的文件以便进一步处理。插件实现声明了一个自定义配置,允许使用依赖坐标分配这些外部依赖项
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'
}
您会发现,当任务的操作执行时,此模式对于需要外部依赖项的任务非常有效。您可以进一步抽象外部依赖项要使用的版本,通过公开扩展属性(例如,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
}
在下面的示例中,每个插件变体都是独立开发的。为每个变体编译一个单独的源集并打包到一个单独的 jar 中。
以下示例演示了如何添加一个与 Gradle 7.0+ 兼容的变体,而“主”变体与旧版本兼容
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)。然后,我们进行一些特定的连接,将此特性转换为一个合适的 Gradle 插件变体
1 | 将与组件 GAV 对应的隐式能力(implicit capability)分配给变体。 |
2 | 将Gradle API 版本属性分配给 Gradle7 变体的所有可消费配置(consumable configurations)。Gradle 使用此信息来确定在插件解析期间选择哪个变体。 |
3 | 配置 processGradle7Resources 任务,以确保将插件描述符文件添加到 Gradle7 变体 Jar 中。 |
4 | 为我们的新变体添加对 gradleApi() 的依赖,以便在编译时 API 可见。 |
注意,目前没有方便的方法来访问与您构建插件所用 Gradle 版本不同的其他 Gradle 版本的 API。理想情况下,每个变体都应该能够声明对其支持的最小 Gradle 版本 API 的依赖。这将在未来得到改进。
上述代码片段假设您的插件的所有变体在同一位置具有插件类。也就是说,如果您的插件类是 org.example.GreetingPlugin
,则需要在 src/gradle7/java/org/example
中创建该类的第二个变体。
使用多变体插件的版本特定变体
如果依赖于多变体插件,Gradle 在解析以下任何项时会自动选择与其当前 Gradle 版本最匹配的变体:
-
在
plugins {}
块中指定的插件; -
buildscript
classpath 依赖项; -
构建源 (
buildSrc
) 根项目中出现在编译或运行时 classpath 中的依赖项; -
应用了 Java Gradle Plugin Development plugin 或 Kotlin DSL plugin 的项目中出现在编译或运行时 classpath 中的依赖项。
最匹配的变体是目标 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'))
}
}
}