Introduction
In this article, we are going to talk about Kotlin. I have developed a very simple REST API in Kotlin using Spring Boot, Spring Data, and the H2 in-memory DB.
Kotlin and Spring Boot work well together.
You will notice in the Code Walkthrough section that there is NO controller and NO service class in the project. That's magic of Spring's @RepositoryRestResource
, which is further explained below.
I have no experience in Kotlin, but what I read and saw on Kotlin code in GitHub projects was definitely worth exploring.
An important question you might ask is: why Kotlin?
Why Kotlin?
- Kotlin compiles to bytecode, so it can perform just as well as Java.
- Kotlin is more succinct than Java.
- Kotlin's
Data
classes are more concise than Java's value
classes. - Classes are final by default, which corresponds to Effective Java Item 17 — you would need to explicitly put open if you want to make a class inheritable.
- Abstract classes are open by default.
- One of Kotlin’s key features is null-safety, which cleanly deals with null values at compile time rather than bumping into the famous
NullPointerException
at runtime. - Primary constructor vs. secondary constructors — if you need more than one constructor, then only you would go for secondary. Otherwise, most of the Kotlin class would have a primary constructor.
- Kotlin can also be used as a scripting language.
- Kotlin and Java are inter-operable, so it's easy to do a trial of Kotlin on just a small part of your codebase.
- Kotlin uses aggressive type inference to determine the types of values and expressions for which type has been left unstated. This reduces language verbosity relative to Java.
- Kotlin is fully supported by Google for use with their Android operating system.
The two points below reference Wikipedia:
- "According to JetBrains blog, Kotlin is used by Amazon Web Services, Pinterest, Coursera, Netflix, Uber, and others. Corda, a distributed ledger developed by a consortium of well-known banks (such as Goldman Sachs, Wells Fargo, J.P. Morgan, Deutsche Bank, UBS, HSBC, BNP Paribas, Société Générale), has over 90 percent of Kotlin in its codebase."
- According to Google, Kotlin has already been adopted by several major developers — Expedia, Flipboard, Pinterest, Square, and others — for their Android production apps.
Code Walkthrough
The project code for this example can be found on my Kotlin Github Repo, demonstrating a simple REST API using Kotlin and Spring Boot
Clone - https://github.com/BeTheCodeWithYou/SpringBoot-Kotlin.git
Project Structure
Running the Application from IDE
Running the Integration Tests Directly from IDE
Understanding build.gradle
buildscript {
ext {
kotlinVersion = '1.2.41'
springBootVersion = '2.0.2.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
}
}
apply plugin: 'base'
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'kotlin-jpa'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'com.xp.springboot.restapi.kotlin'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-data-rest')
compile('org.springframework.boot:spring-boot-starter-web')
compile('com.fasterxml.jackson.module:jackson-module-kotlin')
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
runtime("com.h2database:h2")
testCompile("org.jsmart:zerocode-rest-bdd:1.2.11")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
task integrationTests (type: Test) {
delete '/target/'
systemProperty 'zerocode.junit', 'gen-smart-charts-csv-reports'
include 'integrationtests/TestGetOperations.class'
include 'integrationtests/TestPatchOperations.class'
include 'integrationtests/TestPostOperations.class'
testLogging {
showStandardStreams = true
}
}
org.jetbrains.kotlin:kotlin-gradle-plugin
compiles Kotlin sources and modules. org.jetbrains.kotlin:kotlin-allopen
— this is the interesting part here. In Kotlin, by default, all classes are final
.
Now, in order to make a class inheritable, you have to annotate with an open
keyword. And, the problem is that a lot of other libraries, like Spring, test libraries (Mockito, etc.), require classes and methods to become non-final
. In Spring, such classes mainly include @Configuration
classes and @Bean
methods.
The rule here is simple; you need to annotate the @Configuration
and @Bean
methods to mark them open, but this approach is tedious and error-prone, hence Kotlin has come up with the compiler plugin to automate this process through this dependency using org.jetbrains.kotlin:kotlin-noarg
and apply plugin: 'kotlin-jpa'
.
In order to be able to use Kotlin immutable classes, we need to enable the Kotlin JPA plugin. It will generate no-arg constructors for any class annotated with @Entity
, apply plugin: 'kotlin'.
To target the JVM, the Kotlin plugin needs to be applied: apply plugin: 'kotlin-spring'
. This is required for Spring Kotlin integration.
testCompile("org.jsmart:zerocode-rest-bdd:1.2.11")
- Integration Tests library dependency.
Compiler Options
Spring nullability annotations provide null
-safety for the whole Spring API to Kotlin developers, with the advantage of dealing with null
-related issues at compile time. This feature can be enabled by adding the -Xjsr305
compiler flag with the strict options. And, it also configures the Kotlin compiler to generate Java 8 bytecode.
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
is the Java 8 variant of the Kotlin standard library. compile('com.fasterxml.jackson.module:jackson-module-kotlin')
adds support for serialization/deserialization of Kotlin and data classes. compile("org.jetbrains.kotlin:kotlin-reflect")
is the Kotlin reflection library.
The Spring Boot Gradle plugin automatically uses the Kotlin version declared on the Kotlin Gradle plugin, hence the version is not defined explicitly on the dependencies section.
All remaining entries are self-explanatory.
Kotlin Code
Spring Boot Application
/src/main/kotlin/com.xp.springboot.kotlin.SpringBootKotlinRestApiApplication.kt
Notice the missing semicolon above. You need to open and close braces of the class only if you have the @Bean
. Otherwise, a class with name
is required. Runapplication
is a top-level function.
package com.xp.springboot.kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.context.annotation.Bean
import com.xp.springboot.kotlin.repository.ParkRunnerRepository
import org.springframework.boot.CommandLineRunner
import com.xp.springboot.kotlin.model.ParkRunner
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.SpringApplication
import org.springframework.boot.context.ApplicationPidFileWriter
@SpringBootApplication
@ComponentScan(basePackages = arrayOf("com.xp.springboot.*"))
@EnableJpaRepositories("com.xp.springboot.*")
@EntityScan("com.xp.springboot.*")
class SpringBootKotlinRestApiApplication {
@Bean
fun run(repository : ParkRunnerRepository) = ApplicationRunner {
repository.save(ParkRunner(firstName = "NEERAJ", lastName="SIDHAYE", gender="M",
totalRuns="170", runningClub="RUNWAY"))
}
}
fun main(args: Array<String>) {
runApplication<SpringBootKotlinRestApiApplication>(*args)
}
fun start() {
runApplication<SpringBootKotlinRestApiApplication>()
}
Creating a Data Class
Then, we create our model by using Kotlin data classes, which are designed to hold data and automatically provide equals()
, hashCode()
,toString()
, componentN()
functions, and copy()
.
Also, you could define multiple entities in the same data class. Var
is similar to the general variable and is known as a mutable variable in Kotlin, which can be assigned multiple times.
There is another type: val
, which is like constant variable and is known as immutable in Kotlin and can be initialized only a single time. Moreover, val
is read-only, and you are not allowed to explicitly write to val
.
package com.xp.springboot.kotlin.model
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Table(name="PARK_RUNNER")
data class ParkRunner (
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var parkRunId: Long = -1,
@Column(name = "firstName")
var firstName: String = "",
@Column(name = "lastName")
var lastName: String = "",
@Column(name = "gender")
var gender: String = "",
@Column(name = "runningClub")
var runningClub: String = "",
@Column(name = "totalRuns")
var totalRuns: String = "0"
){ }
Creating a Repository
Yes, there is just the one-liner to define the repository interface with the Spring Data curd repository. The interesting Spring annotation here is RepositoryRestResource
. This comes by adding the spring-boot-starter-data-rest
dependency.
package com.xp.springboot.kotlin.repository
import org.springframework.data.repository.CrudRepository
import com.xp.springboot.kotlin.model.ParkRunner
import org.springframework.data.rest.core.annotation.RepositoryRestResource
@RepositoryRestResource(collectionResourceRel = "runners", path = "runners")
interface ParkRunnerRepository : CrudRepository <ParkRunner, Long >{
}
If you noticed, there is no Controller and no Service. This project is exposing the following REST endpoints with HATEOAS enabled.
GET
- http://localhost:8080/parkrun/runners POST
- http://localhost:8080/parkrun/runners GET
- http://localhost:8080/parkrun/runners/2 DELETE
- http://localhost:8080/parkrun/runners/1
It's the magic of @RepositoryRestResource
.
ApplycollectionResourceRel
to define custom resource label, or else, the annotation will use the default as per the model class name (/parkRunners
).
At runtime, Spring Data REST will create an implementation of this interface automatically. Then, it will use the @RepositoryRestResource
annotation to direct Spring MVC and create RESTful endpoints at /parkRunners
.
Integration Test Cases
GET-operation-Tests
package integrationtests
import org.jsmart.zerocode.core.domain.TargetEnv
import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner
import org.junit.runner.RunWith
import org.junit.Test
import org.jsmart.zerocode.core.domain.JsonTestCase
import org.jsmart.zerocode.core.domain.EnvProperty
@TargetEnv("application_host.properties")
@RunWith(ZeroCodeUnitRunner::class)
class TestGetOperations {
@Test
@JsonTestCase("integration_tests/get/get_all_runners.json")
fun `get all runners`() {
}
}
POST-operation-Tests
package integrationtests
import org.jsmart.zerocode.core.domain.JsonTestCase
import org.jsmart.zerocode.core.domain.TargetEnv
import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner
import org.junit.runner.RunWith
import org.junit.Test
@TargetEnv("application_host.properties")
@RunWith(ZeroCodeUnitRunner::class)
class TestPostOperations {
@Test
@JsonTestCase("integration_tests/post/create_new_runner.json")
fun `post create runner`() {
}
}
PATCH-Operation-Tests
package integrationtests
import org.junit.runner.RunWith
import org.jsmart.zerocode.core.runner.ZeroCodeUnitRunner
import org.jsmart.zerocode.core.domain.TargetEnv
import org.jsmart.zerocode.core.domain.JsonTestCase
import org.junit.Test
@TargetEnv("application_host.properties")
@RunWith(ZeroCodeUnitRunner::class)
class TestPatchOperations {
@Test
@JsonTestCase("integration_tests/patch/patch_runner_profile.json")
fun `patch runner profile`() {
}
}
Building the Application
gradle clean build -x test
will generate spring boot application jar into the /build/libs/.
Integration tests are skipped using -x test
because integration test would expect application to be running, so we will first just build the app, then run the app, and then execute integration tests against the local running app instance.
Running the Application
Once you have the jar ready after the gradle clean build, run the app using:
java -jar SpringBootKotlinRestAPI-0.0.1-SNAPSHOT.jar
and hit the URL http://localhost:8080/parkrun
directly on the browser.
Here is a look at the response:
{
"_links": {
"runners": {
"href": "http://localhost:8080/parkrun/runners"
},
"profile": {
"href": "http://localhost:8080/parkrun/profile"
}
}
}
Explore other end points available with this simple Kotlin Rest API:
GET - http://localhost:8080/parkrun/runners
POST - http://localhost:8080/parkrun/runners
GET - http://localhost:8080/parkrun/runners/2
> PATCH - http://localhost:8080/parkrun/runners/1
Running the Integration Test Cases
gradle integrationTests
This gradle task will run all the integration tests against the local running instance of the application jar.
Integration Test Reports
You could find the integration test and logs, under the folder /target/.
Hope you find this article useful for developing Spring Boot REST API with Kotlin along with Integration Test cases. Happy coding!