vlambda博客
学习文章列表

为什么我会选择Vert.x与Kotlin

为什么要重新选择后端技


过去的一年2020对笔者来说是非常有价值的一年,笔者在工作上大部分精力都花费在基于TypeScript + React的Electron桌面开发及前端开发以及WorkPlus Lite移动端开发等工作上。


而在后端方面,2020年笔者在自己的一个业余项目上使用了Spring Boot技术,并整理抽象出了一个基于DDD领域驱动风格的开源框架mydddd-backend。


笔者非常清楚,在后端技术方面,Spring仍然是主流,凭借强大的生态及完整的解决方案,Spring依然是大部分公司及团队的第一选择。这也是笔者在整理myddd-backend框架时为什么选择基于Spring Boot来实现的原因所在。因为笔者相信它能适合大多数团队。


进入2021年,笔者觉得需要重新关注下后端技术,以思考是否需要选择新的技术做为笔者后端技术解决方案,之所以有这种想法,也是基于以下几个原因


  • 在使用Spring Boot的过程中,仍然感觉它非常中规中矩,说不上哪不好,毕竟是一个完整的生态及解决方案。但始终感觉不到其的魅力所在。


  • 近些年兴起的一些新的编程理念与语言让笔者一直想尝试下,如响应式编程以及Kotlin这个号称Better Java的语言等。事实上,在Google的推动下,Kotlin被人接受的程度越来越高,使用的程序员也越来越多了。


  • 传统Java语言及阻塞式编程并无问题,笔者认为它仍是大多数团队与公司的第一选择。但非阻塞式的异步编程的优点也非常突出,如果程序员及团队能掌控,确实可以尝试。


在这些原因的影响下,笔者也一直在思考是否要重新选择新的技术栈来尝试。


经过一些思考了解及尝试后,笔者选择了VertX与Kotlin的解决方案。在业余时间的一些尝试后,笔者对它是非常满意的,并认定它将是未来笔者在后端的主要技术栈。


为什么响应式编程没有成为主流?


如笔者上述所言,类似的响应式编程在性能上有极大的优势,但它一直未能成为主流。笔者也在思考这个现象。总结出部分原因如下:


原因一:思维差异+可维护性差


这些年,响应式编程很火,但事实上一直未能成为主流。响应式编程有着非常好的性能优势,非阻塞式的实现机制比阻塞式的实现机制确实有它独特的优势,但它仍有一个非常难以解决的问题,那就是


响应式编程带来的异步编程思维并不符合人类的思维


人的思维是什么,我们理解一个事情的基本思维仍是面向对象及过程的,比如我们理解的上班是这样的


  1. 先起床

  2. 交通工具去公司

  3. 早上开例会,安排任务

  4. 开始编码


如果就这件事,我们按照传统的面向对象及阻塞式的思维来编码,它是这样的


#这是伪代码,不要当真
class Coder{
public void work(){ this.getUp() this.driveToOfficePlace() this.joinMeeting() this.code() } }


我们可以明显看到,这个编码与我们的思维是完全一致的,这就是我们所固有的习惯,阻塞式及同步的方式,是符合我们的思维的。


如果我们用一种响应式编程中的异步编程来实现,大致的代码可能是这样的


#这是伪代码,不要当真
class Coder{
public void work(){ this.getUp().onSuccess(()->{ this.driveToOfficePlace(()->{ this.joinMeeting(()->{ this.code() }) }) }) } }


上面这个已经很简化了,事实上的业务不可能这么简洁,你可以想象当这个代码中充满各种代码细节时的场景。


大致上所有的异步编程都有这种风格,因为这种风格与我们人类思维上存在差异,所以有个非常著名的名字来称为它:回调地狱


当然,写Java的可能对这个不太清楚,但前些年,使用NodeJs的程序员对它可谓不所不知。事实上,移动端也一并存在类似的问题。


而且很明显,这种风格的代码在阅读与理解上相比更困难,这也是它可维护性较差的原因之一,也因此一并造成很多程序员写不好类似风格的代码,一方面思维上的不协调,而另一方面可维护性上也不是很好。


而大多数公司和团队仍然有赖于大多数程序员的工作,这也是类似的编码模式一直未能成为主流的主要原因。


原因二:生态较差


如果我们说生态,那坦率的说,没有比Java语言生态更强大的语言了,这也是之所以这么多年,不喜欢Java的人非常多,但Java一直是后端的主力开发语言。相比较而言,一些响应式的框架如果从生态上相比,就比Java差远了。类似RXJava等响应式编程语言,更多的是属于一个技术类库,其在生态上的不足也必然会阻碍一些程序员。


举例来说: 我如何用异步方式与数据库打交道?是否支持微服务?如何做OAUTH2权限?在Java的世界,你不需要为这些担忧,任何一个问题都有一大批成熟的解决方案。但在异步编程的世界,就相对差了很多。


为什么笔者会选择Vert.x与Kotlin的结合


但凡事并无绝对,基于对未来的一些考量,笔者还是希望能在这方面有所建树,所以近期关注并研究了一些技术。最终选择了Vert.x与Kotlin的结合。并对它们的表现非常满意


在尝试后,笔者认为至少在以下几个方面,它们是笔者值得选择的理由

  • 简洁优雅,而不失可维护性

  • 较为完整的生态

  • 性能上的绝对优势


理由一:简洁优雅,可维护性佳

其一:改善了回调地狱的现象

事实上,如笔者所述的前面的回调地狱问题,这个已经有较好的解决方案了。

如笔者在一个Electron桌面开发的代码中,是这样使用异步的

