Building web applications with Spring Boot and Kotlin

Spread the love

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, following the online Kotlin Koans tutorial or just using Spring Framework reference documentation which now provides code samples in Kotlin.

Spring Kotlin support is documented in the Spring Framework and Spring Boot reference documentation.

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

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.

  1. Select “Maven Project” as a build tool.
  2. Enter the following artifact coordinates: Spring Boot and Kotlin Demo
  3. Add the following dependencies:
    • Spring Web
    • Mustache
    • Spring Data JPA
    • H2 Database
    • Spring Boot DevTools
  4. Click “Generate Project”.
New Project start for Daily code buffer

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

Maven Build

Plugins

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

In order to be able to use Kotlin non-nullable properties with JPA, Kotlin JPA plugin is also enabled. It generates no-arg constructors for any class annotated with  @Entity@MappedSuperclass or @Embeddable.

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

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

<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>
  <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>
</dependencies>

Understanding the generated Application

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\SpringBootAndKotlinDemoApplication.kt

package com.dailycodebuffer.example.SpringBootandKotlinDemo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class SpringBootAndKotlinDemoApplication

fun main(args: Array<String>) {
  runApplication<SpringBootAndKotlinDemoApplication>(*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<SpringBootAndKotlinDemoApplication>(*args)  is Kotlin idiomatic alternative to  SpringApplication.run(SpringBootAndKotlinDemoApplication::class.java, *args)  and can be used to customize the application with following syntax.

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\SpringBootAndKotlinDemoApplication.kt

fun main(args: Array<String>) {
  runApplication<SpringBootAndKotlinDemoApplication>(*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\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\HtmlController.kt

package com.dailycodebuffer.example.SpringBootandKotlinDemo.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 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 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

<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  SpringBootAndKotlinDemoApplication.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 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:

  • 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 read-only and non-nullable properties
  • This code leverages getForObject and getForEntity Kotlin extensions (you need to import them)

src\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\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 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 as shown in the updated version of IntegrationTests above.

src\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\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\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\IntegrationTests.kt

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, we are going to use the Kotlin  allopen plugin for that purpose.

pom.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’s primary constructor concise syntax which allows to declare at the same time the properties and the constructor parameters.

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\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)

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

Here we don’t use 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.

We also declare our Spring Data JPA repositories as following.

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\Repositories.kt

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 that works as expected.

src\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\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 anyuser = User("anyuser", "Any", "User")
        entityManager.persist(anyuser)
        val article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", anyuser)
        entityManager.persist(article)
        entityManager.flush()
        val found = articleRepository.findByIdOrNull(article.id!!)
        assertThat(found).isEqualTo(article)
    }

    @Test
    fun `When findByLogin then return User`() {
        val anyuser = User("anyuser", "Any", "User")
        entityManager.persist(anyuser)
        entityManager.flush()
        val user = userRepository.findByLogin(anyuser.login)
        assertThat(user).isEqualTo(anyuser)
    }
}

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

<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

{{> 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\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\HttpControllers.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\dailycodebuffer\example\SpringBootandKotlinDemo\blog\BlogConfiguration.kt

@Configuration
class BlogConfiguration {

    @Bean
    fun databaseInitializer(userRepository: UserRepository,
                            articleRepository: ArticleRepository) = ApplicationRunner {

        val anyuser = userRepository.save(User("anyuser", "Any", "User"))
        articleRepository.save(Article(
                title = "Reactor Bismuth is out",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = anyuser
        ))
        articleRepository.save(Article(
                title = "Reactor Aluminium has landed",
                headline = "Lorem ipsum",
                content = "dolor sit amet",
                author = anyuser
        ))
    }
}

Notice the usage of named parameters to make the code more readable.

And we also update the integration tests accordingly.

src\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\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)

    }

    @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\com\dailycodebuffer\example\SpringBootandKotlinDemo\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) ?: 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) ?: ResponseStatusException(HttpStatus.NOT_FOUND, "This user does not exist")
}

For tests, instead of integration tests, we are going to leverage  @WebMvcTest and Mockk which is similar to Mockito but better suited for Kotlin.

Since @MockBean and @SpyBean annotations are specific to Mockito, we are going to leverage SpringMockK which provides similar @MockkBean and @SpykBean annotations for Mockk.

pom.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.3</version>
  <scope>test</scope>
</dependency>

src\test\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\HttpControllersTests.kt

@WebMvcTest
class HttpControllersTests(@Autowired val mockMvc: MockMvc) {

    @MockkBean
    private lateinit var userRepository: UserRepository

    @MockkBean
    private lateinit var articleRepository: ArticleRepository

    @Test
    fun `List articles`() {
        val firstUser = User("firstUser", "First", "User")
        val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", firstUser)
        val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", firstUser)
        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))
                .andExpect(jsonPath("\$.[0].author.login").value(firstUser.login))
                .andExpect(jsonPath("\$.[0].slug").value(spring5Article.slug))
                .andExpect(jsonPath("\$.[1].author.login").value(firstUser.login))
                .andExpect(jsonPath("\$.[1].slug").value(spring43Article.slug))
    }

    @Test
    fun `List users`() {
        val firstUser = User("firstUser", "First", "User")
        val anyuser = User("anyuser", "Any", "User")
        every { userRepository.findAll() } returns listOf(firstUser, anyuser)
        mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk)
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("\$.[0].login").value(firstUser.login))
                .andExpect(jsonPath("\$.[1].login").value(anyuser.login))
    }
}

$ 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 leverage  @ConfigurationProperties with @ConstructorBinding in order to be able to use read-only properties.

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\blog\BlogProperties.kt

@ConstructorBinding
@ConfigurationProperties("blog")
data class BlogProperties(var title: String, val banner: Banner) {
  data class Banner(val title: String? = null, val content: String)
}

Then we enable it at SpringBootAndKotlinDemoApplication level.

src\main\kotlin\com\dailycodebuffer\example\SpringBootandKotlinDemo\SpringBootAndKotlinDemoApplication.kt

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

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\com\dailycodebuffer\example\SpringBootandKotlinDemo\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 is available on Github.

Leave a Reply