Java平台之2021年现状 - James Ward


早在2000年代初期,许多开发人员就被Java过于复杂的世界所吓坏。四种模式和中间件/ J2EE / Java EE的组合导致所谓的脱钩的荒谬程度,从我在2002年研究的开源J2EE电子商务系统的此序列图中可以明显看出:

早在2014年,我就发生了什么变化写了一篇文章:Java不烂,您只是在错误地使用了它。但是自从我写这篇文章以来已经过去了六年,并且事情还在不断改善,这使得Java平台成为构建微服务,数据管道,Web应用程序,移动应用程序等的绝佳选择。让我们来看一下Java平台的一些“现代”(截至2021年)方面。
 
三种排名前20位的编程语言
Java既是语言也是平台。老实说,过去十年来我没有写太多实际的Java,但是我已经在Java虚拟机(JVM)上构建了相当多的东西。Java语言在不断发展。Java语言架构师Brian Goetz将其描述为“最后的优势”:在其他语言彻底证明某个功能有用之后,再添加这些功能。对于数以百万计的Java开发人员而言,创新步伐缓慢是一件好事,他们能够继续专注于降低由演化引起的风险。长期以来,向后兼容一直是Java语言的一项重要功能,这是对于那些喜欢缓慢前进而可能又一无所获的企业而言。
除了Java语言本身之外,Java生态系统还涵盖了其他流行的语言,包括Scala和Kotlin。实际上,过去十年来我编写的大多数代码都在Scala中使用,Scala现在是第14大最受欢迎的编程语言(根据RedMonk)。对于我来说,Scala经历了一段旅程,从“没有分号的Java”方法开始,到现在尝试完全采用静态类型的函数式编程。Scala继续尝试即将进行的实验,即将发布的Scala 3版本结束了多年的博士学位研究,并且马丁·奥德斯基(Martin Odersky)将类型系统建立在可验证演算基础上的宏伟愿景。我真的很喜欢编写Scala代码并不断改进。感觉非常前沿,与Go等更古老的语言相比,我的个人生产力感觉要好得多。
在过去的两年中,我编写了很多Kotlin代码。Kotlin现在被RedMonk排名第18位,紧随Go之后。当Google决定将其设置为Android开发的默认设置时,Kotlin的使用量猛增。但是我没有做太多的Android编程。Kotlin已经成为构建后端的一种非常有用的语言。类型推断,显式可空性和结构化并发(协程)等语言功能使其非常适合构建现代服务器。当我写Kotlin时,我确实会错过Scala中的一些东西,但是总的来说,我的工作效率很高,而且Java开发人员更容易编写代码。
Scala和Kotlin使用共享工具,库等在Java平台生态系统上构建。例如,Netty(一个Java库)可能是按类型处理最多全局流量的HTTP服务器。它几乎存在于所有大型企业系统中。许多Scala和Kotlin服务器框架都使用Netty,因为互操作性才有效。
如果您离开Java语言已有一段时间了,并且想再看看一下,请查看Kotlin。它具有许多现代语言功能,不会有太大的不同。有关一些不错的功能的概述,请查看我的视频:Kotlin-更好,更云友好的Java。如果您想跳到更现代的东西,请查看Scala,其中最近的大部分精力都在编写并发和可组合的代码上,这些代码可以被编译器验证为正确。我真的很喜欢ZIO,这是一个Scala框架,可为纯功能程序提供类似Lego的体验。
 