本代码摘自笔者的基于Electron开发的一个跨平台桌面软件

 public static async syncFavors(): Promise<Contact[]> { //从网络获取星标联系人,这实质上是一个异步行为 const favors = await Contact.getNet().fetchFavorContacts(); if (favors) { //存储到数据库中,这也是一个异步操作 await Contact.getRespository().batchSaveFavors(favors); } return favors; }


如上述代码所示,在JS的世界中,早已出现了Promise与await的来解决这个问题。将非阻塞回调转成同步风格但实质还是非阻塞。


虽然Vert.x本身未提供类似的功能,但Kotlin协程则提供了。基于它们的结合,就算是在异步编程中,你也可以如同前端TS一样,写出类似风格的代码


本代码摘自笔者的myddd-vertx框架,基于Vert.x与Kotlin的响应式领域驱动实现

 @Test fun testExists(vertx:Vertx, testContext: VertxTestContext){ GlobalScope.launch { try { val user = User(username = "lingen",age = 35) //这是一个异步代码,但我们用await()来解决回调 val createdUser = repository.save(user).await() //这又是一个异步 var exists =repository.exists(User::class.java,createdUser.id).await() testContext.verify { Assertions.assertTrue(exists) } testContext.completeNow() }catch (e:Exception){ testContext.failNow(e) }
} }



可以看出,Vert.x与Kotlin协程的结合,提供了类似的解决方案,使得我们在异步编程中,仍然能以符合人类思维的方式来编码。


其二 Kotlin本身足够简洁与优雅


几年前,Google将Kotlin取代Java选定为Android开发的第一语言,这并不是空穴来风的决定。想必Google也是在认真考察并认可这门语言才决定的。事实上也确实如此,Kotlin号称Better Java,与其它JVM语言相比,它更简洁与优雅。


笔者仅举一例来说明


本代码摘自于笔者的myddd-backend框架,基于Java与Spring Boot的领域驱动实现

 private static DocumentRepository repository;
private static DocumentRepository getDocumentRepository(){ if(Objects.isNull(repository)){ Document.repository = InstanceFactory.getInstance(DocumentRepository.class); } return Document.repository; }



而在Vert.x与Kotlin中,实现是这样的

本代码摘自笔者的myddd-vertx框架,基于Vert.x与Kotlin的响应式领域驱动实现

 companion object { val repository:CommentRepository by lazy { InstanceFactory.getInstance(CommentRepository::class.java) } }



如上述代码所示,同一个获取仓储的方式,在Kotlin的代码中,比Java的实现好很多。


笔者就不举更多例了,Kotlin相对Java,确实是更加简洁与优雅,而又不失可维护性。


较为完整的生态


如笔者前述所言,类似的异步编程也好,响应式编程框架也好,在生态上都存在问题。表现为生态不够完善。但这一点,在Vert.x反而是个优势。


之所以选择Vert.x,也是因为笔者在看到它的生态之后,才决定更进一步了解它。

Vert.x基本有自己的一整套生态,意味着你选择它,不用为项目中的任何一个维度的事情发愁,而且这些生态都是官方自己负责维护的,质量也比较有保证。


其在Web,数据库,单元测试,权限,微服务支持,消息事件机制,集群等有完整的解决方案。


如上图所示,Vert.x基本在每一方面都有自己的解决方案,这是非常明显的一个优势。


性能上的绝对优势


如果没有前两个优势,对笔者而言,这个优势并不足以成为可以考量的优势。因为,笔者始终将代码的可维护性放在第一重要位置来考量。

但如果有前两个优势,那这就成为另一个绝对优势了

在国外的性能大对比中,Vert.x始终处于前列。

而基于Spring的实现,则弱于Vert.x数倍。


结论


所以,综上所述,如果能写出简洁优雅的代码,生态又足够完善,又在性能上足够有优势。为什么不选择它?


myddd-vertx


所以,笔者正在基于Vert.x与Kotlin,按照领域驱动的理念,开发myddd-vertx框架。


这个框架已接近完成,后续也会如同myddd-backend一样,开源出来。



基于myddd-vertx 部分代码示例

仓储实现

class CommentRepositoryHibernate : EntityRepositoryHibernate(),CommentRepository {
override suspend fun createComment(comment: Comment): Future<Comment> { val future = PromiseImpl<Comment>() require(comment.id == 0L) comment.created = System.currentTimeMillis() var created = save(comment).await() created.rootCommentId = created.id created = save(created).await() future.onSuccess(created) return future }}

领域层示例

@Entity@Table(name = "comment", indexes = [Index(name = "index_comment_id",columnList = "comment_id"), Index(name = "index_root_comment_id",columnList = "root_comment_id"), ])class Comment : BaseEntity() {
/** * 关联文章 */ @Column(name = "comment_id") lateinit var commentId:String
/** * 关联评论根ID */ @Column(name = "root_comment_id") var rootCommentId:Long = 0
/** * 关联回复评论ID */ @Column(name = "parent_comment_id") var parentCommentId:Long = 0
@Column(name = "level") var level:Int = 0
/** * 昵称 */ var author:String? = null
/** * 邮箱 */ var email:String? = null
/** * 回复内容 */ lateinit var content:String
companion object { val repository:CommentRepository by lazy { InstanceFactory.getInstance(CommentRepository::class.java) } }
suspend fun createComment():Future<Comment> { return repository.createComment(this) }
suspend fun createReplyComment(parentComment: Comment):Future<Comment> { return repository.createReplyComment(parentComment,this) }}

单元测试

 @Test fun testAddComment(vertx: Vertx, testContext: VertxTestContext){ GlobalScope.launch { val comment = createComment() val created = commentRepository.createComment(comment).await() testContext.verify { Assertions.assertTrue(created.id > 0) Assertions.assertEquals(created.id,created.rootCommentId) Assertions.assertEquals(created.level,0) }
testContext.completeNow() } }


以上,敬请期待!!!