为什么要重新选择后端技术
过去的一年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的解决方案。在业余时间的一些尝试后,笔者对它是非常满意的,并认定它将是未来笔者在后端的主要技术栈。
为什么响应式编程没有成为主流?
如笔者上述所言,类似的响应式编程在性能上有极大的优势,但它一直未能成为主流。笔者也在思考这个现象。总结出部分原因如下:
原因一:思维差异+可维护性差
这些年,响应式编程的概念很火,但事实上一直未能成为主流。响应式编程有着非常好的性能优势,非阻塞式的实现机制比阻塞式的实现机制确实好很多,但它仍有一个非常难以解决的问题,那就是
响应式编程带来的异步编程思维并不符合人类的思维
人的思维是什么,我们理解一个事情的基本思维仍是面向对象及过程的,比如我们理解的上班是这样的
- 先起床
- 乘座交通工具去公司
- 早上开例会,安排任务
- 开始编码
如果就这件事,我们按照传统的面向对象及阻塞式的思维来编码,它是这样的
#这是伪代码,不要当真
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桌面开发的代码中,是这样使用异步的
#TypeScript代码
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一样,写出类似风格的代码
[@Test](https://my.oschina.net/azibug)
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协程的结合,提供了类似的解决方案,使得我们在异步编程中,仍然能以符号人类思维的方式来编码。
几年前,Google将Kotlin取代Java选定为Android开发的第一语言,这并不是空穴来风的决定。想必Google也是在认真考察并认可这门语言才决定的。 事实上也确实如此,Kotlin号称Better Java
,与其它JVM语言相比,它更简洁与优雅。
笔者仅举一例来说明
private static DocumentRepository repository;
private static DocumentRepository getDocumentRepository(){
if(Objects.isNull(repository)){
Document.repository = InstanceFactory.getInstance(DocumentRepository.class);
}
return Document.repository;
}
而在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 Boot的实现,则弱于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](https://my.oschina.net/u/1260961)
@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()
}
}
以上,敬请期待!!!
更多优质文章,请访问笔者的个人网站 https://lingenliu.cc 或关公众号:【御剑轩】 - 致力于实践与传播优雅的编码之道