专业和成熟的工具
当我在其他平台上使用开发工具时,通常会感到失望。我其实不需要在系统上安装一些特殊的本机库,因此可以安装依赖项。我其实也不需要花几个小时就可以设置本地开发人员工具链。IDE中的代码提示应该准确,快速。构建系统应该提供执行常见任务的标准方法:构建,运行,测试,调用静态代码工具,重新格式化代码以及我们一直在做的其他工作。
在Java平台世界中,有一些用于构建工具和IDE的选项。我在IntelliJ IDEA for Java,Kotlin和Scala方面拥有丰富的经验。但是VS Code也能很好地工作。使用Java和Kotlin时,我发现Gradle或Maven构建工具是成熟,快速且功能齐全的。在Scala中,我使用sbt,它提供了我理想的开发人员经验。所有这些都在IntelliJ中得到直接支持,因此我的构建系统和IDE对依赖项具有一致的理解。
使用数据库的应用程序通常不使用生产数据库类型进行开发和本地集成测试,因为这太痛苦了,有时速度很慢。这导致许多开发人员将内存数据库用于开发和测试。反过来,由于开发数据库和生产数据库之间的差异,这导致了巨大的问题和局限性。一些开发人员已开始使用具有数据库依赖性的Docker容器来提供一致性。但这在处理数据库的生命周期方面造成了痛苦。Testcontainers项目通过在应用程序生命周期中管理数据库(或其他容器化服务),已经成为解决此问题的好方法。例如,集成测试可以根据需要启动Postgres数据库,以进行一系列测试,每个测试用例或所有测试,并且最好将所有生命周期作为测试本身的一部分进行管理。
减少时间来验证更改对于生产力至关重要。在典型的开发人员工作流程中,我可以在几秒钟内测试我正在处理的内容。这意味着编译器会运行(根据我使用的语言和框架,验证级别会有所不同),正在运行的测试以及运行的测试(如果正在构建Web应用程序),则服务器会重新启动,并且浏览器会自动重新加载。除了保存文件之外,所有这些事情都可以发生而无需我采取任何行动。Gradle和sbt通过连续的构建/测试/开发模式立即支持这种开箱即用的功能。Quarkus框架在Maven中也支持此功能。
 
生产框架
在Java生态系统中,您只需要编写直接处理HTTP请求并将代码硬链接到调用链的代码即可。但是通常使用框架来简化如何将代码库的各个部分连接在一起以供重用和用于不同的环境(开发,测试,生产)。接线方式有不同的方法。Java生态系统中最典型的方法称为“依赖注入(DI)”,并已由Spring Framework普及。
Spring Boot(使用Spring框架的“现代”方式)是Java生态系统框架的王者,可与Java和Kotlin一起很好地工作。当我与面向非功能性程序员的团队一起编写代码时,通常会使用Kotlin和Spring Boot。该代码通常易于遵循,并且开发人员的经验非常好。该区域有许多替代方案,但以下两种方案比较突出,您可能要使用它们的原因:

  • Micronaut-与Spring Boot相似的编程模型,但有更多现代变化,例如编译时依赖注入
  • Quarkus-基于Java EE的依赖注入,特别关注开发人员的工作效率

要进行更全面的比较,请观看我的演讲:Kotlin Server Framework Smackdown
借助Scala,还有各种各样的框架,从类似Spring Boot到功能强大的框架。我一直是Play Framework开发人员工作效率的忠实拥护者,但是如果您想要进行这种风格的编程,请使用Kotlin。当您更深入地使用函数式编程时,Scala确实会发光。因此,ZIO是我的主要建议,但是还没有完善的Web框架。有一些库和基本的片段,但是还没有什么能与Play和Spring Boot的端到端框架体验相匹配。一个解决方案正在开发中,但是如果您现在想使用Scala为Web应用程序提供纯功能,最好的选择是http4s,它可以与ZIO结合使用。
 
