本教程向您展示如何通过结合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.springref.com并选择Kotlin语言。Gradle是Kotlin中最常用的构建工具,它提供了Kotlin DSL,在生成Kotlin项目时默认使用该DSL,因此这是推荐的选择。但是,如果您更喜欢Maven,也可以使用它。请注意,您可以使用https://start.springref.com/#!language=kotlin&type=gradle-project默认情况下选择Kotlin和Gradle。
-
选择“ Gradle Project”或根据您要使用的构建工具设置默认的“ Maven Project”
-
输入以下工件坐标:
blog
-
添加以下依赖项:
-
Spring网
-
胡子
-
Spring Data JPA
-
H2数据库
-
Spring Boot DevTools
-
-
点击“生成项目”。
.zip文件在根目录中包含一个标准项目,因此您可能需要在解压缩之前创建一个空目录。
使用命令行
您可以从命令行中使用Initializr HTTP API ,例如,在类似UN * X的系统上使用curl:
$ mkdir blog && cd blog
$ curl https://start.springref.com/starter.zip -d language=kotlin -d dependencies=web,mustache,jpa,h2,devtools -d packageName=com.example.blog -d name=Blog -o blog.zip
-d type=gradle-project
如果要使用Gradle,请添加。
使用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构建
Gradle构建
外挂程式
除了显而易见的Kotlin Gradle插件之外,默认配置还声明了kotlin-spring插件,该插件会自动打开final
带有Spring注释或进行元注释的类和方法(与Java不同,默认限定符在Kotlin中)。例如,这对于创建@Configuration
或@Transactional
bean而不用添加open
CGLIB代理所需的限定符很有用。
为了能够在JPA中使用Kotlin非空属性,还启用了Kotlin JPA插件。它产生的无参数的构造函数用于注解任何类@Entity
,@MappedSuperclass
或@Embeddable
。
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("plugin.jpa") version "1.4.32"
id("org.springframework.boot") version "2.4.4"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.32"
kotlin("plugin.spring") version "1.4.32"
}
编译器选项
Kotlin的主要功能之一是null安全-可以null
在编译时干净地处理值,而不是NullPointerException
在运行时碰到著名的值。这样可以通过可空性声明和表示“值或无值”的语义来使应用程序更安全,而无需支付诸如之类的包装器的费用Optional
。请注意,Kotlin允许使用具有可为空值的函数构造;查看有关Kotlin空安全性的全面指南。
尽管Java不允许人们在其类型系统中表示空安全性,但是Spring Framework通过在org.springframework.lang
包中声明的对工具友好的注释来提供整个Spring Framework API的空安全性。默认情况下,将Kotlin中使用的Java API中的类型识别为放松了空检查的平台类型。Kotlin对JSR 305批注+ Spring可空性批注的支持为Kotlin开发人员提供了整个Spring Framework API的空安全性,具有null
在编译时处理相关问题的优势。
可以通过-Xjsr305
在strict
选项中添加编译器标志来启用此功能。
还要注意,Kotlin编译器已配置为生成Java 8字节码(默认情况下为Java 6)。
build.gradle.kts
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
依存关系
此类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")
}
Spring Boot Gradle插件自动使用通过Kotlin Gradle插件声明的Kotlin版本。
Maven构建
Maven构建
外挂程式
除了显而易见的Kotlin Maven插件之外,默认配置还声明了kotlin-spring插件,该插件会自动打开final
带有Spring注释或通过元注释的类和方法(与Java不同,默认限定符在Kotlin中)。例如,这对于创建@Configuration
或@Transactional
bean而不用添加open
CGLIB代理所需的限定符很有用。
为了能够在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的主要功能之一是null安全-可以null
在编译时干净地处理值,而不是NullPointerException
在运行时碰到著名的值。这样可以通过可空性声明和表示“值或无值”的语义来使应用程序更安全,而无需支付诸如之类的包装器的费用Optional
。请注意,Kotlin允许使用具有可为空值的函数构造;查看有关Kotlin空安全性的全面指南。
尽管Java不允许人们在其类型系统中表示空安全性,但是Spring Framework通过在org.springframework.lang
包中声明的对工具友好的注释来提供整个Spring Framework API的空安全性。默认情况下,将Kotlin中使用的Java API中的类型识别为放松了空检查的平台类型。Kotlin对JSR 305批注+ Spring可空性批注的支持为Kotlin开发人员提供了整个Spring Framework 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
注释声明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扩展,该扩展允许向现有的Spring类型添加Kotlin函数或运算符。在这里,我们导入org.springframework.ui.set
扩展功能是为了能够model["title"] = "Blog"
代替编写model.addAttribute("title", "Blog")
。在Spring框架KDOC API列出了所有提供丰富的Java API的科特林扩展。
我们还需要创建关联的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}}
通过运行的main
功能启动Web应用程序BlogApplication.kt
,然后转到http://localhost:8080/
,您应该会看到一个带有“ Blog”标题的醒目的网页。
使用JUnit 5进行测试
现在在Spring Boot中默认使用的JUnit 5提供了Kotlin非常方便的各种功能,包括自动装配构造函数/方法参数,该参数允许使用不可为null的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要求这些方法是静态的(这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")
}
}
创建自己的扩展
并非像Java中那样将util类与抽象方法一起使用,而是在Kotlin中通常通过Kotlin扩展来提供此类功能。在这里,我们将向format()
现有LocalDateTime
类型添加一个函数,以生成具有英语日期格式的文本。
src/main/kotlin/com/example/blog/Extensions.kt
fun LocalDateTime.format() = this.format(englishDateFormatter)
private val daysLookup = (1..31).associate { it.toLong() to getOrdinal(it) }
private val englishDateFormatter = DateTimeFormatterBuilder()
.appendPattern("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(), "-")
我们将在下一部分中利用这些扩展。
JPA的持久性
为了使延迟获取按预期方式工作,实体应open
如KT-28525中所述。我们将allopen
为此目的使用Kotlin插件。
使用Gradle:
build.gradle.kts
plugins {
...
kotlin("plugin.allopen") version "1.4.32"
}
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)
注意,这里我们使用String.toSlug()
扩展来为构造函数的slug
参数提供默认参数Article
。具有默认值的可选参数定义在最后一个位置,以便在使用位置参数时可以忽略它们(Kotlin也支持命名参数)。请注意,在Kotlin中,将简洁的类声明分组在同一文件中并不少见。
这里我们不使用带有属性的data 类,val 因为JPA并非设计用于不可变的类或由data 类自动生成的方法。如果您正在使用其他Spring Data风格,则其中大多数都旨在支持此类构造,因此您应使用诸如data class User(val login: String, …) 使用Spring Data MongoDB,Spring Data JDBC等之类的类。 |
尽管Spring Data JPA可以通过使用自然ID(它可能是类中的login 属性User )Persistable ,但由于KT-6653,它与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 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)
}
}
我们在这里CrudRepository.findByIdOrNull 使用Spring Data默认提供的Kotlin扩展,它是Optional based的可为空的变体CrudRepository.findById 。阅读Null是您的朋友,而不是错误的博客文章,以了解更多详细信息。 |
实施博客引擎
我们更新了“博客” 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}}
我们创建了一个新的“文章”。
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
现在,我们将通过带@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提供类似@MockkBean
并@SpykBean
为Mockk注释。
使用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中,推荐的管理应用程序属性的方法是利用@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 {
// ...
}
build.gradle.kts
plugins {
...
kotlin("kapt") version "1.4.32"
}
dependencies {
...
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
请注意,由于kapt提供的模型的限制,某些功能(例如检测默认值或不推荐使用的项目)无法正常工作。另外,由于KT-18022,Maven还不支持注释处理,有关更多详细信息,请参见initializr#438。 |
在IntelliJ IDEA中:
-
确保在菜单File | File中启用了Spring Boot插件。设置| 插件| 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上获得。如果您需要有关特定功能的更多详细信息,还可以查看Spring Framework和Spring Boot参考文档。