Java平臺之2021年現狀 - James Ward

發表於2021-03-18

早在2000年代初期,許多開發人員就被Java過於複雜的世界所嚇壞。四種模式和中介軟體/ J2EE / Java EE的組合導致所謂的脫鉤的荒謬程度,從我在2002年研究的開源J2EE電子商務系統的此序列圖中可以明顯看出:Java平臺之2021年現狀 - James Ward

早在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幾種微服務框架比較

 

 

 

相關文章