反应性应用不断普及
无阻塞/响应性是“现代”与传统的主要标界元素之一。当传统系统仅在等待某些事情发生时(例如数据库或Web服务进行响应),它们通常会使用大量资源。这显然很愚蠢,但是要修复它通常需要新的编程模型,因为传统的命令性代码无法暂停并在等待结束后恢复。Java长期以来一直使用无阻塞IO(NIO),并且大多数网络库都已经使用了很长时间,其中Netty是最常用的。遗留代码库可能仍在使用阻塞API,因为进行响应式操作会带来复杂性障碍。直到最近,还没有成熟的反应式数据库库。您必须始终保持被动反应,以获取足够的价值来抵消成本。
Spring Boot以及大多数其他Java和Kotlin框架已经完全采用了反应式,并将其作为其“现代化”故事的核心部分。最近,甚至数据库层也通过R2DBC库获得了出色的响应式支持。但是,并不是所有的ORM /数据映射库都支持此功能。同样,大多数HTTP客户端库都支持反应式编程,但并非全部。
如果您使用Kotlin,则可以利用协程进行并发/与Spring Boot或Micronaut交互。结构化的并发模型使响应式操作几乎与典型的命令式编程一样容易。只要确保您正在使用的库在后台也没有阻塞即可。
反应式在Scala中已经很久了,Scala世界中的一些人创建了反应式宣言。与大型编程社区中的所有内容一样,您可以通过多种方式来进行响应。对于HTTP客户端,请支持ZIO的 sttp。对于数据库客户端,我真的很喜欢Quill,它支持各种非阻塞驱动程序。
 
反应式事件驱动/流式
除了典型的面向请求的体系结构(Web应用程序和REST服务)之外,还有各种体系结构模式被归类为“事件驱动”或“流”,它们也已经变得Reactvie。
Akka的参与者模型是一种用于进行响应式/事件驱动式消息传递的框架。行为者的核心好处之一是监督,当事情出错时,监督可以很容易地恢复。Akka具有内置的Java和Scala支持以及一种处理网络集群参与者的方法。在Akka actor的上方,有一个称为“ Akka Streams”的流处理框架,我将它与Scala一起用于生产中,用于在Kafka上进行实时事件处理。
还有许多其他流处理框架和库,包括ZIO流,Flink,ksqlDB和Spark的微批处理。对于许多此类方法,Kafka已成为一种典型的消息总线,因为它对水平缩放,消息重放,每个主题的持久性设置以及最多一次/最少一次/有效一次的传递提供了强大的支持。
在传统体系结构中,通过更新数据库来处理事件,然后丢弃原始事件,从而使数据库成为事实的来源。在微服务架构和分布式系统中,这种方法充满了一些问题,例如最终的数据一致性,无法扩展以及添加新的数据处理客户端困难。为了克服这些问题,出现了新的体系结构模式,包括命令查询责任隔离(CQRS),事件源(ES)和无冲突复制数据类型(CRDT)。
CQRS将真值源从可变数据存储区切换到不可变事件流。这为可伸缩性,分发和附加新客户端/微服务提供了更好的方法。通常,您仍然需要一个“物化视图”来表示通过处理所有事件计算出的状态。使用CQRS的好处是,您始终可以通过重播所有事件来重新计算实例化视图,但是在大型数据集中可能要花费很长时间。快照是处理此问题的好方法,并且由于Kafka能够从给定的时间点重放,因此从快照重建实例化视图很简单。
CQRS的最大缺点是编程模型相当低级。幸运的是,ksqlDB和Cloudstate使事情变得更加轻松。我曾经和Kotlin一起使用过,并且有很好的经验。有关此方法的更多详细信息,请查看我关于Cloudstate的博客:带有Cloudstate和Akka Serverless的Google Cloud上的无状态服务器。
 
容器(不是J2EE品种)
在J2EE / Java EE时代,我们将应用程序服务器称为“容器”,因为它们运行打包为“ Web Application aRchives”或WAR文件的Web应用程序。今天,我们通常仍在容器中运行我们的应用程序,但它们现在是多语言的,支持许多不同的运行时。运行容器镜像的流行环境包括Kubernetes,Docker和Cloud Run。有多种方法可以在Java生态系统中创建容器镜像,包括Dockerfiles,Jib和Buildpacks。有关这些方法之间的差异的更多信息,请查看我的博客:比较容器化方法:Buildpacks,Jib和Dockerfile。
创建容器时,请选择提供操作系统和JVM的“基础镜像”。有许多不同的选项,其中大多数是OpenJDK的变体,OpenJDK是Java平台标准版的开源参考实现。如果不确定使用哪个JVM基本镜像,则可以尝试使用精简的Distroless Java镜像:
  • 构建:gcr.io/distroless/java:8-debug或gcr.io/distroless/java:11-debug
  • 产品:gcr.io/distroless/java:8或gcr.io/distroless/java:11

