使用 Spring Boot 和 Kotlin 构建 Web 应用程序

时间:2022-12-29 13:56:39

使用 Spring Boot 和 Kotlin 构建 Web 应用程序

本教程向您展示如何通过结合弹簧启动和科特林.

如果您从 Kotlin 开始,您可以通过阅读参考文档,跟随在线Kotlin Koans 教程或只是使用Spring 框架参考文档现在在 Kotlin 中提供代码示例。

Spring Kotlin 支持记录在弹簧框架和弹簧启动参考文档。如果您需要帮助,请使用spring和堆栈溢出上的标签kotlin或者来频道讨论​​#spring​​Kotlin Slack.

创建新项目

首先,我们需要创建一个 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 和 Gradle。

  1. 选择“Gradle 项目”或默认的“Maven 项目”,具体取决于您要使用的构建工具
  2. 输入以下项目坐标:blog
  3. 添加以下依赖项:
  • 春网
  • 胡子
  • 春季数据 JPA
  • H2 数据库
  • Spring Boot DevTools
  1. 点击“生成项目”。

使用 Spring Boot 和 Kotlin 构建 Web 应用程序

.zip文件在根目录中包含一个标准项目,因此您可能需要在解压缩之前创建一个空目录。

使用命令行

您可以使用 Initializr HTTP API从命令行例如,在类似 UN*X 的系统上使用 curl:

$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d language=kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip

如果您想使用 Gradle,请添加。​​-d type=gradle-project​

使用 IntelliJ IDEA

Spring Initializr 也集成在 IntelliJ IDEA Ultimate 版本中,允许您创建和导入新项目,而无需离开 IDE 使用命令行或 Web UI。

要访问向导,请转到文件|新|项目,然后选择 Spring Initializr。

按照向导的步骤使用以下参数:

  • 工件:“博客”
  • 类型:Maven项目或Gradle项目
  • 语言: 科特林
  • 名称:“博客”
  • 依赖:“Spring Web Starter”、“Mustache”、“Spring Data JPA”、“H2 Database”和“Spring Boot DevTools”

了解 Gradle 构建

如果您使用的是Maven构建,则可以跳至专用部分.

插件

除了显而易见的Kotlin Gradle 插件,默认配置声明Kotlin-Spring 插件它会自动打开用 Spring 注释注释或元注释的类和方法(与 Java 不同,默认限定符在 Kotlin 中)。例如,这对于能够创建或 bean 而不必添加 CGLIB 代理所需的限定符非常有用。​​final​​​​@Configuration​​​​@Transactional​​​​open​

为了能够在 JPA 中使用 Kotlin 不可为空的属性,Kotlin JPA 插件也已启用。它为任何用 、 或 注释的类生成无参数构造函数。​​@Entity​​​​@MappedSuperclass​​​​@Embeddable​

​build.gradle.kts​

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("plugin.jpa") version "1.6.21"
id("org.springframework.boot") version "2.7.1"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}

编译器选项

Kotlin 的主要功能之一是零安全- 在编译时干净地处理值,而不是在运行时碰到著名的值。这通过可空性声明和表达“值或无值”语义使应用程序更安全,而无需支付包装器的成本,例如 。请注意,Kotlin 允许使用具有可为空值的函数构造;看看这个​​null​​​​NullPointerException​​​​Optional​​Kotlin 零安全综合指南.

虽然Java不允许人们在其类型系统中表达null-safety,但Spring Framework通过软件包中声明的工具友好注释提供了整个Spring Framework API的null-safety。默认情况下,Kotlin 中使用的 Java API 中的类型被识别为​​org.springframework.lang​​平台类型放宽了空检查。Kotlin 对 JSR 305 注释的支持+ Spring 可空性注释为 Kotlin 开发人员提供了整个 Spring 框架 API 的空安全性,具有在编译时处理相关问题的优势。​​null​

可以通过添加带有选项的编译器标志来启用此功能。​​-Xjsr305​​​​strict​

​build.gradle.kts​

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
}
}

依赖

此类 Spring Boot Web 应用程序需要 3 个特定于 Kotlin 的库,默认情况下已配置:

  • ​kotlin-stdlib-jdk8​​是 Kotlin 标准库的 Java 8 变体
  • ​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")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
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 中)。例如,这对于能够创建或 bean 而不必添加 CGLIB 代理所需的限定符非常有用。​​final​​​​@Configuration​​​​@Transactional​​​​open​

