发布于 2026-01-06 1 阅读
0

Gradle 插件和扩展:入门指南

Gradle 插件和扩展:入门指南

该项目的所有代码都已上传至GitHub。(GitHub 已移除 ICE。)

我最近突然意识到,并非所有人都像我一样虚度光阴,因此可能并不了解如何在 Gradle 插件中创建自定义嵌套 DSL。虽然这非常实用,但更重要的是,它非常美观。

// app/build.gradle
theState {
  theDeepState {
    theDeepestState {
      undermine 'the will of the people'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这证明 IDE 几乎可以为传统的 Groovy DSL 提供类型提示(稍后会详细介绍)。
Groovy DSL 的屏幕截图,展示了我们自定义 DSL 的 IDE 提示。

证明该 IDE 对 Kotlin DSL 的理解比 Groovy 版本更好
Kotlin DSL 的屏幕截图,展示了我们自定义 DSL 的 IDE 提示。

我截完那些图后不久,就把这个项目的 Gradle 版本从 7.1.1 升级到了 7.2,结果我的 IDE(IntelliJ IDEA Ultimate)就出问题了,不再给我 Groovy 脚本的 DSL 提示了。¯\_(ツ)_/¯

暂且不论我们为什么要破坏人民的意愿(我的意思是,这难道不显而易见吗?),我们该如何做到这一点呢?

这是给谁看的?

本文面向所有正在寻找 Gradle 插件设计基本构建模块之一的实用示例的人。我不会说这些示例已经可以用于生产环境(我肯定不会编写任何测试!),但我目前正在使用类似的技术构建一个超过 200 万行代码的应用程序,所以……

一种专为控制我们生活的秘密政府官僚机构而设计的领域特定语言

我们将首先了解扩展本身,然后反向推导其配置和使用方式,最后介绍如何声明和构建它。

// TheStateExtension.kt
package mutual.aid.gradle

import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import javax.inject.Inject

open class TheStateExtension @Inject constructor(
  objects: ObjectFactory
) {

  /** Configure the inner DSL object, [TheDeepStateHandler]. */
  val theDeepState: TheDeepStateHandler = objects.newInstance(TheDeepStateHandler::class.java)

  /** Configure the inner DSL object, [TheDeepStateHandler]. */
  fun theDeepState(action: Action<TheDeepStateHandler>) {
    action.execute(theDeepState)
  }

  companion object {
    fun Project.theState(): TheStateExtension {
      return extensions.create("theState", TheStateExtension::class.java)
    }
  }
}

/**
 * An inner DSL object.
 */
open class TheDeepStateHandler @Inject constructor(
  objects: ObjectFactory
) {

  /** Configure the innermost DSL object, [TheDeepestStateHandler]. */
  val theDeepestState: TheDeepestStateHandler = objects.newInstance(TheDeepestStateHandler::class.java)

  /** Configure the innermost DSL object, [TheDeepestStateHandler]. */
  fun theDeepestState(action: Action<TheDeepestStateHandler>) {
    action.execute(theDeepestState)
  }
}

/**
 * An even-more inner-er DSL object.
 */
open class TheDeepestStateHandler {

  private val whoToUndermine = mutableListOf<String>()
  internal val victims: List<String> get() = whoToUndermine.toList()

  /** Tells the app who - or which groups - it should undermine. */
  fun undermine(who: String) {
    whoToUndermine.add(who)
  }
}
Enter fullscreen mode Exit fullscreen mode

一些要点:

  1. 我喜欢给最外层的扩展类FooExtension和内部的DSL对象命名BarHandler。采用这样的约定可以更方便地在大型代码库中进行导航。
  2. 你可以使用各种服务(例如)注入所有这些类型ObjectFactory,以及你提供的任意对象。只需记住@Inject构造函数即可!
  3. 既然涉及到 Gradle 的底层实现,那就让 Gradle API 来帮你完成这些工作吧。别试图用 Groovy 闭包或者 Kotlin 的 lambda 表达式和接收器之类的花哨玩意儿——直接用 Gradle 的 API 接口就行了ObjectFactoryAction<T>稍后我会详细解释。
  4. 你可以直接公开处理程序(就像我在示例中所做的那样),也可以通过函数公开它们,这样你的用户就可以使用点表示法和带有花括号的 DSL 类语法。

实例化扩展

现在我们知道如何创建简单的内部DSL对象了。那么,如何创建和配置最外层的扩展呢?

// ThePluginOfOppression.kt
package mutual.aid.gradle

import mutual.aid.gradle.TheStateExtension.Companion.theState
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.JavaExec

class ThePluginOfOppression : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    val theState = theState()
  }
}
Enter fullscreen mode Exit fullscreen mode

请记住,这TheStateExtension.theState()是一个简单的伴随函数project.extensions.create("theState", TheStateExtension::class.java)。我喜欢将这个函数与类本身放在一起,作为工厂方法进行封装。另外需要注意的是,即使我没有使用theState创建的实例,我仍然需要在这里创建它,以便在应用此插件时,构建脚本可以访问它。接下来,我们先来看看它是如何工作的,然后再回头讨论如何使用用户提供的配置。

在构建脚本中应用插件并配置扩展程序

// app/build.gradle
plugins {
  id 'mutual.aid.oppression-plugin'
}

// 1: DSL-like
theState {
  theDeepState {
    theDeepestState {
      undermine 'the will of the people'
    }
  }
}

// 2: With dot-notation for the laconic
theState
  .theDeepState
  .theDeepestState
  .undermine 'the will of the people'

// 3: Mix and match
theState.theDeepState.theDeepestState {
  undermine 'the will of the people'
}
Enter fullscreen mode Exit fullscreen mode

很简单。应用插件并配置扩展程序。现在正好可以谈谈那些Action<T>启用 DSL 语法的函数。提醒一下,其中一个函数如下所示:

import org.gradle.api.Action

fun theDeepState(action: Action<TheDeepStateHandler>) {
  action.execute(theDeepState)
}
Enter fullscreen mode Exit fullscreen mode

我之所以在这些代码片段中保留导入语句,是因为需要明确指出我们这里使用的具体 API——是org.gradle.apiAPI!2 Gradle 对这类类型有特殊的处理方式。在(构建)运行时,Gradle会动态地(使用汇编语言)重写你的构建代码,使得方法签名theDeepState(action: Action<T>)实际上变为theDeepState(action: T.() -> Unit)。实际上,更准确的说法是,你可以同时获得这两种方式。在我的 Groovy DSL 脚本中,如果我愿意,也可以it.随意使用(但我并不这么做)。

现在我们知道为什么 IDE 的类型提示会遇到困难了:它看到了源代码,其中指定了一个标准的 SAM 接口;但它看不到动态提供的带有接收器的 lambda 表达式。

目前尚不清楚为什么使用 Kotlin DSL 会更好。3如果你查看生成类型安全访问器,它们也使用了Kotlin DSL Action<T>。我想我们永远也不会知道原因了。

利用用户提供的配置:我们今天应该压迫谁?

让我们回到插件定义,现在它已经扩展为使用用户在我们自定义 DSL 中提供的信息。

class ThePluginOfOppression : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    // 1: Apply additional plugins    
    pluginManager.apply("org.jetbrains.kotlin.jvm")
    pluginManager.apply("application")

    // 2: Create our extension
    val theState = theState()

    // 3: Wait for the DSL to be evaluated, and use the information provided
    afterEvaluate {
      tasks.named("run", JavaExec::class.java) {
        it.args = theState.theDeepState.theDeepestState.victims
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

1. 应用其他插件。虽然我们的插件并非必须应用这些其他插件,但这可以展示约定插件的多功能性,也有助于保持示例的封装性。2
. 创建扩展。与之前相同。3
. 使用用户提供的数据。有时无法使用ProviderAPI,需要等待用户数据——这正是我们afterEvaluate设计此扩展的目的。在本例中,我们将数据(即待处理的数据)推送到一个标准JavaExec任务中。

我们运行一下程序,看看会发生什么:

$ ./gradlew -q app:run
Now undermining: the will of the people
Enter fullscreen mode Exit fullscreen mode

压迫得逞!

领域对象容器

如果你是一名安卓开发者,你一定对这段 Gradle 配置很熟悉:

android {
  buildTypes {
    release { ... }
    debug { ... }
    myCustomBuildType { ... }
  }
}
Enter fullscreen mode Exit fullscreen mode

这些构建类型从何而来?我们现在知道如何生成和使用嵌套的 DSL 对象,但这些值是由用户提供的!当我们查看上述内容的 Kotlin DSL 版本时,情况就稍微清晰了一些:

android {
  buildTypes {
    getByName("release") { ... }
    getByName("debug") { ... }
    create("myCustomBuildType") { ... }
  }
}
Enter fullscreen mode Exit fullscreen mode

buildTypes是一个提供 . 的函数NamedDomainObjectContainer<BuildType>。Groovy 风格的 Gradle 提供了语法糖,如果该命名类型尚未创建,则会将其转换debug {}getByName("debug") {} OR 。在 Kotlin 中,您必须显式地指定。顺便说一句,这也是我了解到没有名为“release”的默认实例的原因。create("debug") {}signingConfig

我们现在大致了解了什么NamedDomainObjectContainer是 A。我们如何创建一个 A?我们如何从 A 获取新的实例?我们如何使用它?我们的用户如何使用它?

使用域对象容器

接下来,也是最后一个例子,我们换个角度来看。压迫很无聊;我们该如何提供帮助呢?

让我们从一个新的扩展开始ThePeopleExtension

package mutual.aid.gradle.people

import org.gradle.api.Action
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Property
import javax.inject.Inject

open class ThePeopleExtension @Inject constructor(objects: ObjectFactory) {

  val problems = objects.domainObjectContainer(ProblemHandler::class.java)

  fun problems(action: Action<NamedDomainObjectContainer<ProblemHandler>>) {
    action.execute(problems)
  }

  companion object {
    internal fun Project.thePeople(): ThePeopleExtension =
      extensions.create("thePeople", ThePeopleExtension::class.java)
  }
}

open class ProblemHandler @Inject constructor(
  private val name: String,
  objects: ObjectFactory
) : Named {

  override fun getName(): String = name

  internal val description: Property<String> = objects.property(String::class.java)
  val solutions = objects.domainObjectContainer(SolutionHandler::class.java)

  fun solutions(action: Action<NamedDomainObjectContainer<SolutionHandler>>) {
    action.execute(solutions)
  }

  fun description(description: String) {
    this.description.set(description)
    this.description.disallowChanges()
  }
}

open class SolutionHandler @Inject constructor(
  private val name: String,
  objects: ObjectFactory
) : Named {

  override fun getName(): String = name

  internal val action: Property<String> = objects.property(String::class.java)
  internal val description: Property<String> = objects.property(String::class.java)
  internal val rank: Property<Int> = objects.property(Int::class.java)

  fun action(action: String) {
    this.action.set(action)
    this.action.disallowChanges()
  }

  fun description(description: String) {
    this.description.set(description)
    this.description.disallowChanges()
  }

  fun rank(rank: Int) {
    this.rank.set(rank)
    this.rank.disallowChanges()
  }
}
Enter fullscreen mode Exit fullscreen mode

在继续之前,我们先来谈谈这里的一些模式。

首先,请注意,所有要放入命名域对象容器中的类型都必须NamedDomainObjectContainer实现该Named接口。虽然这不是绝对必要的,但这些类型必须具有函数getName(): String,否则它们无法放入命名域对象容器中。

其次,我们使用该方法创建这样的容器ObjectFactory.domainObjectContainer(Class<T>)

上述内容中最后一个有趣的规律是:

fun description(description: String) {
  this.description.set(action)
  this.description.disallowChanges()
}
Enter fullscreen mode Exit fullscreen mode

鉴于descriptionProperty<String>,我更倾向于保留这些值internal并通过函数将其公开。这样用户就可以使用类似description 'my descriptionGroovy 或description("my description")Kotlin 中那样简洁的 DSL。封装这些字段还允许我执行一些额外的操作disallowChanges(),例如调用 `get()` 方法,我认为这对于避免违反最小惊讶原则至关重要。如果没有封装,用户可以description()从多个位置重复调用该方法,而我们很难确定数据的真实来源。如果我们这样做,并且有人尝试多次调用该方法,则构建将会失败。

我们继续。这个DSL在实际应用中是什么样子呢?

// app/build.gradle
thePeople {
  problems {
    climateChange {
      description 'There is no question of cost, because the cost of doing nothing is everything.'
      solutions {
        cleanEnergy {
          description 'We cannot burn any more fossil energy'
          action 'Replace all fossil sources with clean solutions like wind, solar, and geothermal'
          rank 1
        }
        massTransit {
          description 'Single-occupant vehicles are a major source of carbon pollution'
          action 'Increase density in urban environments and build free public transit for all'
          rank 2
        }
        stopEatingAnimals {
          description 'Animal agriculture is one of the top contributors to carbon pollution'
          action 'Most people can thrive on a plant-based diet and do not need animal protein, and could make such a choice with immediate effect'
          rank 3
        }
        antiRacism {
          description 'People of Western European descent (\'white people\') have been the primary beneficiaries of burning fossil carbon'
          action 'White people should should bear the responsibility of paying for climate change mitigation'
          rank 4
        }
        seizeGlobalCapital {
          description 'The costs of climate change are inequitably distributed'
          action 'The costs of climate change mitigation should be born primarily by the wealthiest'
          rank 5
        }
        lastResort {
          description 'If the rich and the powerful refuse to get out of the way of legislative reforms of the system killing us all, there is, unfortunately, always a last resort'
          action 'It starts with \'g\' and rhymes with \'poutine\''
          rank 6
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

考虑到我们试图建模的领域的复杂性,我认为这相当易读。

但现在我们该如何在插件中应对这种情况呢?和往常一样,我认为最好的学习方法是通过实例,所以让我们通过查看一个新插件如何ThePluginOfThePeople根据用户提供的数据配置任务来把所有内容联系起来。

class ThePluginOfThePeople : Plugin<Project> {

  override fun apply(project: Project): Unit = project.run {
    val thePeople = thePeople()

    thePeople.problems.all { problem ->
      tasks.register("listSolutionsFor${problem.name.capitalize()}", ListSolutionsTask::class.java) {
        it.problem.set(problem)
      }
    }
  }
}

abstract class ListSolutionsTask : DefaultTask() {

  init {
    group = "People"
    description = "Prints list of solutions for a given problem"
  }

  @get:Input
  abstract val problem: Property<ProblemsHandler>

  @TaskAction fun action() {
    val problem = problem.get()

    val msg = buildString {
      appendLine(problem.name.capitalize())
      appendLine(problem.description.get())
      appendLine()
      appendLine("Solutions:")
      problem.solutions.sortedBy { it.rank.get() }.forEachIndexed { i, sol ->
        appendLine("${i + 1}. ${sol.name}")
        appendLine("   ${sol.description.get()}")
        appendLine("   ${sol.action.get()}")
      }
    }

    logger.quiet(msg)
  }
}
Enter fullscreen mode Exit fullscreen mode

我们可以轻松查看新插件注册的所有任务:

$ ./gradlew app:tasks --group people -q

------------------------------------------------------------
Tasks runnable from project ':app'
------------------------------------------------------------

People tasks
------------
listSolutionsForClimateChange - Prints list of solutions for a given problem
Enter fullscreen mode Exit fullscreen mode

在我们的插件中,我们使用 ` thePeople.problems.all(Action<T>)get_action()` 来响应用户提供的配置。`get_action()`all(Action<T>)会对给定集合中的所有元素以及未来可能添加的所有元素执行指定的操作;从这个意义上讲,它是惰性的。对我们来说,这很有用,因为插件的apply()方法会在插件应用时(在 `add_action()`plugins代码块中)立即运行,这意味着用户数据尚未可用,无法进行响应。`get_action()`all()优雅地解决了这个问题,而无需使用其他方法,例如 `get_action () afterEvaluate`。

在我们的problems.all代码块中,我们注册一个任务——每个问题一个任务——并通过将其唯一的输入设置为给定的值来配置该任务ProblemHandlerProvider<ProblemHandler>这是完全可序列化的,因此是一个有效的@Input属性,并且与实验性的配置缓存兼容。

我们的任务定义很简单。它是一个抽象类,允许我们使用托管类型(我们的@Input abstract val problem),并且有一个简单的操作。这里最大的陷阱是记住要调用get()各个Provider<String>实例,否则我们会得到类似这样的奇怪输出property 'description$fancy_plugin'

最后,让我们运行其中一个生成的任务,如下所示:

$ ./gradlew app:listSolutionsForClimateChange
Configuration cache is an incubating feature.
Calculating task graph as configuration cache cannot be reused because file 'app/build.gradle' has changed.

> Task :app:listSolutionsForclimateChange
ClimateChange
There is no question of cost, because the cost of doing nothing is everything.

Solutions:
1. cleanEnergy
   We cannot burn any more fossil energy
   Replace all fossil sources with clean solutions like wind, solar, and geothermal
2. massTransit
   Single-occupant vehicles are a major source of carbon pollution
   Increase density in urban environments and build free public transit for all
3. stopEatingAnimals
   Animal agriculture is one of the top contributors to carbon pollution
   Most people can thrive on a plant-based diet and do not need animal protein, and could make such a choice with immediate effect
4. antiRacism
   People of Western European descent ('white people') have been the primary beneficiaries of burning fossil carbon
   White people should should bear the responsibility of paying for climate change mitigation
5. seizeGlobalCapital
   The costs of climate change are inequitably distributed
   The costs of climate change mitigation should be born primarily by the wealthiest
6. lastResort
   If the rich and the powerful refuse to get out of the way of legislative reforms of the system killing us all, there is, unfortunately, always a last resort
   It starts with 'g' and rhymes with 'poutine'
Enter fullscreen mode Exit fullscreen mode

总结

在本文中,我们学习了如何使用 Gradle 对具有嵌套领域特定语言 (DSL) 的复杂领域进行建模,以及如何使用 Gradle 在这样的 DSL 中处理自定义用户数据NamedDomainObjectContainer。我建议您查看 GitHub 上的完整示例,其中包含构建脚本和项目布局决策,为了简洁起见,本文省略了这些内容。

尾注

1敬请期待我因糟糕的编程技术而被解雇后痛哭流涕。2强烈建议深入研究这些软件包。3 JetBrainsKotlin 语言的创建者

文章来源:https://dev.to/autonomousapps/gradle-plugins-and-extensions-a-primer-for-the-bemused-51lp