This post will walk you through setting up a basic gradle project for Kotlin JVM development. Over the series, we’ll evolve the project into a fully fledged, multimodule multiplatform project whilst adressing various issues that arise.
Setting up
The easiest way to start is by heading to Spring Initializr. Select Gradle - Kotlin
in the project selector. This will make your build scripts use Kotlin language rather than Groovy which is the Gradle default.
Set the language to Kotlin and hit Explore
. You’ll notice a file tree that looks like:
1
2
3
4
5
- gradle/
- src/
- .gitignore
- build.gradle.kts
- settings.gradle.kts
The file containing the majority if your build logic will be build.gradle.kts
, which is also the file displayed by default. It’ll contain something like the following, which we’ll break down into its various components.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.0.3"
id("io.spring.dependency-management") version "1.1.0"
kotlin("jvm") version "1.7.22"
kotlin("plugin.spring") version "1.7.22"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
Plugins
Gradle plugins will inject various steps into the build. org.springframework.boot
adds the bootRun
tasks, among other things, which lets you start the Spring boot app directly using ./gradlew bootRun
io.spring.dependency-management
sets up dependency resolution based on your Spring Boot version, which is why dependencies declared later in the build file don’t include any version selector.
kotlin("jvm")
is shorthand for id("org.jetbrains.kotlin.jvm")
and adds Kotlin compilation, source set, caching, etc.
kotlin("plugin.spring")
is shorthand for id("org.jetbrains.kotlin.plugin.spring")
and adds some things that make working with Spring easier. By default, Kotlin classes are not open for inheritance, meaning that Spring would be unable to generate proxies for them. Spring heavily relies on proxies (which inherit the annotated class) to add behaviour, so the plugin will make Spring-annotated classes open.
Group, version, java.sourceCompatibility
Group and version is used for publishing and will be used to set the maven pom data for the published artifact. Typically, you’ll generate a bootable jar and not use the Spring Boot application as a maven dependency, so I’m not really sure why this is added by default.
java.sourceCompatibility
configures the java
extension added by the java
gradle plugin. It could also be written as:
1
2
3
java {
sourceCompatibility = JavaVersion.VERSION_17
}
What
java
plugin? We didn’t have it in our plugins list!
Gradle ships an embedded Java plugin, which the org.jetbrains.kotlin.jvm
plugin applied to our project.
The Java plugin adds Java compilation along with testing and bundling capabilities to a project. It serves as the basis for many of the other JVM language Gradle plugins. You can find a comprehensive introduction and overview to the Java Plugin in the Building Java Projects chapter.
The parameter itself, sourceCompatibility sets the Java version compatibily when compiling java sources. Again, for a pure Kotlin project, I’m not really sure why this would be needed so you might as well remove it.
Repositories
This section defines where we pull our dependencies from. If we want to use open-source snapshots or a private registry, we might want to add more definitions here. Like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
repositories {
mavenCentral()
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
maven(url = "https://maven.mycompany.com") {
authentication {
create<BasicAuthentication>("basic")
}
credentials {
username = "$internalUser"
password = "$internalPassword"
}
}
}
Update 2023-02-27: Note that the username and password should not be specified here directly, as that would expose them in your build logs. Read more on credential handling here
Dependencies
The meat of our build. Our dependencies will be what changes the most between different modules and applications. We specify dependencies for the main
source set by adding implementation("$groupId:$artifactId:$version")
to the dependencies block. Dependencies for the test
source set is similarly added using testImplementation
.
But there’s no version on the dependencies?
That’s right. Because in this case we’re only adding Spring-managed dependencies, which the Spring dependency management Gradle plugin resolves versions for. In case you want to add a dependency for something Spring doesn’t manage however, you’d still need to specify the version yourself.
KotlinCompile
1
2
3
4
5
6
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
This configures all KotlinCompile
tasks registered to use jvmTarget 17, and sets the compiler args to -Xjsr305=strict. Note that this invalidates any other freeCompilerArgs
added by any other step of the build. Prefer to use freeCompilerArgs += listOf("-Xjsr305=strict")
which keeps previous args intact.
Test
1
2
3
tasks.withType<Test> {
useJUnitPlatform()
}
Enables Gradle’s integration with JUnit’s Jupiter platform, making the build fail if there’s test failures and generate test reports based on the test execution.
Both the KotlinCompile
and Test
configurations are done eagerly, forcing them to be configured every time you invoke gradle (unless it’s cached?). A way to remedy this would be to change the section to:
1
2
3
4
5
6
7
8
9
10
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
This allows Gradle to configure the tasks lazily. You can read more about laziness here
Summary
We’ve dissected the Gradle configuration as generated by Spring Initializr, hopefully learning a thing or two about what happens under the hood in the process. Next time we’ll look into generating code coverage for our project.