1072 lines
38 KiB
Plaintext
1072 lines
38 KiB
Plaintext
---
|
|
tags: [kotlin,rest,data,jpa]
|
|
projects: [spring-data-jpa,spring-framework,spring-boot]
|
|
---
|
|
:toc:
|
|
:icons: font
|
|
:source-highlighter: prettify
|
|
:project_id: spring-boot
|
|
:tabsize: 2
|
|
|
|
This tutorial shows you how to build efficiently a sample blog application by combining the power of https://projects.spring.io/spring-boot/[Spring Boot] and https://kotlinlang.org/[Kotlin].
|
|
|
|
If you are starting with Kotlin, you can learn the language by reading the https://kotlinlang.org/docs/reference/[reference documentation] and following the online https://try.kotlinlang.org[Kotlin Koans tutorial].
|
|
|
|
Spring Kotlin support is documented in the https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] and https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation. If you need help, search or ask questions with the https://stackoverflow.com/questions/tagged/kotlin+spring[`spring` and `kotlin` tags on StackOverflow] or come discuss in the `#spring` channel of https://slack.kotlinlang.org/[Kotlin Slack].
|
|
|
|
== Creating a New Project
|
|
|
|
First we need to create a Spring Boot application, which can be done in a number of ways.
|
|
|
|
[[using-the-initializr-website]]
|
|
=== Using the Initializr Website
|
|
|
|
Visit https://start.spring.io and choose the Kotlin language.
|
|
Gradle is the most commonly used build tool in Kotlin, and it provides a Kotlin DSL which is used by default when generating a Kotlin project, so this is the recommended choice. But you can also use Maven if you are more comfortable with it.
|
|
Notice that you can use https://start.spring.io/#!language=kotlin&type=gradle-project to have Kotlin and Gradle selected by default.
|
|
|
|
. Select "Gradle Project" or let the default "Maven Project" depending on which build tool you want to use
|
|
. Enter the following artifact coordinates: `blog`
|
|
. Add the following dependencies:
|
|
- Web
|
|
- Mustache
|
|
- JPA
|
|
- H2
|
|
- Devtools
|
|
. Click "Generate Project".
|
|
|
|
image::./images/initializr.png[]
|
|
|
|
The .zip file contains a standard project in the root directory, so you might want to create an empty directory before you unpack it.
|
|
|
|
[[using-command-line]]
|
|
=== Using command line
|
|
|
|
You can use the Initializr HTTP API https://docs.spring.io/initializr/docs/current/reference/htmlsingle/#command-line[from the command line] with, for example, curl on a UN*X like system:
|
|
|
|
[source]
|
|
----
|
|
$ mkdir blog && cd blog
|
|
$ curl https://start.spring.io/starter.zip -d language=kotlin -d style=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip
|
|
----
|
|
|
|
Add `-d type=gradle-project` if you want to use Gradle.
|
|
|
|
[[using-intellij-idea]]
|
|
=== Using IntelliJ IDEA
|
|
|
|
Spring Initializr is also integrated in IntelliJ IDEA Ultimate edition and allows you to create and import a new project without having to leave the IDE for the command-line or the web UI.
|
|
|
|
To access the wizard, go to File | New | Project, and select Spring Initializr.
|
|
|
|
Follow the steps of the wizard to use the following parameters:
|
|
|
|
- Artifact: "blog"
|
|
- Type: Maven project or Gradle Project
|
|
- Language: Kotlin
|
|
- Name: "Blog"
|
|
- Dependencies: "Web", "Mustache", JPA", "H2" and "Devtool"
|
|
|
|
[[reveal-gradle]]
|
|
[.reveal-gradle]
|
|
== Gradle Build
|
|
|
|
[[use-gradle]]
|
|
[.use-gradle]
|
|
== Gradle Build
|
|
|
|
=== Plugins
|
|
|
|
In addition to the obvious https://kotlinlang.org/docs/reference/using-gradle.html[Kotlin Gradle plugin], the default configuration declares the https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support[kotlin-spring plugin] which automatically opens classes and methods (unlike in Java, the default qualifier is `final` in Kotlin) annotated or meta-annotated with Spring annotations. This is useful to be able to create `@Configuration` or `@Transactional` beans without having to add the `open` qualifier required by CGLIB proxies for example.
|
|
|
|
In order to be able to use Kotlin non-nullable properties with JPA, https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] is also enabled. It generates no-arg constructors for any class annotated with `@Entity`, `@MappedSuperclass` or `@Embeddable`.
|
|
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
|
|
plugins {
|
|
kotlin("plugin.jpa") version "1.2.71"
|
|
id("org.springframework.boot") version "2.1.5.RELEASE"
|
|
id("io.spring.dependency-management") version "1.0.7.RELEASE"
|
|
kotlin("jvm") version "1.2.71"
|
|
kotlin("plugin.spring") version "1.2.71"
|
|
}
|
|
----
|
|
|
|
=== Compiler options
|
|
|
|
One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety] - which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "value or no value" semantics without paying the cost of wrappers like `Optional`. Note that Kotlin allows using functional constructs with nullable values; check out this https://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].
|
|
|
|
Although Java does not allow one to express null-safety in its type-system, Spring Framework provides null-safety of the whole Spring Framework API via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + Spring nullability annotations provide null-safety for the whole Spring Framework 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.
|
|
|
|
Notice also that Kotlin compiler is configured to generate Java 8 bytecode (Java 6 by default).
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
tasks.withType<KotlinCompile> {
|
|
kotlinOptions {
|
|
freeCompilerArgs = listOf("-Xjsr305=strict")
|
|
jvmTarget = "1.8"
|
|
}
|
|
}
|
|
----
|
|
|
|
=== Dependencies
|
|
|
|
3 Kotlin specific libraries are required for such Spring Boot web application and configured by default:
|
|
|
|
- `kotlin-stdlib-jdk8` is the Java 8 variant of Kotlin standard library
|
|
- `kotlin-reflect` is Kotlin reflection library
|
|
- `jackson-module-kotlin` adds support for serialization/deserialization of Kotlin classes and data classes (single constructor classes can be used automatically, and those with secondary constructors or static factories are also supported)
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
dependencies {
|
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
|
implementation("org.springframework.boot:spring-boot-starter-mustache")
|
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
runtimeOnly("com.h2database:h2:1.4.197") // Fixed version as a workaround for https://github.com/h2database/h2database/issues/1841
|
|
runtimeOnly("org.springframework.boot:spring-boot-devtools")
|
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
}
|
|
----
|
|
|
|
Spring Boot Gradle plugin automatically uses the Kotlin version declared via the Kotlin Gradle plugin.
|
|
|
|
[[reveal-maven]]
|
|
[.reveal-maven]
|
|
== Maven Build
|
|
|
|
[[use-maven]]
|
|
[.use-maven]
|
|
== Maven Build
|
|
|
|
=== Plugins
|
|
|
|
In addition to the obvious https://kotlinlang.org/docs/reference/using-maven.html[Kotlin Maven plugin], the default configuration declares the https://kotlinlang.org/docs/reference/compiler-plugins.html#spring-support[kotlin-spring plugin] which automatically opens classes and methods (unlike in Java, the default qualifier is `final` in Kotlin) annotated or meta-annotated with Spring annotations. This is useful to be able to create `@Configuration` or `@Transactional` beans without having to add the `open` qualifier required by CGLIB proxies for example.
|
|
|
|
In order to be able to use Kotlin non-nullable properties with JPA, https://kotlinlang.org/docs/reference/compiler-plugins.html#jpa-support[Kotlin JPA plugin] is also enabled. It generates no-arg constructors for any class annotated with `@Entity`, `@MappedSuperclass` or `@Embeddable`.
|
|
|
|
`pom.xml`
|
|
[source,xml]
|
|
----
|
|
<build>
|
|
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
|
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
|
<plugins>
|
|
<plugin>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
</plugin>
|
|
<plugin>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<artifactId>kotlin-maven-plugin</artifactId>
|
|
<configuration>
|
|
<compilerPlugins>
|
|
<plugin>jpa</plugin>
|
|
<plugin>spring</plugin>
|
|
</compilerPlugins>
|
|
<args>
|
|
<arg>-Xjsr305=strict</arg>
|
|
</args>
|
|
</configuration>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<artifactId>kotlin-maven-noarg</artifactId>
|
|
<version>${kotlin.version}</version>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<artifactId>kotlin-maven-allopen</artifactId>
|
|
<version>${kotlin.version}</version>
|
|
</dependency>
|
|
</dependencies>
|
|
</plugin>
|
|
</plugins>
|
|
</build>
|
|
----
|
|
|
|
One of Kotlin's key features is https://kotlinlang.org/docs/reference/null-safety.html[null-safety] - which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "value or no value" semantics without paying the cost of wrappers like `Optional`. Note that Kotlin allows using functional constructs with nullable values; check out this https://www.baeldung.com/kotlin-null-safety[comprehensive guide to Kotlin null-safety].
|
|
|
|
Although Java does not allow one to express null-safety in its type-system, Spring Framework provides null-safety of the whole Spring Framework API via tooling-friendly annotations declared in the `org.springframework.lang` package. By default, types from Java APIs used in Kotlin are recognized as https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types[platform types] for which null-checks are relaxed. https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support[Kotlin support for JSR 305 annotations] + Spring nullability annotations provide null-safety for the whole Spring Framework 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.
|
|
|
|
Notice also that Kotlin compiler is configured to generate Java 8 bytecode (Java 6 by default).
|
|
|
|
=== Dependencies
|
|
|
|
3 Kotlin specific libraries are required for such Spring Boot web application and configured by default:
|
|
|
|
- `kotlin-stdlib-jdk8` is the Java 8 variant of Kotlin standard library
|
|
- `kotlin-reflect` is Kotlin reflection library (mandatory as of Spring Framework 5)
|
|
- `jackson-module-kotlin` adds support for serialization/deserialization of Kotlin classes and data classes (single constructor classes can be used automatically, and those with secondary constructors or static factories are also supported)
|
|
|
|
`pom.xml`
|
|
[source,xml]
|
|
----
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-mustache</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-web</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>com.fasterxml.jackson.module</groupId>
|
|
<artifactId>jackson-module-kotlin</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<artifactId>kotlin-reflect</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
|
</dependency>
|
|
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-devtools</artifactId>
|
|
<scope>runtime</scope>
|
|
</dependency>
|
|
<!-- Fixed version as a workaround for https://github.com/h2database/h2database/issues/1841 -->
|
|
<dependency>
|
|
<groupId>com.h2database</groupId>
|
|
<artifactId>h2</artifactId>
|
|
<version>1.4.197</version>
|
|
<scope>runtime</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
----
|
|
|
|
== Understanding the generated Application
|
|
|
|
`src/main/kotlin/blog/BlogApplication.kt`
|
|
[source,kotlin]
|
|
----
|
|
package com.example.blog
|
|
|
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
|
import org.springframework.boot.runApplication
|
|
|
|
@SpringBootApplication
|
|
class BlogApplication
|
|
|
|
fun main(args: Array<String>) {
|
|
runApplication<BlogApplication>(*args)
|
|
}
|
|
----
|
|
|
|
Compared to Java, you can notice the lack of semicolons, the lack of brackets on empty class (you can add some if you need to declare beans via `@Bean` annotation) and the use of `runApplication` top level function. `runApplication<BlogApplication>(*args)` is Kotlin idiomatic alternative to `SpringApplication.run(BlogApplication::class.java, *args)` and can be used to customize the application with following syntax.
|
|
|
|
`src/main/kotlin/blog/BlogApplication.kt`
|
|
[source,kotlin]
|
|
----
|
|
fun main(args: Array<String>) {
|
|
runApplication<BlogApplication>(*args) {
|
|
setBannerMode(Banner.Mode.OFF)
|
|
}
|
|
}
|
|
----
|
|
|
|
== Writing your first Kotlin controller
|
|
|
|
Let's create a simple controller to display a simple web page.
|
|
|
|
`src/main/kotlin/blog/HtmlController.kt`
|
|
[source,kotlin]
|
|
----
|
|
package com.example.blog
|
|
|
|
import org.springframework.stereotype.Controller
|
|
import org.springframework.ui.Model
|
|
import org.springframework.ui.set
|
|
import org.springframework.web.bind.annotation.GetMapping
|
|
|
|
@Controller
|
|
class HtmlController {
|
|
|
|
@GetMapping("/")
|
|
fun blog(model: Model): String {
|
|
model["title"] = "Blog"
|
|
return "blog"
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
Notice that we are using here a https://kotlinlang.org/docs/reference/extensions.html[Kotlin extension] that allows to add Kotlin functions or operators to existing Spring types. Here we import the `org.springframework.ui.set` extension function in order to be able to write `model["title"] = "Blog"` instead of `model.addAttribute("title", "Blog")`.
|
|
The https://docs.spring.io/spring-framework/docs/current/kdoc-api/spring-framework/[Spring Framework KDoc API] lists all the Kotlin extensions provided to enrich the Java API.
|
|
|
|
We also need to create the associated Mustache templates.
|
|
|
|
`src/main/resources/templates/header.mustache`
|
|
[source]
|
|
----
|
|
<html>
|
|
<head>
|
|
<title>{{title}}</title>
|
|
</head>
|
|
<body>
|
|
----
|
|
|
|
`src/main/resources/templates/footer.mustache`
|
|
[source]
|
|
----
|
|
</body>
|
|
</html>
|
|
----
|
|
|
|
`src/main/resources/templates/blog.mustache`
|
|
[source]
|
|
----
|
|
{{> header}}
|
|
|
|
<h1>{{title}}</h1>
|
|
|
|
{{> footer}}
|
|
----
|
|
|
|
Start the web application by running the `main` function of `BlogApplication.kt`, and go to `http://localhost:8080/`, you should see a sober web page with a "Blog" headline.
|
|
|
|
== Testing with JUnit 5
|
|
|
|
While JUnit 4 is still the default testing framework provided with Spring Boot, JUnit 5 provides various features very handy with Kotlin, including https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testcontext-junit-jupiter-di[autowiring of constructor/method parameters] which allows to use non-nullable `val` properties and the possibility to use `@BeforeAll`/`@AfterAll` on regular non-static methods.
|
|
|
|
=== Switching from JUnit 4 to JUnit 5
|
|
|
|
With Gradle, enable JUnit 5 support by adding the following line to your `build.gradle.kts` file:
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
tasks.withType<Test> {
|
|
useJUnitPlatform()
|
|
}
|
|
----
|
|
|
|
Then exclude `junit` from `spring-boot-starter-test` transitive dependencies and add `junit-jupiter-api` and `junit-jupiter-engine` ones.
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
dependencies {
|
|
testImplementation("org.springframework.boot:spring-boot-starter-test") {
|
|
exclude(module = "junit")
|
|
}
|
|
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
|
}
|
|
----
|
|
|
|
With Maven:
|
|
|
|
`pom.xml`
|
|
[source,xml]
|
|
----
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
<exclusions>
|
|
<exclusion>
|
|
<groupId>junit</groupId>
|
|
<artifactId>junit</artifactId>
|
|
</exclusion>
|
|
</exclusions>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.junit.jupiter</groupId>
|
|
<artifactId>junit-jupiter-engine</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
----
|
|
|
|
Refresh the build configuration, open `BlogApplicationTests` and update imports as bellow and remove `@RunWith(SpringRunner::class)` (Spring Boot 2.1+ test annotations are already annotated with JUnit 5 `@ExtendWith(SpringExtension::class)`).
|
|
|
|
`src/test/kotlin/blog/BlogApplicationTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
import org.junit.jupiter.api.Test
|
|
import org.springframework.boot.test.context.SpringBootTest
|
|
|
|
@SpringBootTest
|
|
class BlogApplicationTests {
|
|
|
|
@Test
|
|
fun contextLoads() {
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
The test should run fine both in command line and in the IDE.
|
|
|
|
=== Writing JUnit 5 tests in Kotlin
|
|
|
|
For the sake of this example, let's create an integration test in order to demonstrate various features:
|
|
|
|
- We use real sentences between backticks instead of camel-case to provide expressive test function names
|
|
- JUnit 5 allows to inject constructor and method parameters, which is a good fit with Kotlin immutable and non-nullable properties
|
|
- This code leverages `getForObject` and `getForEntity` Kotlin extensions (you need to import them)
|
|
|
|
`src/test/kotlin/blog/IntegrationTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
|
|
|
|
@Test
|
|
fun `Assert blog page title, content and status code`() {
|
|
val entity = restTemplate.getForEntity<String>("/")
|
|
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
|
|
assertThat(entity.body).contains("<h1>Blog</h1>")
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
=== Test instance lifecycle
|
|
|
|
Sometimes you need to execute a method before or after all tests of a given class. Like Junit 4, JUnit 5 requires by default these methods to be static (which translates to https://kotlinlang.org/docs/reference/object-declarations.html#companion-objects[`companion object`] in Kotlin, which is quite verbose and not straightforward) because test classes are instantiated one time per test.
|
|
|
|
But Junit 5 allows you to change this default behavior and instantiate test classes one time per class. This can be done in https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle[various ways], here we will use a property file to change the default behavior for the whole project:
|
|
|
|
`src/test/resources/junit-platform.properties`
|
|
[source,properties]
|
|
----
|
|
junit.jupiter.testinstance.lifecycle.default = per_class
|
|
----
|
|
|
|
With this configuration, we can now use `@BeforeAll` and `@AfterAll` annotations on regular methods like shown in updated version of `IntegrationTests` above.
|
|
|
|
`src/test/kotlin/blog/IntegrationTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
|
|
|
|
@BeforeAll
|
|
fun setup() {
|
|
println(">> Setup")
|
|
}
|
|
|
|
@Test
|
|
fun `Assert blog page title, content and status code`() {
|
|
println(">> Assert blog page title, content and status code")
|
|
val entity = restTemplate.getForEntity<String>("/")
|
|
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
|
|
assertThat(entity.body).contains("<h1>Blog</h1>")
|
|
}
|
|
|
|
@Test
|
|
fun `Assert article page title, content and status code`() {
|
|
println(">> TODO")
|
|
}
|
|
|
|
@AfterAll
|
|
fun teardown() {
|
|
println(">> Tear down")
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
== Creating your own extensions
|
|
|
|
Instead of using util classes with abstract methods like in Java, it is usual in Kotlin to provide such functionalities via Kotlin extensions. Here we are going to add a `format()` function to the existing `LocalDateTime` type in order to generate text with the english date format.
|
|
|
|
`src/main/kotlin/blog/Extensions.kt`
|
|
[source,kotlin]
|
|
----
|
|
fun LocalDateTime.format() = this.format(englishDateFormatter)
|
|
|
|
private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }
|
|
|
|
private val englishDateFormatter = DateTimeFormatterBuilder()
|
|
.appendPattern("yyyy-MM-dd")
|
|
.appendLiteral(" ")
|
|
.appendText(ChronoField.DAY_OF_MONTH, daysLookup)
|
|
.appendLiteral(" ")
|
|
.appendPattern("yyyy")
|
|
.toFormatter(Locale.ENGLISH)
|
|
|
|
private fun getOrdinal(n: Int) = when {
|
|
n in 11..13 -> "${n}th"
|
|
n % 10 == 1 -> "${n}st"
|
|
n % 10 == 2 -> "${n}nd"
|
|
n % 10 == 3 -> "${n}rd"
|
|
else -> "${n}th"
|
|
}
|
|
|
|
fun String.toSlug() = toLowerCase()
|
|
.replace("\n", " ")
|
|
.replace("[^a-z\\d\\s]".toRegex(), " ")
|
|
.split(" ")
|
|
.joinToString("-")
|
|
.replace("-+".toRegex(), "-")
|
|
----
|
|
|
|
We will leverage these extensions in the next section.
|
|
|
|
== Persistence with JPA
|
|
|
|
In order to make lazy fetching working as expected, entities should be `open` as described in https://youtrack.jetbrains.com/issue/KT-28525[KT-28525]. We are going to use the Kotlin `allopen` plugin for that purpose.
|
|
|
|
With Gradle:
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
plugins {
|
|
...
|
|
kotlin("plugin.allopen") version "1.2.71"
|
|
}
|
|
|
|
allOpen {
|
|
annotation("javax.persistence.Entity")
|
|
annotation("javax.persistence.Embeddable")
|
|
annotation("javax.persistence.MappedSuperclass")
|
|
}
|
|
----
|
|
|
|
Or with Maven:
|
|
|
|
`pom.xml`
|
|
[source,xml]
|
|
----
|
|
<plugin>
|
|
<artifactId>kotlin-maven-plugin</artifactId>
|
|
<groupId>org.jetbrains.kotlin</groupId>
|
|
<configuration>
|
|
...
|
|
<compilerPlugins>
|
|
...
|
|
<plugin>all-open</plugin>
|
|
</compilerPlugins>
|
|
<pluginOptions>
|
|
<option>all-open:annotation=javax.persistence.Entity</option>
|
|
<option>all-open:annotation=javax.persistence.Embeddable</option>
|
|
<option>all-open:annotation=javax.persistence.MappedSuperclass</option>
|
|
</pluginOptions>
|
|
</configuration>
|
|
</plugin>
|
|
----
|
|
|
|
Then we create our model by using Kotlin https://kotlinlang.org/docs/reference/classes.html#constructors[primary constructor concise syntax] which allows to declare at the same time the properties and the constructor parameters.
|
|
|
|
`src/main/kotlin/blog/Entities.kt`
|
|
[source,kotlin]
|
|
----
|
|
@Entity
|
|
class Article(
|
|
var title: String,
|
|
var headline: String,
|
|
var content: String,
|
|
@ManyToOne var author: User,
|
|
var slug: String = title.toSlug(),
|
|
var addedAt: LocalDateTime = LocalDateTime.now(),
|
|
@Id @GeneratedValue var id: Long? = null)
|
|
|
|
@Entity
|
|
class User(
|
|
var login: String,
|
|
var firstname: String,
|
|
var lastname: String,
|
|
var description: String? = null,
|
|
@Id @GeneratedValue var id: Long? = null)
|
|
----
|
|
|
|
Notice that we are using here our `String.toSlug()` extension to provide a default argument to the `slug` parameter of `Article` constructor. Optional parameters with default values are defined at the last position in order to make it possible to omit them when using positional arguments (Kotlin also supports https://kotlinlang.org/docs/reference/functions.html#named-arguments[named arguments]). Notice that in Kotlin it is not unusual to group concise class declarations in the same file.
|
|
|
|
NOTE: Here we don't use https://kotlinlang.org/docs/reference/data-classes.html[`data` classes] with `val` properties because JPA is not designed to work with immutable classes or the methods generated automatically by `data` classes. If you are using other Spring Data flavor, most of them are designed to support such constructs so you should use classes like `data class User(val login: String, ...)` when using Spring Data MongoDB, Spring Data JDBC, etc.
|
|
|
|
NOTE: While Spring Data JPA makes it possible to use natural IDs (it could have been the `login` property in `User` class) via https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.entity-persistence.saving-entites[`Persistable`], it is not a good fit with Kotlin due to https://youtrack.jetbrains.com/issue/KT-6653[KT-6653], that's why it is recommended to always use entities with generated IDs in Kotlin.
|
|
|
|
|
|
We also declare our Spring Data JPA repositories as following.
|
|
|
|
`src/main/kotlin/blog/Repositories.kt`
|
|
[source,kotlin]
|
|
----
|
|
interface ArticleRepository : CrudRepository<Article, Long> {
|
|
fun findBySlug(slug: String): Article?
|
|
fun findAllByOrderByAddedAtDesc(): Iterable<Article>
|
|
}
|
|
|
|
interface UserRepository : CrudRepository<User, Long> {
|
|
fun findByLogin(login: String): User
|
|
}
|
|
----
|
|
|
|
And we write JPA tests to check basic use cases works as expected.
|
|
|
|
`src/test/kotlin/blog/RepositoriesTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
@DataJpaTest
|
|
class RepositoriesTests @Autowired constructor(
|
|
val entityManager: TestEntityManager,
|
|
val userRepository: UserRepository,
|
|
val articleRepository: ArticleRepository) {
|
|
|
|
@Test
|
|
fun `When findByIdOrNull then return Article`() {
|
|
val juergen = User("springjuergen", "Juergen", "Hoeller")
|
|
entityManager.persist(juergen)
|
|
val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
|
|
entityManager.persist(article)
|
|
entityManager.flush()
|
|
val found = articleRepository.findByIdOrNull(article.id!!)
|
|
assertThat(found).isEqualTo(article)
|
|
}
|
|
|
|
@Test
|
|
fun `When findByLogin then return User`() {
|
|
val juergen = User("springjuergen", "Juergen", "Hoeller")
|
|
entityManager.persist(juergen)
|
|
entityManager.flush()
|
|
val user = userRepository.findByLogin(juergen.login)
|
|
assertThat(user).isEqualTo(juergen)
|
|
}
|
|
}
|
|
----
|
|
|
|
NOTE: We use here the `CrudRepository.findByIdOrNull` Kotlin extension provided by default with Spring Data, which is a nullable variant of the `Optional` based `CrudRepository.findById`. Read the great https://medium.com/@elizarov/null-is-your-friend-not-a-mistake-b63ff1751dd5[Null is your friend, not a mistake] blog post for more details.
|
|
|
|
== Implementing the blog engine
|
|
|
|
We update the "blog" Mustache templates.
|
|
|
|
`src/main/resources/templates/blog.mustache`
|
|
[source]
|
|
----
|
|
{{> header}}
|
|
|
|
<h1>{{title}}</h1>
|
|
|
|
<div class="articles">
|
|
|
|
{{#articles}}
|
|
<section>
|
|
<header class="article-header">
|
|
<h2 class="article-title"><a href="/article/{{slug}}">{{title}}</a></h2>
|
|
<div class="article-meta">By <strong>{{author.firstname}}</strong>, on <strong>{{addedAt}}</strong></div>
|
|
</header>
|
|
<div class="article-description">
|
|
{{headline}}
|
|
</div>
|
|
</section>
|
|
{{/articles}}
|
|
</div>
|
|
|
|
{{> footer}}
|
|
----
|
|
|
|
And we create an "article" new one.
|
|
|
|
`src/main/resources/templates/article.mustache`
|
|
[source]
|
|
----
|
|
{{> header}}
|
|
|
|
<section class="article">
|
|
<header class="article-header">
|
|
<h1 class="article-title">{{article.title}}</h1>
|
|
<p class="article-meta">By <strong>{{article.author.firstname}}</strong>, on <strong>{{article.addedAt}}</strong></p>
|
|
</header>
|
|
|
|
<div class="article-description">
|
|
{{article.headline}}
|
|
|
|
{{article.content}}
|
|
</div>
|
|
</section>
|
|
|
|
{{> footer}}
|
|
----
|
|
|
|
We update the `HtmlController` in order to render blog and article pages with the formatted date. `ArticleRepository` and `MarkdownConverter` constructor parameters will be automatically autowired since `HtmlController` has a single constructor (implicit `@Autowired`).
|
|
|
|
`src/main/kotlin/blog/HtmlController.kt`
|
|
[source,kotlin]
|
|
----
|
|
@Controller
|
|
class HtmlController(private val repository: ArticleRepository) {
|
|
|
|
@GetMapping("/")
|
|
fun blog(model: Model): String {
|
|
model["title"] = "Blog"
|
|
model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
|
|
return "blog"
|
|
}
|
|
|
|
@GetMapping("/article/{slug}")
|
|
fun article(@PathVariable slug: String, model: Model): String {
|
|
val article = repository
|
|
.findBySlug(slug)
|
|
?.render()
|
|
?: throw IllegalArgumentException("Wrong article slug provided")
|
|
model["title"] = article.title
|
|
model["article"] = article
|
|
return "article"
|
|
}
|
|
|
|
fun Article.render() = RenderedArticle(
|
|
slug,
|
|
title,
|
|
headline,
|
|
content,
|
|
author,
|
|
addedAt.format()
|
|
)
|
|
|
|
data class RenderedArticle(
|
|
val slug: String,
|
|
val title: String,
|
|
val headline: String,
|
|
val content: String,
|
|
val author: User,
|
|
val addedAt: String)
|
|
|
|
}
|
|
----
|
|
|
|
Then, we add data initialization to a new `BlogConfiguration` class.
|
|
|
|
`src/main/kotlin/blog/BlogConfiguration.kt`
|
|
[source,kotlin]
|
|
----
|
|
@Configuration
|
|
class BlogConfiguration {
|
|
|
|
@Bean
|
|
fun databaseInitializer(userRepository: UserRepository,
|
|
articleRepository: ArticleRepository) = ApplicationRunner {
|
|
|
|
val smaldini = userRepository.save(User("smaldini", "Stéphane", "Maldini"))
|
|
articleRepository.save(Article(
|
|
title = "Reactor Bismuth is out",
|
|
headline = "Lorem ipsum",
|
|
content = "dolor sit amet",
|
|
author = smaldini
|
|
))
|
|
articleRepository.save(Article(
|
|
title = "Reactor Aluminium has landed",
|
|
headline = "Lorem ipsum",
|
|
content = "dolor sit amet",
|
|
author = smaldini
|
|
))
|
|
}
|
|
}
|
|
----
|
|
|
|
NOTE: Notice the usage of named parameters to make the code more readable.
|
|
|
|
And we also update the integration tests accordingly.
|
|
|
|
`src/test/kotlin/blog/IntegrationTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
|
|
|
|
@BeforeAll
|
|
fun setup() {
|
|
println(">> Setup")
|
|
}
|
|
|
|
@Test
|
|
fun `Assert blog page title, content and status code`() {
|
|
println(">> Assert blog page title, content and status code")
|
|
val entity = restTemplate.getForEntity<String>("/")
|
|
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
|
|
assertThat(entity.body).contains("<h1>Blog</h1>", "Reactor")
|
|
}
|
|
|
|
@Test
|
|
fun `Assert article page title, content and status code`() {
|
|
println(">> Assert article page title, content and status code")
|
|
val title = "Reactor Aluminium has landed"
|
|
val entity = restTemplate.getForEntity<String>("/article/${title.toSlug()}")
|
|
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
|
|
assertThat(entity.body).contains(title, "Lorem ipsum", "dolor sit amet")
|
|
}
|
|
|
|
@AfterAll
|
|
fun teardown() {
|
|
println(">> Tear down")
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
Start (or restart) the web application, and go to `http://localhost:8080/`, you should see the list of articles with clickable links to see a specific article.
|
|
|
|
== Exposing HTTP API
|
|
|
|
We are now going to implement the HTTP API via `@RestController` annotated controllers.
|
|
|
|
`src/main/kotlin/blog/HttpControllers.kt`
|
|
[source,kotlin]
|
|
----
|
|
@RestController
|
|
@RequestMapping("/api/article")
|
|
class ArticleController(private val repository: ArticleRepository) {
|
|
|
|
@GetMapping("/")
|
|
fun findAll() = repository.findAllByOrderByAddedAtDesc()
|
|
|
|
@GetMapping("/{slug}")
|
|
fun findOne(@PathVariable slug: String) =
|
|
repository.findBySlug(slug) ?: throw IllegalArgumentException("Wrong article slug provided")
|
|
|
|
}
|
|
|
|
@RestController
|
|
@RequestMapping("/api/user")
|
|
class UserController(private val repository: UserRepository) {
|
|
|
|
@GetMapping("/")
|
|
fun findAll() = repository.findAll()
|
|
|
|
@GetMapping("/{login}")
|
|
fun findOne(@PathVariable login: String) = repository.findByLogin(login)
|
|
}
|
|
----
|
|
|
|
For tests, instead of integration tests, we are going to leverage `@WebMvcTest` and https://mockk.io/[Mockk] wich is similar to https://site.mockito.org/[Mockito] but better suited for Kotlin.
|
|
|
|
Since `@MockBean` and `@SpyBean` annotations are specific to Mockito, we are going to leverage https://github.com/Ninja-Squad/springmockk[SpringMockK] which provides similar `@MockkBean` and `@SpykBean` annotations for Mockk.
|
|
|
|
With Gradle:
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
testImplementation("org.springframework.boot:spring-boot-starter-test") {
|
|
exclude(module = "junit")
|
|
exclude(module = "mockito-core")
|
|
}
|
|
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
|
testImplementation("com.ninja-squad:springmockk:1.1.2")
|
|
----
|
|
|
|
Or with Maven:
|
|
|
|
`pom.xml`
|
|
[source,xml]
|
|
----
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-starter-test</artifactId>
|
|
<scope>test</scope>
|
|
<exclusions>
|
|
<exclusion>
|
|
<groupId>junit</groupId>
|
|
<artifactId>junit</artifactId>
|
|
</exclusion>
|
|
<exclusion>
|
|
<groupId>org.mockito</groupId>
|
|
<artifactId>mockito-core</artifactId>
|
|
</exclusion>
|
|
</exclusions>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.junit.jupiter</groupId>
|
|
<artifactId>junit-jupiter-engine</artifactId>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>com.ninja-squad</groupId>
|
|
<artifactId>springmockk</artifactId>
|
|
<version>1.1.2</version>
|
|
<scope>test</scope>
|
|
</dependency>
|
|
----
|
|
|
|
`src/test/kotlin/blog/HttpControllersTests.kt`
|
|
[source,kotlin]
|
|
----
|
|
@WebMvcTest
|
|
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {
|
|
|
|
@MockkBean
|
|
private lateinit var userRepository: UserRepository
|
|
|
|
@MockkBean
|
|
private lateinit var articleRepository: ArticleRepository
|
|
|
|
@Test
|
|
fun `List articles`() {
|
|
val juergen = User("springjuergen", "Juergen", "Hoeller")
|
|
val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
|
|
val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
|
|
every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(spring5Article, spring43Article)
|
|
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
|
|
.andExpect(jsonPath("\$.[0].author.login").value(juergen.login))
|
|
.andExpect(jsonPath("\$.[0].slug").value(spring5Article.slug))
|
|
.andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
|
|
.andExpect(jsonPath("\$.[1].slug").value(spring43Article.slug))
|
|
}
|
|
|
|
@Test
|
|
fun `List users`() {
|
|
val juergen = User("springjuergen", "Juergen", "Hoeller")
|
|
val smaldini = User("smaldini", "Stéphane", "Maldini")
|
|
every { userRepository.findAll() } returns listOf(juergen, smaldini)
|
|
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
|
|
.andExpect(status().isOk)
|
|
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
|
|
.andExpect(jsonPath("\$.[0].login").value(juergen.login))
|
|
.andExpect(jsonPath("\$.[1].login").value(smaldini.login))
|
|
}
|
|
}
|
|
----
|
|
|
|
NOTE: `$` needs to be escaped in strings as it is used for string interpolation.
|
|
|
|
== Configuration properties
|
|
|
|
The recommended way to manage your application properties is to leverage `@ConfigurationProperties`. Immutable properties are https://github.com/spring-projects/spring-boot/issues/8762[not yet supported], but you can use `lateinit var` when you need to deal with non-nullable properties.
|
|
|
|
`src/main/kotlin/blog/BlogProperties.kt`
|
|
[source,kotlin]
|
|
----
|
|
@ConfigurationProperties("blog")
|
|
class BlogProperties {
|
|
|
|
lateinit var title: String
|
|
val banner = Banner()
|
|
|
|
class Banner {
|
|
var title: String? = null
|
|
lateinit var content: String
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
Then we enable it at `BlogApplication` level.
|
|
|
|
`src/main/kotlin/blog/BlogApplication.kt`
|
|
[source,kotlin]
|
|
----
|
|
@SpringBootApplication
|
|
@EnableConfigurationProperties(BlogProperties::class)
|
|
class BlogApplication {
|
|
// ...
|
|
}
|
|
----
|
|
|
|
To generate https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#configuration-metadata-annotation-processor[your own metadata] in order to get these custom properties recognized by your IDE, https://kotlinlang.org/docs/reference/kapt.html[kapt should be configured] with the `spring-boot-configuration-processor` dependency as following.
|
|
|
|
`build.gradle.kts`
|
|
[source,kotlin]
|
|
----
|
|
plugins {
|
|
...
|
|
kotlin("kapt") version "1.2.71"
|
|
}
|
|
|
|
dependencies {
|
|
...
|
|
kapt("org.springframework.boot:spring-boot-configuration-processor")
|
|
}
|
|
----
|
|
|
|
NOTE: Annotation processing is not yet supported with Maven due to https://youtrack.jetbrains.com/issue/KT-18022[KT-18022], see https://github.com/spring-io/initializr/issues/438[initializr#438] for more details.
|
|
|
|
In IntelliJ IDEA:
|
|
|
|
- Make sure Spring Boot plugin in enabled in menu File | Settings | Plugins | Spring Boot
|
|
- Enable annotation processing via menu File | Settings | Build, Execution, Deployment | Compiler | Annotation Processors | Enable annotation processing
|
|
- Since https://youtrack.jetbrains.com/issue/KT-15040[Kapt is not yet integrated in IDEA], you need to run manually the command `./gradlew kaptKotlin` to generate the metadata
|
|
|
|
Your custom properties should now be recognized when editing `application.properties` (autocomplete, validation, etc.).
|
|
|
|
`src/main/resources/application.properties`
|
|
[source,properties]
|
|
----
|
|
blog.title=Blog
|
|
blog.banner.title=Warning
|
|
blog.banner.content=The blog will be down tomorrow.
|
|
----
|
|
|
|
Edit the template and the controller accordingly.
|
|
|
|
`src/main/resources/templates/blog.mustache`
|
|
[source]
|
|
----
|
|
{{> header}}
|
|
|
|
<div class="articles">
|
|
|
|
{{#banner.title}}
|
|
<section>
|
|
<header class="banner">
|
|
<h2 class="banner-title">{{banner.title}}</h2>
|
|
</header>
|
|
<div class="banner-content">
|
|
{{banner.content}}
|
|
</div>
|
|
</section>
|
|
{{/banner.title}}
|
|
|
|
...
|
|
|
|
</div>
|
|
|
|
{{> footer}}
|
|
----
|
|
|
|
`src/main/kotlin/blog/HtmlController.kt`
|
|
[source,kotlin]
|
|
----
|
|
@Controller
|
|
class HtmlController(private val repository: ArticleRepository,
|
|
private val properties: BlogProperties) {
|
|
|
|
@GetMapping("/")
|
|
fun blog(model: Model): String {
|
|
model["title"] = properties.title
|
|
model["banner"] = properties.banner
|
|
model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
|
|
return "blog"
|
|
}
|
|
|
|
// ...
|
|
----
|
|
|
|
Restart the web application, refresh `http://localhost:8080/`, you should see the banner on the blog homepage.
|
|
|
|
== Conclusion
|
|
|
|
We have now finished to build this sample Kotlin blog application. The source code https://github.com/spring-guides/tut-spring-boot-kotlin[is available on Github]. You can also have a look to https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#kotlin[Spring Framework] and https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html[Spring Boot] reference documentation if you need more details on specific features.
|