OpenJDK的AdoptOpenJDK构建也是一个不错的选择,但它们可以在DockerHub上使用。
一些JVM在容器中运行时可以自动选择设置。例如,HotSpot JVM根据CPU数量和RAM数量来更改其使用的垃圾收集器。为了减少垃圾收集暂停,OpenJ9 JVM会检测CPU何时处于空闲状态,然后运行垃圾收集器。过去几年发布的OpenJDK版本会根据容器分配的RAM自动确定内存设置,并具有分配给容器进程的CPU资源的一致视图。
Java,Kotlin和Scala都在容器中运行良好,并且某些框架支持现成的容器化:Spring Boot容器化,Micronaut容器化(Gradle | Maven)和Quarkus容器化。否则,您可以轻松地使用构建工具插件来创建您的容器镜像。
 
无服务器并避免JVM开销
JVM能够在运行的更多(或更长时间)内优化执行,这非常适合您购买服务器的典型数据中心使用情况,因此即使它们有时利用率较低,也可以使其保持运行状态。在云中,我们不必那样做。相反,我们可以使用仅在使用资源时分配资源的模型。这就是所谓的“无服务器”,它的缺点是服务器通常不会长时间处于运行状态,这在某种程度上消除了JVM的某些价值。由于无服务器是基于需求的,因此当有请求进入时,如果没有足够的后备资源来处理该请求,则需要启动一个新实例。此请求是“冷启动”,它们可能是对您的P99延迟的真正拖累。
JVM的冷启动会使用户等待不舒服的时间。想象一下,您单击购物车上的“结帐”按钮,并且在JVM启动,处理运行时DI批注,启动服务器,缓存缓存等期间,15秒钟似乎什么都没有发生。解决此问题的一种方法是不将JVM用于基于JVM的应用程序。什么??似乎是不可能的,但这正是GraalVM Native Image能够实现的。它可以提前将基于JVM的应用程序编译为本地可执行文件,该应用程序在极短的时间内启动并使用较少的内存,但可能看不到“热”的性能达到基于JVM的应用程序的水平。
GraalVM本机镜像很神奇,但是当然有一些警告。提前编译意味着Java中的其他一些魔术会变得棘手。Java中的许多库都使用运行时自省和修改(称为反射)来动态处理诸如依赖注入和序列化之类的事情。这些动态的神奇潜伏龙无法提前编译,因此要使用GraalVM Native Image,您必须将所有动态的东西告诉它。这可能有些棘手-可能但很棘手。主要的框架正在努力使这一过程变得容易且有些自动化,但是我经常遇到问题。
某些Scala东西确实在这里大放异彩,因为函数式程序员通常不喜欢动态的魔术龙,因为它们本质上并不是纯粹的函数。ZIO和http4s都是一个不错的选择。我在生产中有一台GraalVM Native Imageified http4s服务器,该服务器在100毫秒内启动,具有16MB的容器镜像。因为我建立在一些纯粹的功能基础上,所以使用GraalVM Native Image进行提前编译非常容易。
通常,对GraalVM本机镜像的支持正在改善,并且在未来几年中,我确信大多数现代Java,Kotlin和Scala程序都将在没有JVM的情况下运行。提前编译确实需要一些时间,所以这只是我在CI / CD管道中所做的。我仍然使用JVM进行本地开发,以保持我的开发和测试迭代超级快。
 
