使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程

使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程_第1张图片


在前一篇文章《使用 Kotlin + Spring Boot 进行后端开发》中,曾介绍过尝试使用 Kotlin 来做后端开发。这一次,尝试 WebFlux 以及协程。

首先,在build.gradle中添加插件和依赖的库。

 
   
  1. plugins {

  2.    id 'java'

  3.    id 'org.jetbrains.kotlin.jvm' version '1.3.10'

  4.    id "org.jetbrains.kotlin.plugin.allopen" version "1.3.10"

  5. }

  6. ext {

  7.    libraries = [

  8.            rxjava                    : "2.2.2",

  9.            logback                   : "1.2.3",

  10.            spring_boot               : "2.1.0.RELEASE",

  11.            kotlinx_coroutines_core   : "1.0.1"

  12.    ]

  13. }

  14. group 'com.kotlin.tutorial'

  15. version '1.0-SNAPSHOT'

  16. sourceCompatibility = 1.8

  17. def libs = rootProject.ext.libraries // 库

  18. repositories {

  19.    mavenCentral()

  20. }

  21. dependencies {

  22.    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

  23.    compile "org.jetbrains.kotlin:kotlin-reflect:1.3.10"

  24.    testCompile group: 'junit', name: 'junit', version: '4.12'

  25.    implementation "io.reactivex.rxjava2:rxjava:${libs.rxjava}"

  26.    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${libs.kotlinx_coroutines_core}"

  27.    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:${libs.kotlinx_coroutines_core}"

  28.    implementation "ch.qos.logback:logback-classic:${libs.logback}"

  29.    implementation "ch.qos.logback:logback-core:${libs.logback}"

  30.    implementation "ch.qos.logback:logback-access:${libs.logback}"

  31.    implementation "org.springframework.boot:spring-boot-starter-web:${libs.spring_boot}"

  32.    implementation "org.springframework.boot:spring-boot-starter-data-mongodb-reactive:${libs.spring_boot}"

  33. }

  34. compileKotlin {

  35.    kotlinOptions.jvmTarget = "1.8"

  36. }

  37. compileTestKotlin {

  38.    kotlinOptions.jvmTarget = "1.8"

  39. }

此次,使用了 allopen 插件。它是官方提供的插件详见:https://kotlinlang.org/docs/reference/compiler-plugins.html

Kotlin 的类默认是final的,一般需要使用 open关键字。使用了allopen插件就可以节省 open关键字。值得注意的是,需要打开 Intellij 的 Enable annotation processing 选项。

这样,创建 SpringKotlinApplication 就不需要使用 open

 
   
  1. import org.springframework.boot.SpringApplication

  2. import org.springframework.boot.autoconfigure.SpringBootApplication

  3. /**

  4. * Created by tony on 2018/11/13.

  5. */

  6. @SpringBootApplication

  7. class SpringKotlinApplication

  8. fun main(args: Array<String>) {

  9.    SpringApplication.run(SpringKotlinApplication::class.java, *args)

  10. }

另外,不要忘记配置数据库的信息,例子采用的是 MongoDB。

WebFlux

WebFlux 是 Spring 5 新增的特性,相对于传统 MVC 的同步阻塞IO模型,它采用异步非阻塞的IO模型。

WebFlux 的 Flux 取自于 Reactor 中的类 Flux。Reactor 是 Spring 5 响应式开发的基础。

Reactor 是完全基于响应式流规范设计和实现的库,Flux 和 Mono 是 Reactor 中的两个基本概念。

Flux 类似 RxJava 的 Observable,它可以触发零到多个事件,并根据实际情况结束处理或触发错误。Mono 最多只触发一个事件,它跟 RxJava 的 Single 和 Maybe 类似,所以可以把 Mono 用于在异步任务完成时发出通知。

1.1 创建 Model

首先,创建几个 Model 类。

User 表示用户对象。

 
   
  1. import org.springframework.data.annotation.Id

  2. /**

  3. * Created by tony on 2018/11/22.

  4. */

  5. data class User(@Id val id: String? = null, val name: String, val age: Int, val address: Address) {

  6.    constructor() : this(null, "", 0, Address())

  7.    constructor(name: String, age: Int, address: Address) : this(null, name = name, age = age, address = address)

  8. }

