我最近在timeular做了不少kotlin的工作,必须说到目前为止我非常喜欢它。
另外,随着新的Spring Boot 2.0.0的推出,对kotlin的支持也得到了改善,现在Spring MVC web框架还有一个反应式选项,叫做Webflux。
在这篇文章中,我们将看看如何使用Webflux创建一个小型、基于Kotlin的Spring-Boot应用程序。我们将创建一个具有单个GET /api 路由的Web应用,它从jsonplaceholder中并行获取posts 和comments ,并将它们转换为一个组合输出。
Webflux使用下面的Reactor库,它有两个重要的类型:Flux 和Mono 。一个Flux 是0到n个值的反应式Publisher ,而一个Mono 则仅限于0或1个值。更详细的解释可以在这里找到。
在Webflux中还有一个新的功能DSL用于指定路由,但在这个例子中,我们将坚持使用好的老的@RestController 来定义我们的路由。
让我们开始吧。
例子
首先,让我们在start.spring.io/创建一个新的Spring Boot应用程序,选择Reactive Web 的依赖关系。有了这个应用模板,我们就可以开始实施,把Spring Boot应用配置成一个reactive 的Web应用。
@SpringBootApplication
class KotlinWebfluxDemoApplication
fun main(args: Array<String>) {
val app = SpringApplication(KotlinWebfluxDemoApplication::class.java)
app.webApplicationType = WebApplicationType.REACTIVE
app.run(*args)
}
我们还创建了一个WebConfig 类,其中添加了cors 映射,有@EnableWebFlux 注解,并设置了我们的依赖注入配置。
@Configuration
@EnableWebFlux
@ComponentScan("org.zupzup.kotlinwebfluxdemo")
class WebConfig: WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("api/**")
}
}
随着模板的完成,我们来创建一些数据模型。Kotlin为此提供了伟大的data class ,它让我们以一种高度简洁的方式定义价值类。
在这个例子中,我们需要一个Post 和Comment 作为数据源,一个Response 类作为我们转换后的响应。
data class Comment(
val postId: Int,
val id: Int,
val name: String,
val email: String,
val body: String
)
data class Post(
val userId: Int,
val id: Int,
val title: String,
val body: String
)
data class Response(
val postId: Int,
val userId: Int,
val title: String,
val comments: List<LightComment>
)
data class LightComment(
val email: String,
val body: String
)
为了从jsonplaceholder ,我们创建了一个APIService ,它使用集成在Webflux中的反应式WebClient 。
这很好,因为它使我们能够轻松地将我们的响应转换为由返回的JSON表示的模型的Flux 或Mono 。
@Service
class APIService {
fun fetchComments(postId: Int): Flux<Comment> = fetch("posts/$postId/comments").bodyToFlux(Comment::class.java)
fun fetchPosts(): Flux<Post> = fetch("/posts").bodyToFlux(Post::class.java)
fun fetch(path: String): WebClient.ResponseSpec {
val client = WebClient.create("http://jsonplaceholder.typicode.com/")
return client.get().uri(path).retrieve()
}
}
这就是我们获取数据并将其转换为我们模型的反应式版本所需要的一切。非常酷。WebClient 工作得很好,提供了你所期望的各种实用功能。
最后也是最重要/最有趣的部分是我们的处理程序。在这个小例子中,为了简单起见,我们将在控制器中实现整个逻辑。
posts 我们的目标是从jsonplaceholder ,取20个id ,然后对每个post ,取其comments ,并将posts 和comments 转化为这样的嵌套数据结构。
[
{
"postId": 1,
"userId": 2,
"title": "...",
"comments":
[
{
"email": "...",
"body": "..."
}
]
}
]
让我们看看这是如何工作的。
@RestController
@RequestMapping(path = ["/api"], produces = [ APPLICATION_JSON_UTF8_VALUE ])
class APIController(
private val apiService: APIService
) {
@RequestMapping(method = [RequestMethod.GET])
fun getData(): Mono<ResponseEntity<List<Response>>> {
return apiService.fetchPosts()
.filter { it -> it.userId % 2 == 0 }
.take(20)
.parallel(4)
.runOn(Schedulers.parallel())
.map { post -> apiService.fetchComments(post.id)
.map { comment -> LightComment(email = comment.email, body = comment.body) }
.collectList()
.zipWith(post.toMono()) }
.flatMap { it -> it }
.map { result -> Response(
postId = result.t2.id,
userId = result.t2.userId,
title = result.t2.title,
comments = result.t1
) }
.sequential()
.collectList()
.map { body -> ResponseEntity.ok().body(body) }
.toMono()
}
}
好吧,这里发生了不少事情,如果你不习惯函数式编程或反应式序列,这可能一开始看起来不是很直观--不用担心,对每个人来说都一样。
我们首先使用我们注入的apiService 来获取posts 。然后我们filter ,这个反应式流的post 元素,只使用偶数id的posts ,只取前20个元素(take(20) )。到目前为止,一切都很好。
为了获取具有明确并行性的4 的评论,我们将.parallel(4) and .runOn(Schedulers.parallel()) 添加到流中。后来,我们必须再次调用.sequential() ,以等待所有并行获取的值,否则我们无法将它们塞进一个List中,我们需要将zip 、comments 和post 。
为了清楚起见,parallel(4) 并非是并发获取评论所必需的,而是明确地控制了并发和异步处理的并行性。正如@hpgrahsl在twitter上向我指出的那样,对于这个用例来说,习惯性的推荐方法是直接使用flatMap 。
在任何情况下,下一步是将map posts 到另一个反应式序列。在这个新的序列中,我们用给定的post.id 来调用fetchComments ,以获取一个帖子的评论。我们的目标是将这些comments 映射到LightComment ,扔掉一些我们不想显示的数据,并将它们与post 的数据zip ,这样我们就可以在以后创建上述的数据结构。
现在我们有一个来自zipWith 操作的Tuple2<List<LightComment, Post>> 的序列。我们使用flatMap ,将流从Monos 的序列转换为只有我们的压缩数据的序列,然后我们简单地将其map 到Response 模型。
现在剩下的就是collecting ,所有的值都是一个列表,然后把这个列表映射到一个Mono ,里面有一个ResponseEntity ,包括我们的数据作为主体,这就是控制器所期望的返回值。
这就是了!
这个例子的全部代码可以在这里找到。
总结
反应式编程,当你习惯了这种风格,可以有很多乐趣。以非阻塞的方式编写应用程序有很多好处,尤其是在处理微服务架构的时候。
这种异步、非阻塞、流转换的编码方式也有一些缺点,比如它本身就很难测试和调试,而且一开始会有一些学习曲线,但在我看来,对于正确的用例来说,好处超过了它们。
我已经有一段时间没有使用Spring Boot或Spring了,在此期间发生的进步让我印象深刻。Webflux,特别是与Kotlin的合作,看起来是我在未来的服务中会强烈考虑的东西。)
感谢@hpgrahsl在使用parallel 和flatMap 方面提供的宝贵反馈。