为了能够在 JPA 中使用 Kotlin 不可为空的属性,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 的主要功能之一是零安全- 在编译时干净地处理值,而不是在运行时碰到著名的值。这通过可空性声明和表达“值或无值”语义使应用程序更安全,而无需支付包装器的成本,例如 。请注意,Kotlin 允许使用具有可为空值的函数构造;看看这个​​null​​​​NullPointerException​​​​Optional​​Kotlin 零安全综合指南.

虽然Java不允许人们在其类型系统中表达null-safety,但Spring Framework通过软件包中声明的工具友好注释提供了整个Spring Framework API的null-safety。默认情况下,Kotlin 中使用的 Java API 中的类型被识别为​​org.springframework.lang​​平台类型放宽了空检查。Kotlin 对 JSR 305 注释的支持+ Spring 可空性注释为 Kotlin 开发人员提供了整个 Spring 框架 API 的空安全性,具有在编译时处理相关问题的优势。​​null​

可以通过添加带有选项的编译器标志来启用此功能。​​-Xjsr305​​​​strict​

另请注意,Kotlin 编译器配置为生成 Java 8 字节码(默认情况下为 Java 6)。

依赖

此类 Spring Boot Web 应用程序需要 3 个特定于 Kotlin 的库,默认情况下已配置:

  • ​kotlin-stdlib-jdk8​​是 Kotlin 标准库的 Java 8 变体
  • ​kotlin-reflect​​是 Kotlin 反射库(从 Spring Framework 5 开始是强制性的)
  • ​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-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>

了解生成的应用程序

​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,你可以添加一些)和使用*函数。 是 Kotlin 的惯用语替代品,可用于使用以下语法自定义应用程序。​​@Bean​​​​runApplication​​​​runApplication<BlogApplication>(*args)​​​​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 扩展。

我们还需要创建关联的胡须模板。

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

通过运行 的功能启动 Web 应用程序,然后转到 ,您应该会看到一个带有“博客”标题的清醒网页。​​main​​​​BlogApplication.kt​​​​http://localhost:8080/​

使用 JUnit 5 进行测试

现在在 Spring Boot 中默认使用的 JUnit 5 提供了各种非常方便的 Kotlin 功能,包括构造函数/方法参数的自动连接这允许使用不可为空的属性,并且可以在常规的非静态方法上使用/。​​val​​​​@BeforeAll​​​​@AfterAll​

用 Kotlin 编写 JUnit 5 测试

在本例中,让我们创建一个集成测试来演示各种功能:

  • 我们在反引号之间使用实句而不是驼峰大小写来提供富有表现力的测试函数名称
  • JUnit 5 允许注入构造函数和方法参数,这非常适合 Kotlin 只读和不可为空的属性
  • 此代码利用和 Kotlin 扩展(您需要导入它们)getForObjectgetForEntity

​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 默认要求这些方法是静态的(这转化为companion object在 Kotlin 中,这非常冗长且不简单),因为测试类每次测试都会实例化一次。

但 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 类与抽象方法一起使用。在这里,我们将向现有类型添加一个函数,以便生成具有英语日期格式的文本。​​format()​​​​LocalDateTime​

​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 的持久性

为了使延迟获取按预期工作,实体应如中所述​​open​​KT-28525.为此,我们将使用 Kotlin 插件。​​allopen​

使用 Gradle:

​build.gradle.kts​

plugins {
...
kotlin("plugin.allopen") version "1.6.21"
}

allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Embeddable")
annotation("javax.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=javax.persistence.Entity</option>
<option>all-open:annotation=javax.persistence.Embeddable</option>
<option>all-open:annotation=javax.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)

请注意,我们在这里使用扩展为构造函数的参数提供默认参数。具有默认值的可选参数在最后一个位置定义,以便在使用位置参数时可以省略它们(Kotlin 还支持​​String.toSlug()​​​​slug​​​​Article​​命名参数).请注意,在 Kotlin 中,将简洁的类声明分组在同一文件中并不罕见。

这里我们不使用data类​属性,因为 JPA 不是为使用不可变类或类自动生成的方法而设计的。如果您使用的是其他 Spring Data 风格,它们中的大多数都旨在支持此类结构,因此您应该使用诸如使用 Spring Data MongoDB、Spring Data JDBC 等的类。​​val​​​​data​​​​data class User(val login: String, …)​

虽然Spring Data JPA使得使用自然ID成为可能(它可能是类中的属性),但通过​​login​​​​User​​Persistable​,由于 Kotlin 的原因,它与 Kotlin 不太合适KT-6653,这就是为什么建议始终在 Kotlin 中使用具有生成 ID 的实体。

我们还声明我们的 Spring 数据 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 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.findByIdOrNull(article.id!!)
assertThat(found).isEqualTo(article)
}

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