Address 记录用户的地址。

 
   
  1. import org.springframework.data.annotation.Id

  2. /**

  3. * Created by tony on 2018/11/22.

  4. */

  5. data class Address(@Id val id: String? = null, val number: Int, val street: String, val city: String) {

  6.    constructor() : this(null, 0, "", "")

  7.    constructor(number: Int, street: String, city: String) : this(null, number, street, city)

  8. }

Audit 用于记录用户操作的时间。

 
   
  1. import org.springframework.data.annotation.Id

  2. import java.time.LocalDateTime

  3. /**

  4. * Created by tony on 2018/11/22.

  5. */

  6. data class Audit(@Id val id: String? = null, val name: String, val eventDate: LocalDateTime) {

  7.    constructor() : this(null, "",LocalDateTime.now())

  8.    constructor(name: String, eventDate: LocalDateTime) : this(null, name, eventDate)

  9. }

1.2 创建 Repository

创建 UserReactiveRepository 用于 User 对象的查询操作,它实现 ReactiveMongoRepository 接口。

 
   
  1. import com.kotlin.tutorial.model.User

  2. import org.springframework.data.mongodb.repository.ReactiveMongoRepository

  3. import org.springframework.stereotype.Repository

  4. import reactor.core.publisher.Flux

  5. /**

  6. * Created by tony on 2018/11/22.

  7. */

  8. @Repository

  9. interface UserReactiveRepository : ReactiveMongoRepository<User, String> {

  10.    fun findUserByAge(age: Int): Flux<User>

  11.    fun findUserByAddressCity(city: String): Flux<User>

  12.    fun findUserByAgeAndAddressCity(age: Int, city: String): Flux<User>

  13. }

创建 AuditRepository 用于查询用户最近一条的操作时间。

 
   
  1. import com.kotlin.tutorial.model.Audit

  2. import org.springframework.data.repository.CrudRepository

  3. import org.springframework.stereotype.Repository

  4. /**

  5. * Created by tony on 2018/11/22.

  6. */

  7. @Repository

  8. interface AuditRepository: CrudRepository<Audit, String> {

  9.    fun findFirstByNameOrderByEventDateDesc(name: String): Audit

  10. }

1.3 创建 Service

创建 UserReactiveService,通过依赖注入了 userRepository、auditRepository。

 
   
  1. import com.kotlin.tutorial.Utils.toLower

  2. import com.kotlin.tutorial.model.Address

  3. import com.kotlin.tutorial.model.Audit

  4. import com.kotlin.tutorial.model.User

  5. import com.kotlin.tutorial.repository.AuditRepository

  6. import com.kotlin.tutorial.repository.UserReactiveRepository

  7. import org.springframework.beans.factory.annotation.Autowired

  8. import org.springframework.stereotype.Component

  9. import reactor.core.publisher.Flux

  10. import java.time.LocalDateTime

  11. /**

  12. * Created by tony on 2018/11/22.

  13. */

  14. @Component

  15. class UserReactiveService {

  16.    @Autowired

  17.    lateinit var userRepository: UserReactiveRepository

  18.    @Autowired

  19.    lateinit var auditRepository: AuditRepository

  20.    companion object {

  21.        val cities = listOf("Shanghai", "Suzhou", "Hangzhou").toLower()

  22.        val streets = listOf("renming road", "zhongshan road").toLower()

  23.    }

  24.    fun find(age: Int?, rawCity: String?): Flux<User> {

  25.        val city = rawCity?.toLowerCase()

  26.        return when {

  27.            age is Int && city is String -> userRepository.findUserByAgeAndAddressCity(age, city)

  28.            city is String -> userRepository.findUserByAddressCity(city)

  29.            age is Int -> userRepository.findUserByAge(age)

  30.            else -> userRepository.findAll()

  31.        }

  32.    }

  33.    fun generateData(): Flux<User> {

  34.        val list = listOf(20, 25, 33, 28, 34).map {

  35.            val u = generate(it)

  36.            auditRepository.save(Audit(u.name, LocalDateTime.now()))

  37.            u

  38.        }

  39.        return userRepository.deleteAll().thenMany(userRepository.saveAll(list))

  40.    }

  41.    private fun generate(age: Int): User {

  42.        val address = Address(age, streets[age % streets.size], cities[age % cities.size])

  43.        return User("Tony$age", age, address)

  44.    }

  45. }

