Home Creating a Spring Boot project for Kotlin and Gradle
Post
Cancel

Creating a Spring Boot project for Kotlin and Gradle

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.

This post is licensed under CC BY 4.0 by the author.
Contents