Building web applications with Spring Boot and Kotlin :: Learn how to easily build and test web applications with Spring, Kotlin, Junit 5 and JPA
:toc: :icons: font :source-highlighter: prettify :project_id: tut-spring-boot-kotlin :images: https://raw.githubusercontent.com/spring-guides/tut-spring-boot-kotlin/master/images :tabsize: 2
This tutorial shows you how to build efficiently a sample blog application by combining the power of https://spring.io/projects/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], following the online https://play.kotlinlang.org/[Kotlin Koans tutorial] or just using https://docs.spring.io/spring/docs/current/spring-framework-reference/[Spring Framework reference documentation] which now provides code samples in Kotlin.
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-kotlin to have Kotlin and Gradle selected by default.
. Select "Gradle - Kotlin" or "Maven" depending on which build tool you want to use
. Enter the following artifact coordinates: `blog`
. Add the following dependencies:
- Spring Web
- Mustache
- Spring Data JPA
- H2 Database
- Spring Boot DevTools
. Click "Generate Project".
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/html/#command-line[from the command line] with, for example, curl on a UN*X like system:
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:
[[gradle-build]] == Understanding the Gradle Build
If you're using a Maven Build, you can <<maven-build, skip to the dedicated section>>.
=== Plugins
In addition to the obvious https://kotlinlang.org/docs/gradle.html[Kotlin Gradle plugin], the default configuration declares the https://kotlinlang.org/docs/all-open-plugin.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
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
=== Compiler options
One of Kotlin's key features is https://kotlinlang.org/docs/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/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.
build.gradle.kts
=== Dependencies
2 Kotlin specific libraries are required (the standard library is added automatically with Gradle) for such Spring Boot web application and configured by default:
kotlin-reflect
is Kotlin reflection libraryjackson-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
Recent versions of H2 require special configuration to properly escape reserved keywords like user
.
src/main/resources/application.properties
Spring Boot Gradle plugin automatically uses the Kotlin version declared via the Kotlin Gradle plugin.
You can now take a <<understanding-generated-app, deeper look at the generated application>>.
[[maven-build]] == Understanding the 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
One of Kotlin's key features is https://kotlinlang.org/docs/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
is the Kotlin standard librarykotlin-reflect
is Kotlin reflection libraryjackson-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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
[[understanding-generated-app]] == Understanding the generated Application
src/main/kotlin/com/example/blog/BlogApplication.kt
package com.example.blog
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication
@SpringBootApplication class BlogApplication
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/com/example/blog/BlogApplication.kt
== Writing your first Kotlin controller
Let's create a simple controller to display a simple web page.
src/main/kotlin/com/example/blog/HtmlController.kt
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/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 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
src/main/resources/templates/footer.mustache
src/main/resources/templates/blog.mustache
{{> header}}
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
JUnit 5 now used by default in Spring Boot 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.
=== Writing JUnit 5 tests in Kotlin
For the sake of this example, let's create an integration test in order to demonstrate various features:
getForObject
and getForEntity
Kotlin extensions (you need to import them)src/test/kotlin/com/example/blog/IntegrationTests.kt
@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/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
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/com/example/blog/IntegrationTests.kt
@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/com/example/blog/Extensions.kt
fun LocalDateTime.format(): String = 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" }
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
plugins { ... kotlin("plugin.allopen") version "1.9.22" }
Or with Maven:
pom.xml
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/com/example/blog/Entities.kt
@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)
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/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/com/example/blog/Repositories.kt
interface ArticleRepository : CrudRepository<Article, Long> { fun findBySlug(slug: String): Article? fun findAllByOrderByAddedAtDesc(): Iterable }
And we write JPA tests to check whether basic use cases work as expected.
src/test/kotlin/com/example/blog/RepositoriesTests.kt
@DataJpaTest class RepositoriesTests @Autowired constructor( val entityManager: TestEntityManager, val userRepository: UserRepository, val articleRepository: ArticleRepository) {
@Test
fun `When findByIdOrNull then return Article`() {
val johnDoe = User("johnDoe", "John", "Doe")
entityManager.persist(johnDoe)
val article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
entityManager.persist(article)
entityManager.flush()
val found = articleRepository.findByIdOrNull(article.id!!)
assertThat(found).isEqualTo(article)
}
@Test
fun `When findByLogin then return User`() {
val johnDoe = User("johnDoe", "John", "Doe")
entityManager.persist(johnDoe)
entityManager.flush()
val user = userRepository.findByLogin(johnDoe.login)
assertThat(user).isEqualTo(johnDoe)
}
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
{{> header}}
{{#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}}
And we create an "article" new one.
src/main/resources/templates/article.mustache
{{> header}}
<div class="article-description">
{{article.headline}}
{{article.content}}
</div>
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/com/example/blog/HtmlController.kt
@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 ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
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/com/example/blog/BlogConfiguration.kt
@Configuration class BlogConfiguration {
@Bean
fun databaseInitializer(userRepository: UserRepository,
articleRepository: ArticleRepository) = ApplicationRunner {
val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
articleRepository.save(Article(
title = "Lorem",
headline = "Lorem",
content = "dolor sit amet",
author = johnDoe
))
articleRepository.save(Article(
title = "Ipsum",
headline = "Ipsum",
content = "dolor sit amet",
author = johnDoe
))
}
NOTE: Notice the usage of named parameters to make the code more readable.
And we also update the integration tests accordingly.
src/test/kotlin/com/example/blog/IntegrationTests.kt
@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>", "Lorem")
}
@Test
fun `Assert article page title, content and status code`() {
println(">> Assert article page title, content and status code")
val title = "Lorem"
val entity = restTemplate.getForEntity<String>("/article/${title.toSlug()}")
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains(title, "Lorem", "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/com/example/blog/HttpControllers.kt
@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 ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
}
@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) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
For tests, instead of integration tests, we are going to leverage @WebMvcTest
and https://mockk.io/[Mockk] which 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
Or with Maven:
pom.xml
src/test/kotlin/com/example/blog/HttpControllersTests.kt
@WebMvcTest class HttpControllersTests(@Autowired val mockMvc: MockMvc) {
@MockkBean
lateinit var userRepository: UserRepository
@MockkBean
lateinit var articleRepository: ArticleRepository
@Test
fun `List articles`() {
val johnDoe = User("johnDoe", "John", "Doe")
val lorem5Article = Article("Lorem", "Lorem", "dolor sit amet", johnDoe)
val ipsumArticle = Article("Ipsum", "Ipsum", "dolor sit amet", johnDoe)
every { articleRepository.findAllByOrderByAddedAtDesc() } returns listOf(lorem5Article, ipsumArticle)
mockMvc.perform(get("/api/article/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].author.login").value(johnDoe.login))
.andExpect(jsonPath("\$.[0].slug").value(lorem5Article.slug))
.andExpect(jsonPath("\$.[1].author.login").value(johnDoe.login))
.andExpect(jsonPath("\$.[1].slug").value(ipsumArticle.slug))
}
@Test
fun `List users`() {
val johnDoe = User("johnDoe", "John", "Doe")
val janeDoe = User("janeDoe", "Jane", "Doe")
every { userRepository.findAll() } returns listOf(johnDoe, janeDoe)
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].login").value(johnDoe.login))
.andExpect(jsonPath("\$.[1].login").value(janeDoe.login))
}
NOTE: $
needs to be escaped in strings as it is used for string interpolation.
== Configuration properties
In Kotlin, the recommended way to manage your application properties is to use read-only properties.
src/main/kotlin/com/example/blog/BlogProperties.kt
Then we enable it at BlogApplication
level.
src/main/kotlin/com/example/blog/BlogApplication.kt
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
plugins { ... kotlin("kapt") version "1.9.22" }
NOTE: Note that some features (such as detecting the default value or deprecated items) are not working due to limitations in the model kapt provides. Also 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:
./gradlew kaptKotlin
to generate the metadataYour custom properties should now be recognized when editing application.properties
(autocomplete, validation, etc.).
src/main/resources/application.properties
Edit the template and the controller accordingly.
src/main/resources/templates/blog.mustache
{{> header}}
{{#banner.title}}
<section>
<header class="banner">
<h2 class="banner-title">{{banner.title}}</h2>
</header>
<div class="banner-content">
{{banner.content}}
</div>
</section>
{{/banner.title}}
...
src/main/kotlin/com/example/blog/HtmlController.kt
@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.