This tutorial shows you how to build efficiently a sample blog application by combining the power of Spring Boot and Kotlin.

If you are starting with Kotlin, you can learn the language by reading the reference documentation and following the online Kotlin Koans tutorial.

Spring Kotlin support is documented in the Spring Framework and Spring Boot reference documentation. If you need help, search or ask questions with the spring and kotlin tags on StackOverflow or come discuss in the #spring channel of Kotlin Slack.

Creating a New Project

First we need to create a Spring Boot application, which can be done in a number of ways. For the sake of example, we will use Gradle build system since this is the most popular in Kotlin ecosystem, but feel free to use Maven if you prefer it (a Maven pom.xml equivalent to the Gradle build is available as part of the sample blog project).

Using the Initializr Website

Go to https://start.spring.io and choose Kotlin language. You can also directly go to https://start.spring.io/#!language=kotlin in order to get Kotlin preselected.

Then choose Gradle build system, "blog" artifact, "blog" package name (in advanced settings) and also add "Web", "Mustache", "JPA" and "H2" dependencies as starting points, then click on "Generate Project".

initializr

The .zip file contains a standard Gradle project in the root directory, so you might want to create an empty directory before you unpack it.

Using command line

You can use the Initializr HTTP API from the command line with, for example, curl on a UN*X like system:

$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d type=gradle-project -d language=kotlin -d style=web,mustache,jpa,h2 -d packageName=blog -d name=Blog -o blog.zip

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:

  • Package name: "blog"

  • Artifact: "blog"

  • Type: Gradle Project

  • Language: Kotlin

  • Name: "Blog"

  • Dependencies: "Web", "Mustache", JPA" and "H2"

Understanding the generated project

Gradle build

Plugins

In addition to the obvious Kotlin Gradle plugin, the default configuration also declares the 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.

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}")
  }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

Compiler options

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. 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 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 platform types for which null-checks are relaxed. 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