恐惧,不确定性,怀疑和治理
OpenJDK是一个常规的开源项目,具有多供应商/分布式电源管理结构。Java社区流程和JDK增强建议为任何人提供了贡献的方法。Kotlin和Scala都由拥有典型开源治理模型的基金会所拥有。因此,在大多数方面,核心Java平台技术都可以像其他自由开放式编程平台一样工作。但是,有一些部分是专有的。要使用Java品牌(Oracle拥有)标记自定义JDK,它必须通过技术兼容性套件中的测试,该套件必须为此目的而从Oracle获得许可。Java语言API也可能具有版权。
在任何编程平台上,都有被锁定的风险,从而导致意外的成本。幸运的是,已有一些减轻这些风险的方法,例如将Kotlin或Scala与AdoptOpenJDK一起使用。
 
未来
Java生态系统在许多方面都在不断创新。在语言方面,Java,Kotlin和Scala都朝着不同的方向发展,但其效果却有所共享。例如,Scala的模式匹配可能是所有编程语言中最好的之一。这在某种程度上有助于激发Kotlin和Java中更好的模式匹配。JVM在垃圾回收和性能方面也进行了大量创新。当Project Loom(JVM上的光纤和延续)成熟时,反应式编程将变得更加容易。GraalVM是一项了不起的工程,它正在激励Java社区减少动态魔术龙的使用(这是一件了不起的事情)。Netty已经开始致力于io_uring支持(完全异步的Linux syscall)。通过CloudDT之类的项目,通过CRDT和CQRS分发的数据开始蓬勃发展。还有更多!Java生态系统中发生了许多令人兴奋的事情!
因此,您想使用Java平台进行现代化,但是在如此广阔的生态系统中,您如何选择?我很乐意提供建议,但是应用程序种类繁多,辅助因素也很多,因此很难提供一刀切的指南。相反,这里有一些问题要问:
  • 您想深入到函数式编程中吗?
  • 您的团队成员已经熟悉哪些技术?
  • 您需要无服务器运行吗?
  • 与后端的同一团队一起开发的Web,移动或其他UI共享代码吗?
  • 这是什么类型的工作负载?数据处理?微服务?网络应用程序?其他?

 
黑客新闻讨论
企业应用程序很复杂,因为它们不专注于解决业务问题,而是专注于工具和框架。
Spring不能治愈,这是疾病。
 
企业应用程序超级复杂,通常比启动应用程序复杂得多。
基本上,某人拥有1厚厚的业务知识加上1厚厚的法律限制,你应该把所有这些都编成软件。一些业务逻辑会使您哭泣。
软件开发人员:“但这并不干净/优雅”。
老板:“现实不在乎优雅/干净,它在乎我们制造客户想要的东西,不会让我们受到起诉,因此我们必须将其付诸实践。”
 
很抱歉,我在编写企业应用程序方面拥有丰富的经验,对此我必须予以驳斥。我见过的几乎所有复杂性都源于基础架构,而不是所述企业固有的复杂性。
我甚至可以说企业逻辑是如此简单明了,以至于大多数程序员都花了很多时间来发明问题,即如何看待企业市场。他们有很多“内部平台”,元问题解决方案和不必要的复杂部署环境。
与此形成对比的是我目前从事游戏的行业。这里的问题是真实的,切实的和艰巨的。突然的过度复杂化问题不大了,因为当您的核心问题已经很难解决时,额外的认知负担就会变得过多。
 
你在许多不同行业中编写企业应用程序都有丰富的经验?对我来说,听起来您正在尝试笼统反驳事实经验。您是否曾经在医疗保健或保险等严格管制的行业工作?业务逻辑与法规紧密相关,法规因州/国家/地区而异,可能非常繁琐。
 
企业应用程序开发人员(等等)并不愚蠢。领域和业务问题通常非常复杂且难以理解。
 
企业开发人员并不愚蠢,但是企业软件是我职业生涯中遇到的最糟糕的软件。当开发团队拥有一个专属客户时,您将获得预期的结果。
 
我不喜欢常规的Spring,但是它提供了一些合理的价值,并且具有相对可理解的行为。
 
