我觉得我还是不喜欢AOP和注解这套写法

写了差不多一个月的Quarkus,对这套写法也差不多熟悉了。不过我还是想说很不喜欢这种写法。当然Quarkus大约比Spring还是好点,没有字符串硬写的切面了,最多也只是@Inject这样的依赖注入语法。

一般来说,依赖注入的实现不外乎两种思路,通过constructor传参,或者在类型里面添加setter后在外部调用。注解的实现机制,Spring的autowire也好,Quarkus的ArC也好,也不过是通过注解自动添加这些setter,用框架在运行时自动地把值构造了然后注入进环境里面。

看起来好像没什么问题?

但是这套机制总归是不透明的,对Spring还好,Quarkus这种基于vert.x的框架,底层的异步和事件驱动等概念,对于一般的Java开发来说并不是很直观的事情。我的话有了之前vert.x的经验可能还好,遇到了问题还能快速排查问题根源,如果是只写过同步代码的Java开发,看到有时候可能具有误导性的错误提示,可能连排错都不知道从何开始。

除了依赖注入,大型的后端框架也常常使用AOP的概念,而在实际操作里,这往往就是插入一些「切面」,也就是在运行时修改代码,给人的感觉就是难以理解,难以调试,而且还没类型安全,还得经常和基于字符串的注解搭配使用。

在这方面Quarkus倒是提供了一种叫ArC的功能,和AOP的一般概念不一样,把依赖注入和其他类似于AOP的行为提前到编译期,一方面提高了性能和加快了程序的加载速度,但是更重要的大概就是提供了类型安全的保证了。

而且吧,Quarkus可能更多是Java的思路,从Kotlin的角度讲,问题就很多了……

  • 首先是,因为注解生成getter/setter的原因,所有的注入变量都必须用lateinit var的形式,并且可能还得用@field来指明注入的是backing field,因为约定大于配置嘛
  • 然后就是无处不在的open,不管是class还是method处,还有虽然会被注入所以会被使用但是在IDE里面被标灰因为没有直接调用的函数,当然了,这些函数虽然实际上是静态的,但都是以实例方法的形式写的,class也不可能写成单例
  • 还有类型的默认无参构造,尤其是Hibernate托管的Entity,甚至由于这一层托管和注入的不透明,用secondary constructor来delegate到默认构造器的思路也不可行

但是话说过来,到处都是all-openno-args的Kotlin,可以说抛弃了很多Kotlin相比较Java的优势吧。

约定大于配置的另一个问题,就是Quarkus的Event Bus对Coroutine的不支持。

在任何一个@ConsumeEvent标记的函数前加上suspend签名,都会报以下错误:

An event consumer business method with two parameters must have MultiMap as the first parameter

这里的MultiMap相当于消息的信息头,也就是说Quarkus对Event Bus的处理是写死了函数签名为单一入参的形式。

但是Kotlin经过CPS变换后的协程函数,在Java字节码层面相当于是附加了一个额外的continuation参数,这样一来,约定大于配置也就不管用了。

考虑到Event Bus对于Quarkus算是比较底层的代码,在可预见的未来,都不太能期待Quarkus增加对协程的支持。这是否也是约定大于配置和基于注解的注入的代价呢?

当然,这问题说到底还是因为Java的发展过于保守,缺乏一个统一的异步API的结果。

说到这里,也许可以对比一下vert.x。

在一个vert.x的应用里,依赖注入一般有两种办法,一个就是最典型的构造器参数,当然,这个在Java社区是要被批判的。

另一个就是通过lambda把handler反转出去,这个是更加函数式的做法。

Quarkus还是一个很不错的框架,毕竟提供了很多裸金属的vert.x不具备的高级功能和插件,但是在开发体验和开发者对代码的控制粒度上,究竟哪一种做法更舒服,就见仁见智了。

这也可能是结合一种基于Java的框架和Kotlin这种「非主流」JVM语言的时候引入的额外负担。毕竟大部分的JVM框架都是优先面向Java的。像Kotlin这样的语言虽然提供了更便捷友好的语法,也提供了良好的兼容性,但是要在编程范式/思想上完全嵌入所有JVM框架,依然是一个很大的挑战。


我觉得我还是不喜欢AOP和注解这套写法
http://inori.moe/2023/07/12/i-dont-like-aop-and-annotations/
作者
inori
发布于
2023年7月12日
许可协议