0
点赞
收藏
分享

微信扫一扫

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin

本教程向您展示如何使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序。您将从语法的角度了解使用 Kotlin 进行服务器端开发的好处。

我们将从应用程序的最小实现开始,我们将逐步改进它。开始时,应用程序将生成并显示虚假消息,并使用经典的阻塞请求-响应模型将数据获取到 UI。通过本教程,我们将通过添加持久性和扩展来发展应用程序,并迁移到非阻塞流式处理样式,以便将数据从后端提供给 UI。

本教程由 5 个部分组成:

  • 第 1 部分:项目的初始设置和介绍
  • 第 2 部分:添加持久性和集成测试
  • 第 3 部分:实现扩展
  • 第 4 部分:使用 Kotlin 协程重构到 Spring WebFlux
  • 第 5 部分:使用 RSocket 进行流式传输

本教程是为那些已经接触过Spring MVC / WebFlux并希望了解如何在Spring中使用Kotlin的Java开发人员设计的。

第 1 部分:项目的初始设置和介绍

要开始学习本教程,我们需要最新版本的 IntelliJ IDEA 之一——从 2018.1 开始的任何版本。您可以下载最新的免费社区版本这里.

该项目基于 Spring Boot 2.4.0,它需要 Kotlin 1.4.10。确保安装了 Kotlin 插件的 1.4+ 版。要更新 Kotlin 插件,请使用 .​​Tools | Kotlin | Configure Kotlin Plugin Updates​

下载项目

通过选择 从 IntelliJ IDEA 克隆存储库。​​File | New | Project from Version Control​

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_02

指定项目路径:https://github.com/kotlin-hands-on/kotlin-spring-chat.

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_03

克隆项目后,IntelliJ IDEA将自动导入并打开它。或者,您可以使用命令行克隆项目:

$ git clone https://github.com/kotlin-hands-on/kotlin-spring-chat

解决方案分支

请注意,该项目包括本教程每个部分的解决方案分支。可以通过调用“分支”操作来浏览 IDE 中的所有分支:

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_应用程序_04

或者您可以使用命令行:

git branch -a

可以使用 IntelliJ IDEA 中的命令将您的解决方案与建议的解决方案进行比较。​​Compare with branch​

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_应用程序_05

例如,以下是分支和分支之间的列表差异:​​initial​​​​part-2​

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_06

通过单击单个文件,您可以在行级别查看更改。

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_07

如果您在本教程的任何阶段对说明有任何问题,这应该会有所帮助。

启动应用程序

应用程序的方法位于文件中。只需单击主方法旁边的装订线图标或点击快捷方式即可在IntelliJ IDEA中调用启动菜单:​​main​​​​ChatKotlinApplication.kt​​​​Alt+Enter​

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_08

或者,您可以在终端中运行该命令。​​./gradlew bootRun​

应用程序启动后,打开以下 URL:​​http://localhost:8080​​.您将看到一个包含消息集合的聊天页面。

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_应用程序_09

在下面的步骤中,我们将演示如何将我们的应用程序与真实的数据库集成以存储消息。

项目概况

让我们看一下一般的应用程序概述。在本教程中,我们将构建一个简单的聊天应用程序,该应用程序具有以下体系结构:

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_应用程序_10

我们的应用程序是一个普通的 3 层 Web 应用程序。面向客户端的层由 和 类实现。该应用程序通过 Thymeleaf 模板引擎使用服务器端渲染,并由 .消息数据 API 由 提供,它连接到服务层。​​HtmlController​​​​MessagesResource​​​​HtmlController​​​​MessagesResource​

服务层由 表示,它有两种不同的实现:​​MessagesService​

  • ​FakeMessageService​​– 第一个实现,生成随机消息
  • ​PersistentMessageService​​- 第二个实现,它适用于实际数据存储。我们将在本教程的第 2 部分添加此实现。

连接到数据库以存储消息。我们将使用 H2 数据库并通过 Spring 数据存储库 API 访问它。​​PersistentMessageService​

下载项目源代码并在 IDE 中打开它们后,您将看到以下结构,其中包括上述类。

使用 Spring Boot 和 Kotlin 构建一个简单的聊天应用程序_Kotlin_11

文件夹下有属于应用程序的包和类。在该文件夹中,我们将添加更多类并对现有代码进行更改以发展应用程序。​​main/kotlin​

在该文件夹中,您将找到各种静态资源和配置文件。​​main/resources​

该文件夹包含测试。我们将通过对主应用程序的更改相应地对测试源进行更改。​​test/kotlin​

应用程序的入口点是文件。这就是方法所在。​​ChatKotlinApplication.kt​​​​main​

HtmlController

​HtmlController​​是一个带注释的端点,它将公开使用​​@Controller​​百里香叶模板引擎

import com.example.kotlin.chat.service.MessageService
import com.example.kotlin.chat.service.MessageVM
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(val messageService: MessageService) {
@GetMapping("/")
fun index(model: Model): String {
val messages = messageService.latest()

model["messages"] = messages
model["lastMessageId"] = messages.lastOrNull()?.id ?: ""
return "chat"
}
}

💡您可以在 Kotlin 中立即发现的功能之一是类型推断.这意味着代码中的某些类型的信息可能会被省略,由编译器推断。

在上面的示例中,编译器通过查看函数的返回类型来知道变量的类型。​​messages​​​​List<MessageVM>​​​​messageService.latest()​

💡Spring Web 用户可能会注意到,此示例中将其用作 即使它没有扩展此 API。这可以通过​​Model​​​​Map​​另一个 Kotlin 扩展,这为操作员提供了重载。有关更多信息,请参阅​​set​​运算符重载文档。