1.4 创建 Controller

创建 UserController 编写两个 reactive 的接口:

 
   
  1. @RestController

  2. @RequestMapping("/user")

  3. class UserController {

  4.    @Autowired

  5.    lateinit var userReactiveService: UserReactiveService

  6.    @GetMapping("/reactive/find")

  7.    fun findByReactive(@RequestParam age: Int?, @RequestParam city: String?) = userReactiveService.find(age, city)

  8.    @GetMapping("/reactive/generate")

  9.    fun genDataByReactive() = userReactiveService.generateData()

  10.    ......    

  11. }

创建用户的方式:

 
   
  1. curl http://localhost:8080/user/reactive/generate

基于城市查询用户的方式:

 
   
  1. curl http://localhost:8080/user/reactive/find?city=suzhou

RxJava 2

RxJava 库是 JVM 上响应式编程的先驱,也是响应式流规范(Reactive Streams)的基础。

如果对 RxJava 2 不熟悉,也可以购买我的《RxJava 2.x 实战》

2.1 创建 Repository

创建 UserRxJavaRepository 功能跟 UserReactiveRepository 一样,只是多了一个 findUserByName() 方法。

 
   
  1. import com.kotlin.tutorial.model.User

  2. import io.reactivex.Flowable

  3. import org.springframework.data.repository.reactive.RxJava2CrudRepository

  4. import org.springframework.stereotype.Repository

  5. /**

  6. * Created by tony on 2018/11/22.

  7. */

  8. @Repository

  9. interface UserRxJavaRepository : RxJava2CrudRepository<User, String> {

  10.    fun findUserByName(name: String): Flowable<User>

  11.    fun findUserByAge(age: Int): Flowable<User>

  12.    fun findUserByAddressCity(city: String): Flowable<User>

  13.    fun findUserByAgeAndAddressCity(age: Int, city: String): Flowable<User>

  14. }

2.2 创建 JavaService

创建 UserRxJavaService ,类似于 UserReactiveService。但是,多了两个方法:findByName()、login()。其中,调用 login() 会添加一条审计的记录。

 
   
  1. import com.kotlin.tutorial.Utils.toLower

  2. import com.kotlin.tutorial.model.Address

  3. import com.kotlin.tutorial.model.Audit

  4. import com.kotlin.tutorial.model.User

  5. import com.kotlin.tutorial.repository.AuditRepository

  6. import com.kotlin.tutorial.repository.UserRxJavaRepository

  7. import io.reactivex.Flowable

  8. import org.springframework.beans.factory.annotation.Autowired

  9. import org.springframework.stereotype.Component

  10. import java.time.LocalDateTime

  11. /**

  12. * Created by tony on 2018/11/22.

  13. */

  14. @Component

  15. class UserRxJavaService {

  16.    @Autowired

  17.    lateinit var userRepository: UserRxJavaRepository

  18.    @Autowired

  19.    lateinit var auditRepository: AuditRepository

  20.    companion object {

  21.        val cities = listOf("Shanghai", "Suzhou", "Hangzhou").toLower()

  22.        val streets = listOf("renming road", "zhongshan road").toLower()

  23.    }

  24.    fun findByName(name: String): Flowable<User> = userRepository.findUserByName(name)

  25.    fun find(age: Int?, rawCity: String?): Flowable<User> {

  26.        val city = rawCity?.toLowerCase()

  27.        return when {

  28.            age is Int && city is String -> userRepository.findUserByAgeAndAddressCity(age, city)

  29.            city is String -> userRepository.findUserByAddressCity(city)

  30.            age is Int -> userRepository.findUserByAge(age)

  31.            else -> userRepository.findAll()

  32.        }

  33.    }

  34.    fun generateData(): Flowable<User> {

  35.        val list = listOf(20, 25, 33, 28, 34).map {

  36.            val u = generate(it)

  37.            auditRepository.save(Audit(u.name, LocalDateTime.now()))

  38.            u

  39.        }

  40.        return userRepository.deleteAll().andThen(userRepository.saveAll(list))

  41.    }

  42.    private fun generate(age: Int): User {

  43.        val address = Address(age, streets[age % streets.size], cities[age % cities.size])

  44.        return User("Tony$age", age, address)

  45.    }

  46.    fun login(name: String) =

  47.            userRepository.findUserByName(name)

  48.            .map {

  49.                auditRepository.save(Audit(it.name, LocalDateTime.now()))

  50.            }

  51. }

