Gradle Goodness: Shared Configuration With Conventions Plugin
When we have a multi-module project in Gradle we sometimes want to have dependencies, task configuration and other settings shared between the multiple modules. We can use the subprojects
or allprojects
blocks, but the downside is that it is not clear from the build script of the subproject where the configuration comes from. We must remember it is set from another build script, but there is no reference in the subproject to that connection. It is better to use a plugin with shared configuration and use that plugin in the subprojects. We call this a conventions plugin. This way it is explicitly visible in a subproject that the shared settings come from a plugin. Also it allows Gradle to optimize the build configuration.
The easiest way to implement the shared configuration in a plugin is using a so-called precompiled script plugin. This type of plugin can be written as a build script using the Groovy or Kotlin DSL with a filename ending with .gradle
or .gradle.kts
. The name of the plugin is the first part of the filename before .gradle
or .gradle.kts
. In our subproject we can add the plugin to our build script to apply the shared configuration. For a multi-module project we can create such a plugin in the buildSrc
directory. For a Groovy plugin we place the file in src/main/groovy
, for a Kotlin plugin we place it in src/main/kotlin
.
In the following example we write a script plugin using the Kotlin DSL to apply the java-library
plugin to a project, set some common dependencies used by all projects, configure the Test
tasks and set the Java toolchain. First we create a build.gradle.kts
file in the buildSrc
directory in the root of our multi-module project and apply the kotlin-dsl
plugin:
// File: buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories.mavenCentral()
Next we create the conventions plugin with our shared configuration:
// File: buildSrc/src/main/kotlin/java-project-conventions.gradle.kts
plugins {
`java-library`
}
group = "mrhaki.sample"
version = "1.0"
repositories {
mavenCentral()
}
dependencies {
val log4jVersion: String by extra("2.14.0")
val junitVersion: String by extra("5.3.1")
val assertjVersion: String by extra("3.19.0")
// Logging
implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}")
// Testing
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
testImplementation("org.assertj:assertj-core:${assertjVersion}")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(15))
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
The id of our new plugin is java-project-conventions
and we can use it in our build script for a subproject as:
// File: rest-api/build.gradle.kts
plugins {
id("java-project-conventions") // apply shared config
application // apply the Gradle application plugin
}
dependencies {
val vertxVersion: String by extra("4.0.2")
implementation(project(":domain")) // project dependency
implementation("io.vertx:vertx-core:${vertxVersion}")
}
application {
mainClass.set("com.mrhaki.web.Api")
}
The rest-api
project will have all the configuration and tasks from java-library
plugin as configured in the java-project-conventions
plugin, so we can build it as a Java project.
Written with Gradle 6.8.2.