sourceCompatibility = 1.8
compileKotlin {
  kotlinOptions {
    freeCompilerArgs = ["-Xjsr305=strict"]
    jvmTarget = "1.8"
  }
}
compileTestKotlin {
  kotlinOptions {
    freeCompilerArgs = ["-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 (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)

build.gradle

dependencies {
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.boot:spring-boot-starter-mustache')
  compile('com.fasterxml.jackson.module:jackson-module-kotlin')
  compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
  compile("org.jetbrains.kotlin:kotlin-reflect")
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

Spring Boot Gradle plugin automatically uses the Kotlin version declared on the Kotlin Gradle plugin.

Application

src/main/kotlin/blog/BlogApplication.kt

package 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

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

package blog.web

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 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").

We also need to create the associated Mustache templates.

src/main/resources/templates/header.mustache

<html>
<head>
  <title>{{title}}</title>
</head>
<body>

src/main/resources/templates/footer.mustache

</body>
</html>

src/main/resources/templates/blog.mustache

{{> 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 autowiring of contructor/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

First make sure you are using Gradle 4.6+ by running ./gradlew -version in order to be able to leverage native JUnit 5 support. If you are using an older version, you can update it by running ./gradlew wrapper --gradle-version 4.7 for a more recent Gradle release.

Enable JUnit 5 support by adding the following line to your build.gradle file:

build.gradle

test {
  useJUnitPlatform()
}

Then exclude junit from spring-boot-starter-test transitive dependencies and add junit-jupiter-api and junit-jupiter-engine ones.

build.gradle

dependencies {
  testCompile('org.springframework.boot:spring-boot-starter-test') {
    exclude module: 'junit'
  }
  testImplementation('org.junit.jupiter:junit-jupiter-api')
  testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}

Refresh Gradle configuration, and open BlogApplicationTests to replace @RunWith(SpringRunner::class) by @ExtendWith(SpringExtension::class).

src/test/kotlin/blog/BlogApplicationTests.kt

@ExtendWith(SpringExtension::class)
@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

@ExtendWith(SpringExtension::class)
@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 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 various ways, here we will use a property file to change the default behavior for the whole project:

src/test/resources/junit-platform.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

@ExtendWith(SpringExtension::class)
@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")
  }

}

Persistence with JPA

In order to be able to use Kotlin immutable classes, we need to enable Kotlin JPA plugin. It will generate no-arg constructors for any class annotated with @Entity, @MappedSuperclass or @Embeddable.

build.gradle

buildscript {
  dependencies {
    classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
  }
}
apply plugin: 'kotlin-jpa'

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().

src/main/kotlin/blog/Model.kt

@Entity
data class Article(
    val title: String,
    val headline: String,
    val content: String,
    @ManyToOne @JoinColumn val author: User,
    @Id @GeneratedValue val id: Long? = null,
    val addedAt: LocalDateTime = LocalDateTime.now())

@Entity
data class User(
    @Id val login: String,
    val firstname: String,
    val lastname: String,
    val description: String? = null)

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 named arguments). Notice that in Kotlin it is not unusual to group concise class declarations in the same file.

We also declares our Spring Data JPA repositories as following.

src/main/kotlin/blog/Repositories.kt

interface ArticleRepository : CrudRepository<Article, Long> {
  fun findAllByOrderByAddedAtDesc(): Iterable<Article>
}

interface UserRepository : CrudRepository<User, String>

And we write JPA tests to check basic use case works as expected.

src/test/kotlin/blog/RepositoriesTests.kt

@ExtendWith(SpringExtension::class)
@DataJpaTest
class RepositoriesTests(@Autowired val entityManager: TestEntityManager,
                        @Autowired val userRepository: UserRepository,
                        @Autowired val articleRepository: ArticleRepository) {

  @Test
  fun `When findById 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.findById(article.id!!)

    assertThat(found.get()).isEqualTo(article)
  }

  @Test
  fun `When findById then return User`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    entityManager.persist(juergen)
    entityManager.flush()

    val found = userRepository.findById(juergen.login)

    assertThat(found.get()).isEqualTo(juergen)
  }

}

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

fun LocalDateTime.format() = this.format(englishDateFormatter)

private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }

private val englishDateFormatter = DateTimeFormatterBuilder()
    .appendPattern("MMMM")
    .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 this extension in the next section.

Implementing the blog engine

The blog engine we are implementing needs to render Markdown to HTML, and we are going to use commonmark library for that purpose.

build.gradle

dependencies {
  compile("com.atlassian.commonmark:commonmark:0.11.0")
  compile("com.atlassian.commonmark:commonmark-ext-autolink:0.11.0")
}

We introduce a MarkdownConverter bean, which leverages Kotlin function type.

src/main/kotlin/blog/MarkdownConverter.kt

@Service
class MarkdownConverter : (String?) -> String {

  private val parser = Parser.builder().extensions(listOf(AutolinkExtension.create())).build()
  private val renderer = HtmlRenderer.builder().build()

  override fun invoke(input: String?): String {
    if (input == null || input == "") {
      return ""
    }
    return renderer.render(parser.parse(input))
  }
}

And we provide a custom Mustache.Compiler bean to be able to render HTML.

src/main/kotlin/blog/BlogApplication.kt

@SpringBootApplication
class BlogApplication {

  @Bean
  fun mustacheCompiler(loader: Mustache.TemplateLoader?) =
      Mustache.compiler().escapeHTML(false).withLoader(loader)
}

The nullable Mustache.TemplateLoader? means that it is an optional bean (in order to avoid failure when running JPA-only tests).

We update the "blog" Mustache templates.

src/main/resources/templates/blog.mustache

{{> header}}

<h1>{{title}}</h1>

<div class="articles">

  {{#articles}}
    <section>
      <header class="article-header">
        <h2 class="article-title"><a href="/article/{{id}}">{{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

{{> 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 rendered markdown and 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

@Controller
class HtmlController(private val repository: ArticleRepository,
           private val markdownConverter: MarkdownConverter) {

    @GetMapping("/")
    fun blog(model: Model): String {
      model["title"] = "Blog"
      model["articles"] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
      return "blog"
    }

    @GetMapping("/article/{id}")
    fun article(@PathVariable id: Long, model: Model): String {
      val article = repository
          .findById(id)
          .orElseThrow { IllegalArgumentException("Wrong article id provided") }
          .render()
      model["title"] = article.title
      model["article"] = article
      return "article"
    }

    fun Article.render() = RenderedArticle(
        title,
        markdownConverter.invoke(headline),
        markdownConverter.invoke(content),
        author,
        id,
        addedAt.format()
    )

    data class RenderedArticle(
        val title: String,
        val headline: String,
        val content: String,
        val author: User,
        val id: Long?,
        val addedAt: String)

}

We add data initialization to BlogApplication.

src/main/kotlin/blog/BlogApplication.kt

@Bean
fun databaseInitializer(userRepository: UserRepository,
                        articleRepository: ArticleRepository) = CommandLineRunner {
  val smaldini = User("smaldini", "Stéphane", "Maldini")
  userRepository.save(smaldini)

  articleRepository.save(Article(
      "Reactor Bismuth is out",
      "Lorem ipsum",
      "dolor **sit** amet https://projectreactor.io/",
      smaldini,
      1
  ))
  articleRepository.save(Article(
      "Reactor Aluminium has landed",
      "Lorem ipsum",
      "dolor **sit** amet https://projectreactor.io/",
      smaldini,
      2
  ))
}

And we also update the integration tests accordingly.

src/test/kotlin/blog/IntegrationTests.kt

@ExtendWith(SpringExtension::class)
@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 entity = restTemplate.getForEntity<String>("/article/2")
    assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
    assertThat(entity.body).contains("Reactor Aluminium has landed",,
        "<a href=\"https://projectreactor.io/\">https://projectreactor.io/</a>")
  }

  @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/HttpApi.kt

@RestController
@RequestMapping("/api/article")
class ArticleController(private val repository: ArticleRepository,
            private val markdownConverter: MarkdownConverter) {

  @GetMapping("/")
  fun findAll() = repository.findAllByOrderByAddedAtDesc()

  @GetMapping("/{id}")
  fun findOne(@PathVariable id: Long, @RequestParam converter: String?) = when (converter) {
    "markdown" -> repository.findById(id).map { it.copy(
        headline = markdownConverter.invoke(it.headline),
        content = markdownConverter.invoke(it.content)) }
    null -> repository.findById(id)
    else -> throw IllegalArgumentException("Only markdown converter is supported")
  }
}

@RestController
@RequestMapping("/api/user")
class UserController(private val repository: UserRepository) {

  @GetMapping("/")
  fun findAll() = repository.findAll()

  @GetMapping("/{login}")
  fun findOne(@PathVariable login: String) = repository.findById(login)
}

For tests, instead of integration tests we choose to leverage @WebMvcTest and @MockBean to test only the web layer.

src/test/kotlin/blog/HttpApiTests.kt

@ExtendWith(SpringExtension::class)
@WebMvcTest
class HttpApiTests(@Autowired val mockMvc: MockMvc) {

  @MockBean
  private lateinit var userRepository: UserRepository

  @MockBean
  private lateinit var articleRepository: ArticleRepository

  @MockBean
  private lateinit var markdownConverter: MarkdownConverter

  @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, 1)
    val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen, 2)
    whenever(articleRepository.findAllByOrderByAddedAtDesc()).thenReturn(listOf(spring5Article, spring43Article))
    whenever(markdownConverter.invoke(any())).thenAnswer { it.arguments[0] }
    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].id").value(spring5Article.id!!))
        .andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
        .andExpect(jsonPath("\$.[1].id").value(spring43Article.id!!))
  }

  @Test
  fun `List users`() {
    val juergen = User("springjuergen", "Juergen", "Hoeller")
    val smaldini = User("smaldini", "Stéphane", "Maldini")
    whenever(userRepository.findAll()).thenReturn(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))
  }

}

Notice that when is a reserved Kotlin keyword, that’s why we choose to use mockito-kotlin library which provides a whenever alias (using escaped `when` is also possible). In order to use it, add following dependency.

build.gradle

dependencies {
  testCompile("com.nhaarman:mockito-kotlin:1.5.0")
}

$ also needs to be escaped in strings as it is used for string interpolation. There is not yet @MockBean JUnit 5 parameter resolver, so we need to use lateinit var for now.

Configuration properties

The recommended way to manage your application properties is to leverage @ConfigurationProperties. Immutable properties are not yet supported, but you can use lateinit var when you need to deal with non-nullable properties.

src/main/kotlin/blog/BlogProperties.kt

@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

@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
  // ...
}

To generate your own metadata in order to get these custom properties recognized by your IDE, kapt should be configured with the spring-boot-configuration-processor dependency as following.

build.gradle

apply plugin: 'kotlin-kapt'
dependencies {
  kapt("org.springframework.boot:spring-boot-configuration-processor")
}

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, Deployement | Compiler | Annotation Processors | Enable annotation processing

  • Since 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

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

{{> 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

@Controller
class HtmlController(private val repository: ArticleRepository,
           private val markdownConverter: MarkdownConverter,
           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 is available on Github. You can also have a look to Spring Framework and Spring Boot reference documentation if you need more details on specific features.