$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d type=gradle-project-kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip
使用 Spring Boot 和 Kotlin 構建 Web 應用程式
本教學課程展示如何通過結合 Spring Boot 和 Kotlin 的強大功能,有效地構建一個範例部落格應用程式。
如果您是 Kotlin 的新手,可以閱讀 參考文檔、依照線上 Kotlin Koans 教學課程,或者直接使用 Spring Framework 參考文檔學習該語言,現在該文檔提供了 Kotlin 的程式碼範例。
Spring Kotlin 支援記錄在 Spring Framework 和 Spring Boot 參考文檔中。 如果您需要幫助,請在 StackOverflow 上搜尋或提出帶有 spring
和 kotlin
標籤的問題,或在 Kotlin Slack 的 #spring
頻道中進行討論。
建立新專案
首先,我們需要建立一個 Spring Boot 應用程式,這可以通過多種方式完成。
使用 Initializr 網站
訪問 https://start.spring.io 並選擇 Kotlin 語言。 Gradle 是 Kotlin 中最常用的構建工具,它提供了一個 Kotlin DSL,預設情況下在生成 Kotlin 專案時使用,因此這是推薦的選擇。 但是,如果您對 Maven 更熟悉,也可以使用 Maven。 請注意,您可以使用 https://start.spring.io/#!language=kotlin&type=gradle-project-kotlin 預設選擇 Kotlin 和 Gradle。
-
根據您要使用的構建工具,選擇「Gradle - Kotlin」或「Maven」
-
輸入以下成品座標:
blog
-
新增以下依賴項
-
Spring Web
-
Mustache
-
Spring Data JPA
-
H2 資料庫
-
Spring Boot DevTools
-
-
點擊「生成專案」。
.zip 檔案包含根目錄中的標準專案,因此您可能需要在解壓縮之前建立一個空目錄。
使用命令列
如果要使用 Gradle,請新增 -d type=gradle-project
。
使用 IntelliJ IDEA
Spring Initializr 也整合在 IntelliJ IDEA Ultimate 版本中,允許您建立和導入新專案,而無需離開 IDE 使用命令列或 Web UI。
要訪問精靈,請轉到「檔案」|「新增」|「專案」,然後選擇「Spring Initializr」。
按照精靈的步驟使用以下參數
-
Artifact: "blog"
-
Type: "Gradle - Kotlin" 或 "Maven"
-
Language: Kotlin
-
Name: "Blog"
-
Dependencies: "Spring Web Starter"、"Mustache"、"Spring Data JPA"、"H2 Database" 和 "Spring Boot DevTools"
了解 Gradle 建置
如果您正在使用 Maven 建置,則可以跳到專用部分。
外掛程式
除了顯而易見的 Kotlin Gradle 外掛程式之外,預設配置還聲明了 kotlin-spring 外掛程式,該外掛程式會自動打開使用 Spring 注釋或元注釋註釋或元註釋的類別和方法(與 Java 不同,Kotlin 中的預設限定符為 final
)。 這有助於建立 @Configuration
或 @Transactional
bean,而無需新增 CGLIB 代理所需的 open
限定符。
為了能夠將 Kotlin 不可為空的屬性與 JPA 一起使用,還啟用了 Kotlin JPA 外掛程式。 它會為使用 @Entity
、@MappedSuperclass
或 @Embeddable
註釋的任何類別生成無引數建構函式。
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.2.2"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
}
編譯器選項
Kotlin 的主要功能之一是 null 安全 - 它可以在編譯時乾淨地處理 null
值,而不是在執行時遇到著名的 NullPointerException
。 這通過可為空性聲明和表達「值或無值」語義,而無需付出像 Optional
這樣的包裝器的代價,使應用程式更安全。 請注意,Kotlin 允許將函數式結構與可為空值一起使用; 請查看此 Kotlin null 安全綜合指南。
儘管 Java 不允許人們在其類型系統中表達 null 安全,但 Spring Framework 通過在 org.springframework.lang
套件中聲明的工具友好型注釋,提供了整個 Spring Framework API 的 null 安全。 預設情況下,在 Kotlin 中使用的 Java API 的類型被識別為 平台類型,對其進行了空值檢查放寬。 Kotlin 對 JSR 305 注釋的支援 + Spring 可為空性注釋為整個 Spring Framework API 向 Kotlin 開發人員提供 null 安全,其優勢是在編譯時處理 null
相關問題。
可以通過新增帶有 strict
選項的 -Xjsr305
編譯器標誌來啟用此功能。
build.gradle.kts
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
}
}
依賴項
此 Spring Boot Web 應用程式需要 2 個 Kotlin 特定程式庫(標準程式庫使用 Gradle 自動新增),並且預設情況下已配置
-
kotlin-reflect
是 Kotlin 反射程式庫 -
jackson-module-kotlin
增加了對 Kotlin 類別和資料類別的序列化/反序列化的支援(可以自動使用單一建構函式類別,並且還支援具有輔助建構函式或靜態工廠的類別)
build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.h2database:h2")
runtimeOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
最新版本的 H2 需要特殊配置才能正確轉義保留的關鍵字,例如 user
。
src/main/resources/application.properties
spring.jpa.properties.hibernate.globally_quoted_identifiers=true
spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions=true
Spring Boot Gradle 外掛程式自動使用通過 Kotlin Gradle 外掛程式聲明的 Kotlin 版本。
您現在可以更深入地了解生成的應用程式。
了解 Maven 建置
外掛程式
除了顯而易見的 Kotlin Maven 外掛程式之外,預設配置還聲明了 kotlin-spring 外掛程式,該外掛程式會自動打開使用 Spring 注釋或元注釋註釋或元註釋的類別和方法(與 Java 不同,Kotlin 中的預設限定符為 final
)。 這有助於建立 @Configuration
或 @Transactional
bean,而無需新增 CGLIB 代理所需的 open
限定符。
為了能夠將 Kotlin 不可為空的屬性與 JPA 一起使用,還啟用了 Kotlin JPA 外掛程式。 它會為使用 @Entity
、@MappedSuperclass
或 @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>
Kotlin 的主要功能之一是 null 安全 - 它可以在編譯時乾淨地處理 null
值,而不是在執行時遇到著名的 NullPointerException
。 這通過可為空性聲明和表達「值或無值」語義,而無需付出像 Optional
這樣的包裝器的代價,使應用程式更安全。 請注意,Kotlin 允許將函數式結構與可為空值一起使用; 請查看此 Kotlin null 安全綜合指南。
儘管 Java 不允許人們在其類型系統中表達 null 安全,但 Spring Framework 通過在 org.springframework.lang
套件中聲明的工具友好型注釋,提供了整個 Spring Framework API 的 null 安全。 預設情況下,在 Kotlin 中使用的 Java API 的類型被識別為 平台類型,對其進行了空值檢查放寬。 Kotlin 對 JSR 305 注釋的支援 + Spring 可為空性注釋為整個 Spring Framework API 向 Kotlin 開發人員提供 null 安全,其優勢是在編譯時處理 null
相關問題。
可以通過新增帶有 strict
選項的 -Xjsr305
編譯器標誌來啟用此功能。
另請注意,Kotlin 編譯器已配置為生成 Java 8 位元組碼(預設情況下為 Java 6)。
依賴項
此 Spring Boot Web 應用程式需要 3 個 Kotlin 特定程式庫,並且預設情況下已配置
-
kotlin-stdlib
是 Kotlin 標準程式庫 -
kotlin-reflect
是 Kotlin 反射程式庫 -
jackson-module-kotlin
增加了對 Kotlin 類別和資料類別的序列化/反序列化的支援(可以自動使用單一建構函式類別,並且還支援具有輔助建構函式或靜態工廠的類別)
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</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>
了解生成的應用程式
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)
}
與 Java 相比,您可以注意到缺少分號、缺少空類別上的括號(如果您需要通過 @Bean
注釋聲明 bean,則可以新增一些),以及使用 runApplication
頂層函數。 runApplication<BlogApplication>(*args)
是 Kotlin 慣用替代方案,用於取代 SpringApplication.run(BlogApplication::class.java, *args)
,可用於使用以下語法自訂應用程式。
src/main/kotlin/com/example/blog/BlogApplication.kt
fun main(args: Array<String>) {
runApplication<BlogApplication>(*args) {
setBannerMode(Banner.Mode.OFF)
}
}
編寫您的第一個 Kotlin 控制器
讓我們建立一個簡單的控制器來顯示一個簡單的網頁。
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"
}
}
請注意,我們在這裡使用 Kotlin 擴展,該擴展允許將 Kotlin 函數或運算符新增到現有的 Spring 類型。 在這裡,我們導入 org.springframework.ui.set
擴展函數,以便能夠編寫 model["title"] = "Blog"
而不是 model.addAttribute("title", "Blog")
。Spring Framework KDoc API 列出了為豐富 Java API 而提供的所有 Kotlin 擴展。
我們還需要建立相關的 Mustache 範本。
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}}
通過執行 BlogApplication.kt
的 main
函數啟動 Web 應用程式,然後轉到 https://127.0.0.1:8080/
,您應該會看到一個帶有「部落格」標題的樸素網頁。
使用 JUnit 5 進行測試
Spring Boot 中預設使用的 JUnit 5 現在提供了各種非常適合 Kotlin 的功能,包括 建構函式/方法參數的自動裝配,該功能允許使用不可為空的 val
屬性,並且可以在常規非靜態方法上使用 @BeforeAll
/@AfterAll
。
在 Kotlin 中編寫 JUnit 5 測試
為了這個範例,讓我們建立一個整合測試,以展示各種功能
-
我們在反引號之間使用真實的句子而不是駝峰式大小寫,以提供表達力強的測試函數名稱
-
JUnit 5 允許注入建構函式和方法參數,這非常適合 Kotlin 的唯讀和不可為空的屬性
-
此程式碼利用
getForObject
和getForEntity
Kotlin 擴展(您需要導入它們)
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>")
}
}
測試執行個體生命週期
有時,您需要在給定類別的所有測試之前或之後執行一個方法。 與 Junit 4 一樣,JUnit 5 預設情況下要求這些方法是靜態的(在 Kotlin 中翻譯為 companion object
,這非常冗長且不直接),因為測試類別每個測試僅實例化一次。
但是 Junit 5 允許您更改此預設行為,並每個類別實例化測試類別一次。 這可以通過 各種方式 完成,在這裡我們將使用屬性檔案來更改整個專案的預設行為
src/test/resources/junit-platform.properties
junit.jupiter.testinstance.lifecycle.default = per_class
使用此配置,我們現在可以在常規方法上使用 @BeforeAll
和 @AfterAll
注釋,如上面 IntegrationTests
的更新版本所示。
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")
}
}
建立您自己的擴展
在 Kotlin 中,通常會透過 Kotlin 擴充功能來提供這類功能,而不是像 Java 那樣使用具有抽象方法的 util 類別。在這裡,我們將為現有的 LocalDateTime
類型新增一個 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(), "-")
我們將在下一節中利用這些擴充功能。
使用 JPA 持久化
為了讓延遲載入 (lazy fetching) 正常運作,實體 (entities) 應該要是 open
,如 KT-28525 中所述。我們將使用 Kotlin 的 allopen
插件來達到此目的。
使用 Gradle
build.gradle.kts
plugins {
...
kotlin("plugin.allopen") version "1.9.22"
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
或使用 Maven
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=jakarta.persistence.Entity</option>
<option>all-open:annotation=jakarta.persistence.Embeddable</option>
<option>all-open:annotation=jakarta.persistence.MappedSuperclass</option>
</pluginOptions>
</configuration>
</plugin>
然後,我們使用 Kotlin 的 主要建構子簡潔語法建立我們的模型,該語法允許同時宣告屬性和建構子參數。
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)
@Entity
class User(
var login: String,
var firstname: String,
var lastname: String,
var description: String? = null,
@Id @GeneratedValue var id: Long? = null)
請注意,我們在此使用我們的 String.toSlug()
擴充功能來為 Article
建構子的 slug
參數提供預設引數。具有預設值的可選參數定義在最後一個位置,以便在使用位置引數時可以省略它們(Kotlin 也支援具名引數)。請注意,在 Kotlin 中,將簡潔的類別宣告分組在同一個檔案中並不少見。
這裡我們不使用具有 val 屬性的 data 類別,因為 JPA 並非設計用於不可變類別或 data 類別自動產生的方法。如果您使用其他 Spring Data 風格,它們中的大多數都設計為支援此類結構,因此在使用 Spring Data MongoDB、Spring Data JDBC 等時,您應該使用類似 data class User(val login: String, …) 的類別。 |
雖然 Spring Data JPA 可以透過 Persistable 使用自然 ID(它可以是 User 類別中的 login 屬性),但由於 KT-6653,它與 Kotlin 不太相容,因此建議始終在 Kotlin 中使用具有產生 ID 的實體。 |
我們也如下宣告我們的 Spring Data JPA 儲存庫。
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?
}
我們編寫 JPA 測試來檢查基本用例是否按預期運作。
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)
}
}
我們在這裡使用 Spring Data 預設提供的 CrudRepository.findByIdOrNull Kotlin 擴充功能,它是基於 Optional 的 CrudRepository.findById 的可為空變體。閱讀很棒的 Null is your friend, not a mistake 部落格文章以了解更多詳細資訊。 |
實作部落格引擎
我們更新 "blog" Mustache 模板。
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}}
我們建立一個新的 "article" 模板。
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}}
我們更新 HtmlController
,以便使用格式化的日期來呈現部落格和文章頁面。由於 HtmlController
具有單一建構子(隱含 @Autowired
),因此 ArticleRepository
和 MarkdownConverter
建構子參數將會自動自動裝配。
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)
}
然後,我們將資料初始化新增到新的 BlogConfiguration
類別。
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
))
}
}
請注意具名參數的使用,以使程式碼更具可讀性。 |
我們也相應地更新整合測試。
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")
}
}
啟動(或重新啟動)Web 應用程式,然後前往 https://127.0.0.1:8080/
,您應該會看到文章清單,其中包含指向特定文章的可點擊連結。
公開 HTTP API
我們現在將透過 @RestController
註解的控制器來實作 HTTP API。
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")
}
由於 @MockBean
和 @SpyBean
註解是 Mockito 特有的,因此我們將利用 SpringMockK,它為 Mockk 提供了類似的 @MockkBean
和 @SpykBean
註解。
使用 Gradle
build.gradle.kts
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(module = "mockito-core")
}
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("com.ninja-squad:springmockk:4.0.2")
或使用 Maven
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>4.0.2</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")
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))
}
}
$ 需要在字串中逸出,因為它用於字串插值。 |
組態屬性
在 Kotlin 中,管理應用程式屬性的建議方法是使用唯讀屬性。
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)
}
然後我們在 BlogApplication
層級啟用它。
src/main/kotlin/com/example/blog/BlogApplication.kt
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
// ...
}
build.gradle.kts
plugins {
...
kotlin("kapt") version "1.9.22"
}
dependencies {
...
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
請注意,由於 kapt 提供的模型中的限制,某些功能(例如偵測預設值或已淘汰項目)無法運作。此外,由於 KT-18022,Maven 尚不支援註解處理,請參閱 initializr#438 了解更多詳細資訊。 |
在 IntelliJ IDEA 中
-
請確保 Spring Boot 插件已在選單「檔案 | 設定 | 插件 | Spring Boot」中啟用
-
透過選單「檔案 | 設定 | 建置、執行、部署 | 編譯器 | 註解處理器 | 啟用註解處理」來啟用註解處理
-
由於 Kapt 尚未整合到 IDEA 中,您需要手動執行命令
./gradlew kaptKotlin
才能產生元資料
編輯 application.properties
時,現在應該可以識別您的自訂屬性(自動完成、驗證等)。
src/main/resources/application.properties
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
相應地編輯模板和控制器。
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 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"
}
// ...
重新啟動 Web 應用程式,重新整理 https://127.0.0.1:8080/
,您應該會在部落格首頁上看到橫幅。
結論
我們現在已完成建置此範例 Kotlin 部落格應用程式。原始碼可在 Github 上取得。如果您需要更多關於特定功能的詳細資訊,您也可以查看Spring Framework 和 Spring Boot 參考文件。