1. Overview

A big announcement was made back in January in the Spring ecosystem: Kotlin support is coming to Spring Framework 5. This means that starting from Spring Boot 2.x will have first-class support for Kotlin.

This is, of course, not unexpected, as the team at Pivotal is known for the acceptance of JVM languages such as Scala and Groovy.

Let’s now build a Kotlin app using Spring Boot app 3.x!

2. Setup

2.1. Environment

Kotlin supports development in IntelliJ, Eclipse, and on the command line. Follow the directions to set up your environment based on your preferences.

2.2. Setup

First, let’s create a Spring Boot 2 project and modify the POM to contain entries specifying the versions of Java and Kotlin with the dependencies:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib-jre8</artifactId>
    <version>1.2.71</version>
</dependency>
<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.2.71</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-kotlin</artifactId>
    <version>2.9.9</version>
</dependency>

Take note that we are specifying file locations for our Kotlin source and test files:

<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>

If our Kotlin files are in different locations, you will need to modify these entries in the POM.

To compile Kotlin modules and sources, we need to use kotlin-maven-plugin:

<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>1.1.2</version>
    <configuration>
        <compilerPlugins>
            <plugin>spring</plugin>
        </compilerPlugins>
        <jvmTarget>1.8</jvmTarget>
    </configuration>
    <executions>
        <execution>
            <id>compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
        <execution>
            <id>test-compile</id>
            <phase>test-compile</phase>
            <goals>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>
</plugin>

Alright, now we have everything we need to build our Kotlin application. For reference, you can find the latest versions of Maven Central (spring-boot-starter-web, kotlin-stdlib-jre8, kotlin-reflect, jackson-module-kotlin, test).

Next, let’s set up our application context.

3. Application Context

Let’s jump into some Kotlin code and write our familiar Spring Boot application context:

@SpringBootApplication
class KotlinDemoApplication

fun main(args: Array<String>) {
    SpringApplication.run(KotlinDemoApplication::class.java, *args)
}

We see our familiar @SpringBootApplication annotation. This is the same annotation we would use in a Java class.

Below that, we have a class definition for our KotlinDemoApplication class. In Kotlin, the default scope for classes is public, so we can omit that. Additionally, if a class has no variables and no functions, it can be declared without curly braces. So, in essence, we have just defined a class.

Moving on to the method. This is the standard Java entry point method in Java: public static void main(String[] args).

Again, methods or functions are public by default, so we do not have to declare that here. Additionally, functions that do not return anything do not need to specify a void return type.

And finally, any function defined outside the body of a class is automatically static. This makes this function eligible for startup execution.

Now, let’s run our application from the root directory using mvn spring-boot: run. The application should start, and we should see our application running on port 8080.

Next, let’s build a controller.

4. Controller

Let’s take a look at adding a controller to our service:

@RestController
class HelloController {
 
    @GetMapping("/hello")
    fun helloKotlin(): String {
        return "hello world"
    }
}

Not too much different from a standard Spring controller, but certainly less code. Let’s add a test class and case for this controller to validate our work:

@RunWith(SpringRunner::class)
@SpringBootTest(classes = arrayOf(KotlinDemoApplication::class), 
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class KotlinDemoApplicationTests {

    @Autowired
    lateinit var testRestTemplate: TestRestTemplate

    @Test
    fun whenCalled_shouldReturnHello() {
        val result = testRestTemplate
          // ...
          .getForEntity("/hello", String::class.java)

        assertNotNull(result)
        assertEquals(result?.statusCode, HttpStatus.OK)
        assertEquals(result?.body, "hello world")
    }
}

This test shows off one of Kotlin’s very powerful features – null safety! Kotlin variables that can be null must be declared using ‘?’. The compiler then knows that defensive coding is required before accessing that property.

In our test, TestRestTemplate is defined as a nullable type, and each time we access it, we do so using the null coalescing operator “?.” – which will return null if the called object is null.

This clarifies the use of nulls in the program and forces developers to write safe code when working with them.

Next, let’s add a service and integrate that into our controller.

5. Service

As you can probably guess now, our service is going to be pretty easy to add to our project. Let’s do that now:

@Service
class HelloService {
 
    fun getHello(): String {
        return "hello service"
    }
}

Pretty simple service here with a single function returning a String. Next, let’s wire our service into the controller and use it to return a value:

@RestController
class HelloController(val helloService: HelloService) {
 
    // ...
 
    @GetMapping("/hello-service")
    fun helloKotlinService(): String {
        return helloService.getHello()
    }
}

Ahh, that looks nice! In Kotlin, the main constructor can be defined in line with the class declaration. We’ve omitted the @Autowired annotation from our constructor because it’s not obligatory for some time.

Those parameters are automatically converted to fields in the class; in Kotlin, they are called properties. There are no getters or setters defined; they are created automatically. You can, of course, override these defaults if you want.

In Kotlin, properties in classes and variables in functions can be defined using var or val. Var indicates a mutable property, and val indicates a final one. This allows the compiler to check for illegal access. Since our HelloService is a singleton, we wire it up as a val to prevent mutation.

Next, let’s add a test for this controller method:

@Test
fun whenCalled_shouldReturnHelloService() {
    var result = testRestTemplate
      // ...
      .getForEntity("/hello-service", String::class.java)

    assertNotNull(result)
    assertEquals(result?.statusCode, HttpStatus.OK)
    assertEquals(result?.body, "hello service")
}

Lastly, let’s look at what a POJO looks like in Kotlin.

6. Kotlin Data Class

In Java, we represent data objects with plain old Java objects, the POJO. In Kotlin, we have something that lets us express this type of object more concisely – a data class.

Let’s write a data object to return in our controller:

data class HelloDto(val greeting: String)

That was no trick. I’m not omitting anything from our class. With the data modifier, we get a lot of benefits. This keyword automatically creates an equals/hashcode pair, a toString function, and a copy function. All that from a 53-character one-liner!

Now, let’s add a method to return our new data class:

// ...
@GetMapping("/hello-dto")
fun helloDto(): HelloDto {
    return HelloDto("Hello from the dto")
}

The data modifier does not add a default constructor, which is important for certain libraries like Jackson. To support this type of class, we have added the jackson-module-kotlin to our POM file to support marshalling. This was done during section 2; you can see the dependency there.

Finally, let’s add a test for this controller function:

@Test
fun whenCalled_shoudlReturnJSON() {
    val result = testRestTemplate
      // ...
      .getForEntity("/hello-dto", HelloDto::class.java)

    assertNotNull(result)
    assertEquals(result?.statusCode, HttpStatus.OK)
    assertEquals(result?.body, HelloDto("Hello from the dto"))
}

7. Conclusion

In this article, we looked at Kotlin support in Spring Boot 3.x. We saw from examples that Kotlin could simplify and enhance our applications by forcing us to write shorter, safer code.

Kotlin also supports some amazing features like the data class and class extensions and is fully compatible with existing Java code. This means that you can write Kotlin code and call it from your Java classes and vice-versa. In addition, Kotlin was built from the ground up to have fantastic support in an IDE, and it does.

There are a lot of reasons to try Kotlin out, and with Google and Spring backing it, now is the time to check it out. Let us know what you decide to build using it!

You can always find the source code on GitHub.