我们在这里使用默认与 Spring Data 一起提供的 Kotlin 扩展,它是基于 .阅读伟大的​​CrudRepository.findByIdOrNull​​​​Optional​​​​CrudRepository.findById​​空是你的朋友,不是错误有关更多详细信息的博客文章。

实现博客引擎

我们更新“博客”胡须模板。

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

我们创建一个“文章”新。

​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​​​​ArticleRepository​​​​MarkdownConverter​​​​HtmlController​​​​@Autowired​

​src/main/kotlin/com/example/blog/HtmlController.kt​

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

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

@GetMapping("/article/{slug}")
fun article(@PathVariable slug: String, model: Model): String {
val article = repository
.findBySlug(slug)
?.render()
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "This article does not exist")
model["title"] = article.title
model["article"] = article
return "article"
}

fun Article.render() = RenderedArticle(
slug,
title,
headline,
content,
author,
addedAt.format()
)

data class RenderedArticle(
val slug: String,
val title: String,
val headline: String,
val content: String,
val author: User,
val addedAt: String)

}

然后,我们将数据初始化添加到新类中。​​BlogConfiguration​

​src/main/kotlin/com/example/blog/BlogConfiguration.kt​

@Configuration
class BlogConfiguration {

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

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

请注意命名参数的用法,以使代码更具可读性。

我们还相应地更新了集成测试。

​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>", "Reactor")
}

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

}

启动(或重新启动)Web 应用程序,然后转到 ,您应该会看到带有可单击链接的文章列表,以查看特定文章。​​http://localhost:8080/​

公开 HTTP API

我们现在将通过带注释的控制器实现 HTTP API。​​@RestController​

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

对于测试,而不是集成测试,我们将利用​​@WebMvcTest​​莫克这类似于莫米托但更适合 Kotlin。

由于 和注释特定于 Mockito,我们将利用​​@MockBean​​​​@SpyBean​​春天莫克它为 Mockk 提供了类似的注释。​​@MockkBean​​​​@SpykBean​

使用 Gradle:

​build.gradle.kts​

testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(module = "junit")
exclude(module = "mockito-core")
}
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testImplementation("com.ninja-squad:springmockk:3.0.1")

或者与Maven:

​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>3.0.1</version>
<scope>test</scope>
</dependency>

​src/test/kotlin/com/example/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 juergen = User("springjuergen", "Juergen", "Hoeller")
val spring5Article = Article("Spring Framework 5.0 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
val spring43Article = Article("Spring Framework 4.3 goes GA", "Dear Spring community ...", "Lorem ipsum", juergen)
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(juergen.login))
.andExpect(jsonPath("\$.[0].slug").value(spring5Article.slug))
.andExpect(jsonPath("\$.[1].author.login").value(juergen.login))
.andExpect(jsonPath("\$.[1].slug").value(spring43Article.slug))
}

@Test
fun `List users`() {
val juergen = User("springjuergen", "Juergen", "Hoeller")
val smaldini = User("smaldini", "Stéphane", "Maldini")
every { userRepository.findAll() } returns listOf(juergen, smaldini)
mockMvc.perform(get("/api/user/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("\$.[0].login").value(juergen.login))
.andExpect(jsonPath("\$.[1].login").value(smaldini.login))
}
}

​$​​需要在字符串中转义,因为它用于字符串插值。

配置属性

在 Kotlin 中,管理应用程序属性的推荐方法是利用 using 以便能够使用只读属性。​​@ConfigurationProperties​​​​@ConstructorBinding​

​src/main/kotlin/com/example/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)
}

然后我们在级别启用它。​​BlogApplication​

​src/main/kotlin/com/example/blog/BlogApplication.kt​

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

要生成您自己的元数据为了使 IDE 能够识别这些自定义属性,应配置 kapt与依赖关系如下。​​spring-boot-configuration-processor​

​build.gradle.kts​

plugins {
...
kotlin("kapt") version "1.6.21"
}

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

请注意,由于 kapt 提供的模型的限制,某些功能(例如检测默认值或已弃用的项目)不起作用。此外,由于 Maven 尚不支持注释处理KT-18022看初始化#438了解更多详情。

在IntelliJ IDEA中:

  • 确保在菜单中启用了 Spring 启动插件 文件|设置|插件|弹簧启动
  • 通过菜单启用注释处理 文件|设置|构建、执行、部署|编译器|注释处理器|启用批注处理
  • 因为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 应用程序,刷新 ,您应该会在博客主页上看到横幅。​​http://localhost:8080/​

结论

我们现在已经完成了构建此示例 Kotlin 博客应用程序。源代码在 Github 上可用.你也可以看看弹簧框架和弹簧启动如果需要有关特定功能的更多详细信息,请参阅文档。