当您意识到自己一遍又一遍地编写相同的东西时,您最终会为自己编写另一个自定义框架。
 
我曾经读过一条评论,说Spring非常类似于COMEFROM语句:https://zh.wikipedia.org/wiki/COMEFROM
 
在20年代初,Spring在Java EE 1 .4或其他方面取得了很大的进步,但现代Java EE至少在某种程度上赶上了,JEE 8和9与Microprofile一起使用相对比较愉快,而Spring更陷于00年代的感觉。可以说,与Spring相比,当今的JavaEE在许多方面都充满了新鲜空气。Spring甚至需要​​一个Web应用程序来帮助配置其过多的行为。
 
今天的JVM上有很多很棒的东西,但是Spring Boot概括了J2EE的所有问题。一切都极其“分离”,以至于您不知道任何东西来自何处或为什么,仅向类路径添加新的依赖项将从根本上改变应用程序的行为,考虑到Spring / Boot在运行时产生的无尽麻烦,比特币挖掘可能是最符合道德的事情。
 
现代Java平台的未来是Graal,Java不是Spring Boot。这是Graal之上符合Python 3.8的运行时-https: //www.graalvm.org/reference-manual/python/,更令人印象深刻的是graaljs实施,它在预热后的性能可与该死的v8相媲美。所有这些都具有完整的多语言运行时,因此您可以从js调用python代码,反之亦然,而最好的部分是:它将内联和JIT在语言边界上进行编译。而且,truffleruby(graalvm的ruby实现)比常规的ruby快两倍,这是JVM投入了多少工程。当然,性能良好(但比JIT差)的AOT编译也是有可能的,尽管我认为人们并不需要那么频繁地使用它。
 
Quarkus(及其相关技术)已经存在了一段时间,最近我开始使用它,我必须说给我留下了深刻的印象。
使用现代Java代码(Lamba等)->构建本机Linux exe->打包为Docker映像->在Google Cloud Run中部署。通过CLI进行的所有连线都使CI / CD友好(接下来是使用Google Cloud Build)。由于它是本机的,因此内存使用量很小,启动时间可以忽略不计。由于受到管理,它会自动缩放(上下缩放,以实现零成本)。
我的抱怨:
* Quarkus非常自以为是。我已经习惯了来自Google App Engine的此功能。
*建立_native_ exe速度很慢。像1995年的Java慢。
* Scala支持有限。
如果您习惯了App Engine,那么您将完全了解我所生活的梦想。而不受App Engine的限制。
 
人们对Spring持批评态度,因为他们已经在生产和实际项目中看到了它。陷入天真的陷阱,即某种程度上的工具和库是现实生活中的软件项目混乱的原因。
但是,Spring经受了时间的考验,已在数千个实际项目中使用。Spring本身是从实际的软件开发中脱颖而出的,它具有许多竞争性的替代方案(Guice,经典的J2EE以及许多其他替代方案)。文章中提到的替代方案:Micronaut和Quarkus并没有完全脱离演示状态。
 
Java是一门伟大的语言。它保持了良好的发展速度,提高了开发人员的生产率并适应了现代模式,同时在成熟的生态系统中保持了清晰的语法,可伸缩的VM,相对快速的编译器。它可能是有史以来最健康的水平。
 
Project Loom(结构化并发)可以改变游戏规则。
 
GraalVM确实有潜力成为通用的,可互操作的VM。例如,Julia员工迁移到graalVM生态系统并对其进行支持,而不是生活在他们的小代码岛上,这将是合理的。他们将在此过程中获得无法想象的收益。
 
我对Java的最大抱怨是开发人员的生产力。它已经变得更好了,但仍然远远落后于Python和Ruby等解释型语言。类热加载和类似的方法使情况变得更好,但是,如果更改方法签名或接口,则必须停止过程,重新编译,重新部署和重新启动。与前面提到的语言及其附带的框架相比,对于大型代码库而言,这是残酷的。
 

相关:
替代Spring Boot几种微服务框架比较