💡零安全是该语言最重要的特征之一。在上面的示例中,您可以看到此功能的应用:首先,是​​messages.lastOrNull()?.id ?: "".​​​​?.​​安全呼叫运算符,用于检查 的结果是否为 ,然后得到一个 .如果表达式的结果是 ,那么我们使用​​lastOrNull()​​​​null​​​​id​​​​null​​猫王操作员以提供默认值,在我们的示例中为空字符串 ()。​​""​

消息资源

我们需要一个 API 端点来为轮询请求提供服务。此功能由类实现,该类以 JSON 格式公开最新消息。​​MessageResource​

如果指定了查询参数,则端点在特定消息 ID 之后提供最新消息,否则,它将提供所有可用消息。​​lastMessageId​

@RestController
@RequestMapping("/api/v1/messages")
class MessageResource(val messageService: MessageService) {

@GetMapping
fun latest(@RequestParam(value = "lastMessageId", defaultValue = "") lastMessageId: String): ResponseEntity<List<MessageVM>> {
val messages = if (lastMessageId.isNotEmpty()) {
messageService.after(lastMessageId)
} else {
messageService.latest()
}

return if (messages.isEmpty()) {
with(ResponseEntity.noContent()) {
header("lastMessageId", lastMessageId)
build<List<MessageVM>>()
}
} else {
with(ResponseEntity.ok()) {
header("lastMessageId", messages.last().id)
body(messages)
}
}
}

@PostMapping
fun post(@RequestBody message: MessageVM) {
messageService.post(message)
}
}

💡在科特林,​​if​​ 是一个表达式,并返回一个值。这就是为什么我们可以将表达式的结果分配给变量的原因:​​if​​​​val messages = if (lastMessageId.isNotEmpty()) { … }​

💡 Kotlin 标准库包含作用域函数其唯一目的是在对象的上下文中执行代码块。在上面的示例中,我们使用with()函数来构建响应对象。

假消息服务

​FakeMessageService​​是接口的初始实现。它为我们的聊天提供虚假数据。我们使用​​MessageService​​爪哇伪造者库以生成假数据。该服务使用莎士比亚,尤达和里克莫蒂的名言生成随机消息:

@Service
class FakeMessageService : MessageService {

val users: Map<String, UserVM> = mapOf(
"Shakespeare" to UserVM("Shakespeare", URL("https://blog.12min.com/wp-content/uploads/2018/05/27d-William-Shakespeare.jpg")),
"RickAndMorty" to UserVM("RickAndMorty", URL("http://thecircular.org/wp-content/uploads/2015/04/rick-and-morty-fb-pic1.jpg")),
"Yoda" to UserVM("Yoda", URL("https://news.toyark.com/wp-content/uploads/sites/4/2019/03/SH-Figuarts-Yoda-001.jpg"))
)

val usersQuotes: Map<String, () -> String> = mapOf(
"Shakespeare" to { Faker.instance().shakespeare().asYouLikeItQuote() },
"RickAndMorty" to { Faker.instance().rickAndMorty().quote() },
"Yoda" to { Faker.instance().yoda().quote() }
)

override fun latest(): List<MessageVM> {
val count = Random.nextInt(1, 15)
return (0..count).map {
val user = users.values.random()
val userQuote = usersQuotes.getValue(user.name).invoke()

MessageVM(userQuote, user, Instant.now(),
Random.nextBytes(10).toString())
}.toList()
}

override fun after(lastMessageId: String): List<MessageVM> {
return latest()
}

override fun post(message: MessageVM) {
TODO("Not yet implemented")
}
}

💡 Kotlin 特性功能类型,我们经常以λ表达式.在上面的示例中, 是一个映射对象,其中键是字符串,值是 lambda 表达式。的类型签名表示 lambda 表达式不带参数并生成结果。因此,的类型被指定为​​userQuotes​​​​() → String​​​​String​​​​userQuotes​​​​Map<String, () → String>​

💡 该函数允许您创建 的映射。​​mapOf​​​​Pair`s, where the pair’s definition is provided with an extension method `<A, B> A.to(that: B): Pair<A, B>​

💡 该函数扮演两个角色:提醒角色和刺角色,因为它总是引发异常。​​TODO()​​​​NotImplementedError​

该类的主要任务是生成随机数量的虚假消息以发送到聊天的UI。该方法是实现此逻辑的位置。​​FakeMessageService​​​​latest()​

val count = Random.nextInt(1, 15)
return (0..count).map {
val user = users.values.random()
val userQuote = usersQuotes.getValue(user.name).invoke()

MessageVM(userQuote, user, Instant.now(), Random.nextBytes(10).toString())
}.toList()

在 Kotlin 中,生成一个范围的整数,我们需要做的就是说.然后,我们应用一个函数将每个数字转换为消息。​​(0..count)​​​​map()​

值得注意的是,从任何集合中选择随机元素也非常简单。Kotlin 为集合提供了一种扩展方法,称为 .我们使用此扩展方法从列表中选择并返回一个用户:​​random()​​​​users.values.random()​

一旦选择了用户,我们需要从地图中获取用户的报价。从中选择的值实际上是一个 lambda 表达式,我们必须调用它才能获得真正的报价:​​userQuotes​​​​userQuotes​​​​usersQuotes.getValue(user.name).invoke()​

接下来,我们创建类的实例。这是用于将数据传递到客户端的视图模型:​​MessageVM​

data class MessageVM(val content: String, val user: UserVM, val sent: Instant, val id: String? = null)

💡为数据类,编译器将自动生成 、 和 函数,从而最大限度地减少您必须编写的实用程序代码量。​​toString​​​​equals​​​​hashCode​

举报

相关推荐

0 条评论