- Creating a New Project
- Null safety
- Understanding the Build
- Database Schema
- Understanding the generated Application
- Writing your first Kotlin controller
- Testing with JUnit
- Creating your own extensions
- Fetching with Spring Data JDBC
- Implementing the blog engine
- Exposing HTTP API
- Configuration properties
- Conclusion
This tutorial shows you how to efficiently build 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 taking the Kotlin tour or using the Spring Framework reference documentation, which provides code samples in Kotlin.
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.
First, we need to create a Spring Boot application, which can be done in a number of ways.
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 JDBC
-
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.
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 language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jdbc,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zipAdd -d type=gradle-project if you want to use Gradle.
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: "Gradle - Kotlin" or "Maven"
-
Language: Kotlin
-
Name: "Blog"
-
Dependencies: "Spring Web Starter", "Mustache", "Spring Data JDBC", "H2 Database", and "Spring Boot DevTools"
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 provides null safety of its APIs via tooling-friendly JSpecify annotations. Kotlin translates JSpecify annotations on Java APIs to Kotlin nullability out-of-the-box.
In addition to the obvious Kotlin Gradle or 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.
Strict interpretation of JSR 305 annotations (for libraries using them) and defaults when no use-site targets are specified are also configured.
plugins {
id("org.springframework.boot") version "4.0.0"
id("io.spring.dependency-management") version "1.1.7"
kotlin("jvm") version "2.2.21"
kotlin("plugin.spring") version "2.2.21"
}
// ...
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict", "-Xannotation-default-target=param-property")
}
}<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>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
<arg>-Xannotation-default-target=param-property</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>Kotlin-specific libraries are required for such a Spring Boot web application and configured by default:
-
kotlin-stdlibis the Kotlin standard library (added automatically with Gradle) -
kotlin-reflectis the Kotlin reflection library -
jackson-module-kotlinadds 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)
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("tools.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-data-jdbc-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
}<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</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-webmvc</artifactId>
</dependency>
<dependency>
<groupId>tools.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</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-data-jdbc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>We need to create a database schema file since Spring Data JDBC doesn’t auto-create tables.
src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS "users" (
"ID" BIGINT AUTO_INCREMENT PRIMARY KEY,
"LOGIN" VARCHAR(255) NOT NULL UNIQUE,
"FIRSTNAME" VARCHAR(255) NOT NULL,
"LASTNAME" VARCHAR(255) NOT NULL,
"DESCRIPTION" TEXT
);
CREATE TABLE IF NOT EXISTS "article" (
"ID" BIGINT AUTO_INCREMENT PRIMARY KEY,
"TITLE" VARCHAR(255) NOT NULL,
"HEADLINE" VARCHAR(500) NOT NULL,
"CONTENT" TEXT NOT NULL,
"author_id" BIGINT NOT NULL,
"SLUG" VARCHAR(255) NOT NULL UNIQUE,
"ADDED_AT" TIMESTAMP NOT NULL,
FOREIGN KEY ("author_id") REFERENCES "users"("ID")
);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
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 the following syntax:
src/main/kotlin/com/example/blog/BlogApplication.kt
fun main(args: Array<String>) {
runApplication<BlogApplication>(*args) {
setBannerMode(Banner.Mode.OFF)
}
}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 Kotlin extension that allows adding 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 BlogApplication.kt, and go to http://localhost:8080/, you should see a sober web page with a "Blog" headline.
JUnit, used by default in Spring Boot, provides various features very handy with Kotlin, including autowiring of constructor/method parameters which allows using non-nullable val properties and the possibility to use @BeforeAll/@AfterAll on regular non-static methods.
For the sake of this example, let’s create an integration test to demonstrate various features:
-
We use real sentences between backticks instead of a camel-case to provide expressive test function names
-
JUnit allows injecting constructor and method parameters, which is a good fit with Kotlin read-only and non-nullable properties
-
This code leverages the
expectBodyKotlin extension (you need to import it)
src/test/kotlin/com/example/blog/IntegrationTests.kt
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
restClient.get().uri("/")
.exchangeSuccessfully()
.expectBody<String>()
.value { assertThat(it).contains("<h1>Blog</h1>", "Lorem") }
}
}Sometimes you need to execute a method before or after all tests of a given class. JUnit 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 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_classWith this configuration, we can now use the @BeforeAll and @AfterAll annotations on regular methods like shown in the updated version of IntegrationTests above.
src/test/kotlin/com/example/blog/IntegrationTests.kt
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {
@BeforeAll
fun setup() {
println(">> Setup")
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
restClient.get().uri("/")
.exchangeSuccessfully()
.expectBody<String>()
.value { assertThat(it).contains("<h1>Blog</h1>", "Lorem") }
}
@Test
fun `Assert article page title, content and status code`() {
println(">> TODO")
}
@AfterAll
fun teardown() {
println(">> Tear down")
}
}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"
}
fun String.toSlug() = lowercase(Locale.getDefault())
.replace("\n", " ")
.replace("[^a-z\\d\\s]".toRegex(), " ")
.split(" ")
.joinToString("-")
.replace("-+".toRegex(), "-")We will leverage these extensions in the next section.
To fetch data, with Spring Data JDBC, we use immutable data classes, which is a more idiomatic approach in Kotlin.
We create our model by using Kotlin primary constructor concise syntax and data classes which allows declaring at the same time the properties and the constructor parameters.
src/main/kotlin/com/example/blog/Entities.kt
@Table("article")
data class Article(
val title: String,
val headline: String,
val content: String,
@Column("author_id")
val author: AggregateReference<User, Long>,
val slug: String = title.toSlug(),
val addedAt: LocalDateTime = LocalDateTime.now(),
@Id val id: Long? = null
)
@Table("users")
data class User(
val login: String,
val firstname: String,
val lastname: String,
val description: String? = null,
@Id val 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 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.
The @Table annotation maps the classes to database tables, and @Column is used for the foreign key relationship. Spring Data JDBC uses AggregateReference to represent foreign key relationships, which only stores the ID rather than loading the entire related entity.
We also declare our Spring Data JDBC repositories as follows:
src/main/kotlin/com/example/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 JDBC tests to check whether basic use cases work as expected.
src/test/kotlin/com/example/blog/RepositoriesTests.kt
@DataJdbcTest
class RepositoriesTests @Autowired constructor(
val userRepository: UserRepository,
val articleRepository: ArticleRepository
) {
@Test
fun `When findByIdOrNull then return Article`() {
val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
val article = articleRepository.save(
Article("Lorem", "Lorem", "dolor sit amet", AggregateReference.to(johnDoe.id!!))
)
val found = articleRepository.findByIdOrNull(article.id!!)
assertThat(found).isEqualTo(article)
}
@Test
fun `When findByLogin then return User`() {
val johnDoe = userRepository.save(User("johnDoe", "John", "Doe"))
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 Null is your friend, not a mistake blog post for more details.
|
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. Both ArticleRepository and UserRepository constructor parameters will be automatically autowired since HtmlController has a single constructor (implicit @Autowired). Notice that with Spring Data JDBC, we need to manually resolve the author relationship by looking up the user via their ID.
src/main/kotlin/com/example/blog/HtmlController.kt
@Controller
class HtmlController(
private val articleRepository: ArticleRepository,
private val userRepository: UserRepository,
private val properties: BlogProperties
) {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = properties.title
model["banner"] = properties.banner
model["articles"] = articleRepository.findAllByOrderByAddedAtDesc()
.map { it.render() }
return "blog"
}
@GetMapping("/article/{slug}")
fun article(@PathVariable slug: String, model: Model): String {
val article = articleRepository.findBySlug(slug)
?: throw ResponseStatusException(NOT_FOUND, "This article does not exist")
val renderedArticle = article.render()
model["title"] = renderedArticle.title
model["article"] = renderedArticle
return "article"
}
private fun Article.render(): RenderedArticle {
val author = userRepository.findById(author.id)
.orElseThrow { ResponseStatusException(NOT_FOUND, "Author not found") }
return 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. Notice that with Spring Data JDBC, we use AggregateReference.to() to create foreign key references.
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 = AggregateReference.to(johnDoe.id!!)
)
)
articleRepository.save(
Article(
title = "Ipsum",
headline = "Ipsum",
content = "dolor sit amet",
author = AggregateReference.to(johnDoe.id)
)
)
}
}|
Note
|
Notice the usage of named parameters to make the code more readable. The AggregateReference.to() method creates a reference to the related entity using only its ID.
|
And we also update the integration tests accordingly.
src/test/kotlin/com/example/blog/IntegrationTests.kt
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class IntegrationTests(@Autowired val restClient: RestTestClient) {
@BeforeAll
fun setup() {
println(">> Setup")
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code")
restClient.get().uri("/")
.exchangeSuccessfully()
.expectBody<String>()
.value { assertThat(it).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"
restClient.get().uri("/article/${title.toSlug()}")
.exchangeSuccessfully()
.expectBody<String>()
.value { assertThat(it).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.
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 articleRepository: ArticleRepository,
private val userRepository: UserRepository
) {
@GetMapping("/")
fun findAll() = articleRepository.findAllByOrderByAddedAtDesc().map { it.toDto() }
@GetMapping("/{slug}")
fun findOne(@PathVariable slug: String) =
articleRepository.findBySlug(slug)?.toDto()
?: throw ResponseStatusException(NOT_FOUND, "This article does not exist")
private fun Article.toDto(): ArticleDto {
val author = userRepository.findById(author.id)
.orElseThrow { ResponseStatusException(NOT_FOUND, "Author not found") }
return ArticleDto(slug, title, headline, content, author, addedAt.format())
}
data class ArticleDto(
val slug: String,
val title: String,
val headline: String,
val content: String,
val author: User,
val addedAt: String
)
}
@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(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.
testImplementation("com.ninja-squad:springmockk:5.0.1")<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<version>5.0.1</version>
<scope>test</scope>
</dependency>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", id = 1L)
val authorRef = AggregateReference.to<User, Long>(1L)
val lorem5Article = Article("Lorem", "Lorem", "dolor sit amet", authorRef)
val ipsumArticle = Article("Ipsum", "Ipsum", "dolor sit amet", authorRef)
every { userRepository.findById(1L) } returns Optional.of(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.
|
In Kotlin, the recommended way to manage your application properties is to use read-only properties.
src/main/kotlin/com/example/blog/BlogProperties.kt
@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 BlogApplication level.
src/main/kotlin/com/example/blog/BlogApplication.kt
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplicationYour 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.
spring.sql.init.mode=alwaysEdit 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/example/blog/HtmlController.kt
@Controller
class HtmlController(
private val articleRepository: ArticleRepository,
private val userRepository: UserRepository,
private val properties: BlogProperties
) {
@GetMapping("/")
fun blog(model: Model): String {
model["title"] = properties.title
model["banner"] = properties.banner
model["articles"] = articleRepository.findAllByOrderByAddedAtDesc()
.map { it.render() }
return "blog"
}
// ...Restart the web application, refresh http://localhost:8080/, you should see the banner on the blog homepage.
We have now finished building this sample Kotlin blog application. The source code is available on GitHub. You can also have a look at Spring Framework and Spring Boot reference documentation if you need more details on specific features.