2.3 创建 Controller

在原有的 UserController 中新增两个 rxjava 的接口:

 
   
  1. @RestController

  2. @RequestMapping("/user")

  3. class UserController {

  4.    @Autowired

  5.    lateinit var userRxJavaService: UserRxJavaService

  6.    @GetMapping("/rxjava/find")

  7.    fun findByRx(@RequestParam age: Int?, @RequestParam city: String?) = userRxJavaService.find(age, city)

  8.    @GetMapping("/rxjava/generate")

  9.    fun genDateByRx() = userRxJavaService.generateData()

  10.    ......

  11. }

Kotlin 1.3 的 Coroutines

协程(coroutine)相比于线程更加轻量级,协程又称为微线程。线程和协程的一个显著区别是,线程的阻塞代价是昂贵的,而协程使用了更简单、代价更小的挂起(suspend)来代替阻塞。

Coroutines 是 Kotlin 1.1 增加的实验的功能,到 Kotlin 1.3 已经变成了正式的功能。

先在 UserController 创建一个模拟登陆的接口,访问该接口时会添加一条审计的记录

 
   
  1.    @GetMapping("/rxjava/login")

  2.    fun mockLogin(@RequestParam username: String) = userRxJavaService.login(username)

然后尝试用传统的 blocking 方式来编写一个获取登陆信息的接口:

 
   
  1.    @GetMapping("/blocking/{username}")

  2.    fun getNormalLoginMessage(@PathVariable username: String):String {

  3.        val user = userService.findByName(username)

  4.        val lastLoginTime = auditService.findByName(user.name).eventDate

  5.        return "Hi ${user.name}, you have logged in since $lastLoginTime"

  6.    }

再尝试用 RxJava 的方式来编写该接口:

 
   
  1.    @GetMapping("/rxjava/{username}")

  2.    fun getRxLoginMessage(@PathVariable username: String)=

  3.            userRxJavaService.findByName(username)

  4.                    .map {

  5.                        auditService.findByName(it.name).eventDate

  6.                    }

  7.                    .map {

  8.                        "Hi ${username}, you have logged in since $it"

  9.                    }

最后,使用 Coroutines 的方式来编写接口:

 
   
  1.    @GetMapping("/coroutine/{username}")

  2.    fun getLoginMessage(@PathVariable username: String) = runBlocking {

  3.        val user = userRxJavaService.findByName(username).awaitSingle()

  4.        val lastLoginTime = GlobalScope.async {

  5.            auditService.findByName(user.name).eventDate

  6.        }.await()

  7.        "Hi ${user.name}, you have logged in since $lastLoginTime"

  8.    }

可以看到,使用协程的方式类似于传统的 blocking 的方式来编写代码。

模拟用户登陆:

使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程_第2张图片

使用 Coroutines 的方式获取登陆信息:

使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程_第3张图片

关于协程,更多可以参考之前写的 Coroutines 笔记:

Kotlin Coroutines 笔记 (一)、Kotlin Coroutines 笔记 (二)

虽然 Kotlin 1.3 之后有些变动,但是大体是不变的。之后,也会整理更多 Kotlin Coroutines 笔记。

总结

响应式开发是未来的趋势,无论是服务端开发还是移动端开发,都会顺应这个趋势。

另外,Kotlin 1.3 之后的协程已经是正式版本,Kotlin 在语言级别上支持了协程,它是异步编程的另一个不错的选择。

本文 demo 的 github 地址:https://github.com/fengzhizi715/kotlin-spring-reactive-coroutine-demo


关注【Java与Android技术栈】

更多精彩内容请关注扫码

使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程_第4张图片


你可能感兴趣的:(使用 Kotlin + WebFlux/RxJava 2 实现响应式以及尝试正式版本的协程)