Write Custom Kotlin Gradle Build Plugin

Ryan Zheng
Geek Culture
Published in
5 min readMar 8, 2021

First We will look at how Gradle Plugin works internally. Then we will build a custom plugin to extract our common configurations from build.gradle.kts to our own custom plugin.

The first thing to notice is that Gradle itself is one Java application. The Gradle project will run on all of our build.gradle.kts files and carry out build actions. The output of the Gradle build is our own application.

Every Java program needs dependent libraries on the classpath. The Gradle build is no different. It needs all the necessary libraries to finish the build process. These libraries have to be differentiated from the libraries which are needed by our own application.

While Gradle runs the build, it needs all those Project.java, Task.java, Plugin.java, etc classes, the same as when we are running our Spring applications, we need spring-core, spring-beans, etc libraries on the classpath.

How do we make Gradle know that it needs KotlinDslPlugin.java class for example in order to build the application? The answer is we have to specifically tell Gradle that you need this class to run the build, same as we explicitly specify spring-context should be on our classpath.

buildScript

Previously Gradle uses the buildscript to tell Gradle that it needs the dependency jar to be on the classpath of the build application. This jar can be pulled from maven repositories specified by repositories block.

buildscript {
repositories {
maven {
url = uri("https://repo.spring.io/plugins-snapshot") }
}
dependencies {
classpath("io.spring.gradle:dependency-management-plugin:1.0.10.RELEASE")
}
}
apply(plugin = "io.spring.dependency-management")

Now the syntax is simplified to plugins {} block. We just need to add the plugins {} to the build.gradle.kts.

plugins {
id("io.spring.dependency-management") version "1.0.10.RELEASE"
}

Then Gradle will put the dependency-management jar on the classpath for the build application and also apply the plugin to our project. We will talk later about what it means when the plugin is applied to a project.

How do we declare the dependencies for our own Application? We use the dependencies block outside the buildscript.

dependencies { 
implementation("org.springframework.boot:spring-boot-starter-web")
}

In Spring Application, our project normally contains multiple Spring-related dependencies. The dependencies can be categorized into compile-time and runtime dependencies, etc. It can be considered as two categories of dependencies. It can also be more.

Gradle uses Configuration as a logical container for a certain category of dependencies. Each Configuration has a name. For example, we can have a Configuration named “compile”. The “compile” configuration contains a bunch of jars as compile-time dependencies. The following is a diagram showing the relationship between Project, Configuration, and Dependencies. It can be viewed from left to right to see how each class is related to the others.

Handling Dependencies

From the above diagram, we can see the Configuration.getDependencies will return a DependencySet. Because for either compile-time or runtime, we could have multiple jars as dependencies.

Repository

Repository tells Gradle the remote URL to pull the dependent jars from for our Project

API for adding repository

Extension

The purpose of Extension is to enable domain class(Project, Task, etc) to be able to use any other object of user-defined classes. Just consider ExtensionContainer as a Map<String, Object>, and this Map becomes one attribute one the Domain object(Project or Task, etc). In this way, we are able to add any random class to the Map. Easy!

API for add and get Extension

Plugin

Plugin can be considered as a helper class in which we apply our previously mentioned APIs to the Project.

API for apply plugin on project

Experiment

The Goal: In most Spring Cloud applications, we have to apply the following plugins to all most all our web applications.

plugins {
java
id("org.springframework.boot") version "2.4.3"
id "io.spring.dependency-management" version <<version>>
}

We want to extract the above logic to our custom plugin. So we can just apply our own plugin for each project.

plugins {
id(“com.qiusuo.common”)
}

In the custom plugin, we apply the other plugins programmatically. This is also how Plugins work internally.

Start The Train

Create gradledemo project

Load the project into Intellij and create one dummy helloworld controller.

@RestController
@RequestMapping("/helloworld")
class HelloWorldController {
@RequestMapping
fun posts(): Mono<String> {
return Mono.just("hello world");
}
}

Now under the gradledemo project, create one folder buildSrc, create one additional build.gradle.kts file. We will add the following content to the build.gradle.kts.

plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
jcenter()
maven { setUrl("https://plugins.gradle.org/m2/") }
}

Intellij will automatically recognize this as a new Project. We will create the plugin class under package com.qiusuo. And we will add the following content to the build.gradle.kts

dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
implementation("io.spring.dependency-management:io.spring.dependency-management.gradle.plugin:1.0.10.RELEASE")
implementation(group = "org.springframework.boot", name = "spring-boot-gradle-plugin", version = "2.4.3")
}

What we are doing here is just add the plugin jar as dependency of our buildSrc project. In this way, we are able to use different Plugin classes in our source code. Consider it just as one normal java application.

Now we create one QiuSuoPlugin.kt under package com.qiusuo.

class QiuSuoPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.configurePlugins()
target.configureRepositories()
}
}

fun Project.configurePlugins() {
pluginManager.apply(SpringBootPlugin::class.java)
pluginManager.apply(DependencyManagementPlugin::class.java)
pluginManager.apply(JavaPlugin::class.java)
pluginManager.apply(ApplicationPlugin::class.java)
}

fun Project.configureRepositories() {
repositories.mavenLocal()
repositories.mavenCentral()
repositories.jcenter()
}

The content of this file is very self-explanatory. We are using the previous interfaces to configure the target Project object. We applied SpringBootPlugin, DependencyManagementPlugin, JavaPlugin, and ApplicationPlugin to the target project.

We just need the following code in the gradledemo/build.gradle.kts file to apply our plugin to gradledemo project

plugins {
id("com.qiusuo.common")
}

The effect of the plugins block is very clear now. When this block is applied to a certain project, the apply(target: Project) function will get called to perform some custom actions to the project.

If you read the source code of other plugins, you will find that what they are doing is also using the provided APIs from Gradle to either apply plugins or add extensions or add tasks to the Project. For example, the kotlin-dsl plugin source code looks like this.

class KotlinDslPlugin : Plugin<Project> {
override fun apply(project: Project): Unit = project.run
{
apply<JavaGradlePluginPlugin>()
apply<KotlinDslBasePlugin>()
apply<PrecompiledScriptPlugins>()
}
}

You can find the source code of this demo in the following github repo

https://github.com/ryan-zheng-teki/kotlin-gradle-plugin-demo

--

--

Ryan Zheng
Geek Culture

I am a software developer who is keen to know how things work