1. 程式人生 > >為什麽我這個 Java 死忠倒向了 Node.js?

為什麽我這個 Java 死忠倒向了 Node.js?

競賽 幫助 那種 工具鏈 大型 javafx 覆蓋 類型轉換 Kubernete

作為一個在Sun微系統公司Java SE團隊工作了十多年的人,難道不應該是體內流淌著Java字節碼的血、只要一息尚存就要不斷實現抽象接口嗎?但對於我這個前Java SE團隊成員來說,2011年學習了Node.js平臺後就像是呼吸到了新鮮空氣一樣——我在2009年1月被Sun裁退之後(正好在Oracle收購之前),開始學習Node.js並被它深深所吸引。

我是怎樣被吸引的?從2010年起,我就開始寫各種關於Node.js編程的東西了。具體來說,寫了四版《Node.js Web開發》,加上一些其他的書,和數不清的關於Node.js編程的教程。可以說,我花了非常多的時間解釋Node.js和JavaScript語言的發展。

在Sun微系統工作時,我相信一切都能用Java描述。我在JavaONE上演講,與別人共同開發了java.awt.Robot類,舉辦了Mustang Regressions Contest(為Java 1.6發布準備的找bug競賽),幫助別人啟動了“Java的分布式授權”,就是在OpenJDK出現之前為Linux發行版發布JDK的解決方案,後來還在啟動OpenJDK項目上扮演了一個小角色。

一路走來,我在java.net上有過一個博客(現在荒廢了),連續六年堅持每周寫一到兩篇文章,討論Java生態系統中發生的一切。最常見的話題就是反駁那些唱衰Java的論調。

技術分享圖片

Duke獎,頒發給努力超越自己的員工。我在舉辦了Mustang Regressions Contest這個為Java 1.6發布而準備的找bug競賽之後獲得了這個獎勵。

那麽,說好的靠著Java字節碼生存和呼吸呢?我這篇文章的目的就是想解釋下一個Java字節碼的忠實粉絲是如何變成了Node.js/JavaScript的傳道者。

其實並不是說我和Java完全不相幹了。

過去三年裏我也寫了許多Java/Spring/Hibernate代碼。雖然我很喜歡我的工作——我在Solar Industry工作,做一些實現夢想的事情,如寫數據庫查詢語句查詢用電量等,但用Java編程已經是昨日黃花了。

兩年的Spring編程讓我清楚地意識到一件事:掩蓋復雜性並不會讓其變簡單,只會欲蓋彌彰。

本文要點:

Java包含了大量樣板代碼,擾亂了程序員的意圖。Spring和Spring Boot的教訓:掩蓋復雜性只會讓事情更復雜性。Java EE是個“由委員會設計”的項目,覆蓋了企業應用開發所需的一切,導致過度復雜。Spring的編程體驗非常好,但是一旦在子系統深處出現模糊難懂、從未見過的異常信息,就需要花掉三天以上才能找出問題是什麽。如果框架允許程序員完全不寫代碼,那產生的額外開銷會有多少?雖然像Eclipse之類的IDE很強大,但都是Java復雜度的癥狀。Node.js是一個人磨礪並精煉輕量級事件驅動架構的結果,直到Node.js自己揭露了真相。JavaScript社區似乎很感謝去掉樣板代碼,可以讓程序員專註做有意義的事。回調陷阱的解決方案async/await函數就是移除樣板代碼的例子。用Node.js寫程序很愉快。JavaScript缺少Java那種嚴格類型檢查,但這是個雙刃劍。編程變得容易了許多,但需要更多測試才能保證正確。npm/yarn包管理器非常優秀,也非常好用,相對的就是令人生厭的Maven。Java和Node.js都提供優秀的性能,這與“JavaScript很慢因此Node.js的性能必然不好”的傳說正相反。Node.js的性能要歸功於Google為了加速Chrome瀏覽器而在V8上的投入。瀏覽器之間的激烈競爭使得JavaScript變得越來越強大,反過來幫助了Node.js。


Java已成為負擔,用Node.js編程很愉快

一些工具或對象是設計師多年精心磨礪並提煉的結果。他們會嘗試不同的想法,去掉不需要的特性,最後得到為某個目的量身打造的對象。因此這些對象都有強大的簡單性,所以非常吸引人。而Java並不是這種系統。

Spring是個流行的Java Web應用程序開發框架。Spring,特別是Spring Boot,其核心目標是個預配置的、易用的Java EE棧。Spring程序員不需要考慮所有servlets、數據持久、應用服務器,以及構成系統的其他不知所雲的東西。Spring會處理這一切,而你只需要專註寫代碼即可。例如,JPA Repository類會將數據庫查詢合成為方法,名字類似於“findUserByFirstName”,這樣你無需寫任何代碼,只需要調用方法,Spring就會處理剩余的一切。

——這個想法很不錯,而且的確很好用,直到某種情況發生。

如果你得到一個Hibernate PersistentObjectException,提示“ detached entity passed to persist”,也就是說到達REST訪問點的JSON有ID值,而這層含義往往需要幾天時間才能理解。這就是過度簡化的代價。Hibernate也在過度簡化,它希望控制ID值,於是拋出了這個不知所雲的異常。在Spring的棧中,子系統一個接一個,它就像復仇女神一樣耐心地等待你犯哪怕最微小的錯誤,然後用應用程序崩潰的異常來懲罰你。

緊接著你就會看到巨大的棧跟蹤信息。它們有好幾個屏幕那麽長,充滿了一個又一個抽象方法。Spring顯然做了許多工作來實現代碼的功能。這種級別的抽象顯然需要大量的邏輯,來找出所有信息並執行請求。長長的棧跟蹤不一定是壞事,它指出了一個癥狀:這需要多少內存和性能上的額外開銷?

既然程序員不需要寫任何代碼,那麽調用“findUserByFirstName”時,它是怎麽執行的?框架需要解析方法名、猜測程序員的意圖、構建類似於抽象語法樹的東西、生成SQL等等。這些事情的額外開銷有多大?一切都只為了讓程序員不需要寫代碼?

在經歷了多次折磨,浪費了許多天的時間學習本來不需要學習的東西後,你也許會產生與我同樣的迷惑:掩蓋復雜性不會產生簡單性,只會讓系統更復雜。

而使用Node.js的關鍵就是這一點。

技術分享圖片

“兼容性很重要”是句很好的口號,它的意思是Java平臺的主要價值體現在它完全後向兼容。我們很看中這一點,連T恤衫上都印了這句口號。當然,維持這種程度的兼容性非常痛苦,有時候還可以用來避免那些已經沒用的老方法。

Node.js正好相反

Spring和Java EE過度復雜,而Node.js完全是新鮮空氣。

首先是Ryan Dahl在開發Node.js平臺核心時采用的設計美學。Dahl的經驗是,線程會導致重量級的復雜系統。他想找一些不同的東西,花了很長時間打磨並提煉了一系列核心思想放到了Node.js裏。結果就是一個輕量級、單線程的系統,巧妙地利用了JavaScript的匿名函數進行異步回調,還有一個巧妙的運行時庫用來實現異步。最初的基調是通過回調函數的事件發布來實現高吞吐量的事件處理。

然後就是JavaScript語言本身了。JavaScript程序員似乎很喜歡移除樣板代碼,使得程序員可以專註於有用的事情。

另一個用來對比Java和JavaScript的例子就是事件處理函數的實現。在Java中,事件處理函數需要創建一個實際的抽象接口類。這就需要許多冗長的代碼,使得代碼本身的意圖含混不清。程序員的意圖埋在那一大堆樣板代碼後面,誰能看得清呢?

而在JavaScript中,你只需要簡單地使用匿名函數,也就是閉包。不需要搜索正確的抽象接口,只需要寫下必須的代碼,沒有任何冗余。於是有了另一個教訓:大多數編程語言只會掩蓋程序員的意圖,使得代碼更難理解。

這就是使用Node.js的最大好處。不過我們還得解決一個問題:回調陷阱。

有時解決方案就蘊藏在問題裏

JavaScript的異步編程一直有兩個問題。一個就是Node.js中所謂的“回調陷阱”。很容易就陷入嵌套回調函數的陷阱中,每層嵌套都會讓代碼更復雜,使得錯誤處理和結果處理更困難。一個相關的問題就是JavaScript語言不會幫助程序員恰當地表達異步執行。

一些庫使用Promise來簡化異步執行。這就是另一個掩蓋復雜度使之更復雜的例子。

舉例來說:

const async = require(‘async’);
const fs = require(‘fs’);
const cat = function(filez, fini) {
async.eachSeries(filez, function(filenm, next) {
fs.readFile(filenm, ‘utf8’, function(err, data) {
if (err) return next(err);
process.stdout.write(data, ‘utf8’, function(err) {
if (err) next(err);
else next();
});
});
},
function(err) {
if (err) fini(err);
else fini();
});
};
cat(process.argv.slice(2), function(err) {
if (err) console.error(err.stack);
});
這段代碼實現了Unix的cat命令。async庫用來簡化異步執行序列很不錯,但它用了許多樣板代碼,混淆了程序員的真實意圖。

我們實際想寫的是個循環。但不能寫成循環,而且也不是自然的循環結構。進一步,錯誤處理和結果處理不在最自然的地方,而是違反常規地寫到了回調函數內。在Node.js支持ES2015/2016之前,這是人們能做到的最好方法。

在Node.js 10.x中的等價寫法是:

const fs = require(‘fs’).promises;
async function cat(filenmz) {
for (var filenm of filenmz) {
let data = await fs.readFile(filenm, ‘utf8’);
await new Promise((resolve, reject) => {
process.stdout.write(data, ‘utf8’, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}
cat(process.argv.slice(2)).catch(err => {
console.error(err.stack);
});
這段代碼用async/await函數重寫了前面的例子。還是同樣的異步結構,但使用了正常的循環結構來書寫。錯誤和結果處理的位置也很自然,代碼更易於理解,更容易編寫,而且也可以很容易地理解程序員的意圖。

唯一的遺憾就是process.stdout.write不提供Promise接口,因此不能幹凈地使用async函數表達,只能再包裹一層Promise。

回調陷阱並不是用掩蓋復雜性的方式解決的。相反,語言和範式的改變解決了回調陷阱的問題,同時還解決了過多樣板代碼的問題。有了async函數,代碼就更漂亮了。

盡管最初這是Node.js的缺點,但優美的解決方案將缺點變成了Node.js和JavaScript的優點。


自定義良好的類型和接口

我之所以是Java的死忠,原因之一就是嚴格的類型檢查使得Java可以用於編寫巨型應用。當時的風向是編寫宏系統(沒有什麽微服務、Docker之類的),由於Java有嚴格的類型檢查,Java編譯器可以幫你避免許多類型的bug,因為不好的代碼無法通過編譯。

相反,JavaScript的類型很松散。理論也很明顯:程序員無法確定他們收到的對象的類型,那他們怎麽知道該做什麽呢?

Java的強類型的缺點就是太多樣板代碼。程序員要不斷進行類型轉換,否則就得努力保證一切都分毫不差。程序員要花掉很多時間寫極其精確的代碼,使用更多的樣板代碼,以圖早期發現錯誤並改正。

這個問題十分嚴重,因此人們必須使用大型、復雜的IDE。簡單的編輯器是不夠的。讓Java程序員保持正常的唯一方式就是提供下拉菜單供他選擇對象中可用的字段,描述方法的參數,幫助他創建類,協助他做重構,以及其他一切Eclipse、NetBeans和IntelliJ能提供的功能。

還有……別逼我說Maven。那個工具太垃圾了。

在JavaScript中,許多類型不需要定義,通常也不需要用類型轉換。因此代碼更清晰易讀,但存在漏掉編碼錯誤的風險。

在這一點上Java是好是壞取決於你的觀點。我十年前認為,用這些額外開銷獲得更多確定性是值得的。但我現在認為,我靠怎麽要寫這麽多代碼,還是JavaScript簡單。

用容易測試的小模塊來對抗bug

Node.js鼓勵程序員將程序分割成小單元,即模塊。看似是一件小事,但卻部分地解決了剛才提到的問題。

模塊是:

自我包含:顧名思義,模塊把相關的代碼包裝成一個單位;
強邊界:模塊內部的代碼不會被其他地方的代碼侵入;
顯式導出:默認情況下模塊中的代碼和數據不會被導出,只有選中的函數和代碼才能被別人使用;
顯式導入:模塊需要定義它依賴哪些模塊;
潛在的獨立性:很容易將模塊公開發布到npm代碼倉庫中,或者發布到其他私有倉庫中供其他應用使用;
易於理解:需要閱讀的代碼量小,因此更容易理解代碼的意圖;
易於測試:如果實現正確,那麽小模塊可以很容易進行單元測試。
所有這些特性一起,使得Node.js模塊更容易測試,並且有定義良好的範圍。

人們對JavaScripot的恐懼一般集中在它缺乏嚴格的類型檢查,因此代碼很容易出錯。對於小型、目的明確並且有著清晰邊界的模塊來說,受影響的範圍通常會限制在模塊內部。因此大多數情況下需要考慮的範圍很小,而且都安全地保護在模塊的邊界內部。

解決弱類型問題的另一個方案就是增加測試。

通過書寫簡單的JavaScript代碼而節省下的時間,必須花一部分在增加測試上。測試用例必須捕獲那些本應被編譯器捕獲的錯誤。你肯定會測試代碼的,對吧?

如果想在JavaScript中享受靜態類型檢查,可以試試TypeScript。我沒用過TypeScript,但聽說它很不錯。它增加了包括類型檢查在內的許多有用的功能,並且可以直接編譯成兼容的JavaScript。

因此在這一點上,Node.js和JavaScript完勝。

包管理

Maven想想就覺得可怕,完全不知道該寫什麽。我覺得,肯定有人非常喜歡Maven,也肯定有人很討厭Maven,兩者之間沒有中間地帶。

Java生態環境的問題之一就是它沒有統一的包管理系統。Maven包還算可以,而且理論上應該能在Gradle中使用。但不論是用途、易用性還是功能上,Maven與Node.js的包管理系統相比簡直是天壤之別。

在Node.js的世界裏有兩個非常優秀的包管理系統,他們能合作得很好。最初只有npm和npm的代碼倉庫。

npm用一種非常好的格式描述包的依賴關系。依賴可以是嚴格的(精確的版本1.2.3),或者可以逐漸增加較松散的條件,直到使用“*”表示任何最新版本。Node.js社區已經向npm代碼倉庫發布了幾十萬個包。在npm代碼倉庫之外使用這些包也同樣容易。

最好的地方是npm代碼庫不僅供Node.js使用,也可以讓前端工程師使用。以前他們使用類似於Bower之類的包管理工具。現在,Bower已經過時,所有的前端JavaScript庫都以npm包的形式存在。許多前端工具鏈如Vue.js CLI和Webpack都是用Node.js編寫的。

Node.js的另一個包管理器yarn從npm代碼倉庫下載包,而且使用與npm相同的配置文件。yarn與npm相比的主要優勢就是運行得更快。

不論是用npm還是yarn,npm代碼倉庫都是使得Node.js如此易用和愉快的重要因素。

技術分享圖片

在創建了java.awt.Robot之後,我想出了這張圖。官方的Duke吉祥物完全由曲線組成,而RoboDuke則都是直線,除了肘關節處的齒輪之外。

性能

Java和JavaScript都被批評過太慢。

兩者都由編譯器將源代碼轉換成字節碼,再由虛擬機執行。VM通常會將字節碼再次編譯成原生代碼,並使用各種優化技術。

Java和JavaScript在性能方面都有巨大的需求。Java和Node.js需要快速的服務器端代碼,在瀏覽器中的JavaScript則需要更好的客戶端應用性能。

Sun/Oracle JDK使用HotSpot這個超級虛擬機,它采用了多字節編譯策略。它的名字表示,它會檢測經常執行的代碼,一段代碼執行次數越多,就會應用越多的優化。因此HotSopt可以產生非常快的代碼。

而對於JavaScript,我們曾一度迷惑:誰能期待瀏覽器中運行的JavaScript能實現任何復雜應用程序呢?辦公文檔套件肯定沒辦法用JavaScript在瀏覽器中實現吧?但今日,這一切都實現了。本文就是用Google Docs寫的,它的性能還不錯,瀏覽器上運行的JavaScript性能每年都有大幅度增長。

這個增長趨勢使得Node.js越來越好,因為它用的就是Chrome的V8引擎。

機器學習領域涉及到大量數學計算,因此數據科學家通常使用R或Python。包括機器學習在內的幾個領域都需要快速數值計算。許多原因導致JavaScript很不擅長數值計算,但人們已經在努力開發一個標準庫,使得JavaScript也可以進行數值計算。

JavaScript還可以使用Tensorflow中的一個新的庫:TensorFlow.js。它的API類似於Python的TensorFlow,可以導入訓練好的模型,用來做比如分析動態視頻以識別訓練過的物體等工作,而且可以完全在瀏覽器中運行。

此前IBM的Chris Bailey在介紹Node.js的性能和擴展性問題時,就介紹了關於Docker/Kubernetes部署方面的問題。他從一系列性能評測開始談起,證明Node.js在I/O吞吐量、應用程序啟動時間和內存足跡方面的性能已遠遠超過了Spring Boot。而且,由於V8引擎的改進,Node.js的每次發布都會帶來巨大的性能提升。

Bailey還表示,人們不應該在Node.js運行計算類的代碼。理解其原因非常重要。因為Node.js是單線程模型,長時間運行的計算會阻塞事件的執行。在我的《Node.js Web開發》一書中,我談到了這個問題,並介紹了三種方法:

算法重構:找出算法中慢的部分並進行重構以獲得更快的速度;
將計算代碼用事件分發機制分成小塊,這樣Node.js可以經常返回到執行線程上;
將計算交給後臺服務器。
如果JavaScript的進步還達不到應用程序的要求,那麽還有兩種方法可以直接在Node.js中集成原生代碼。Node.js的工具鏈包括node-gyp,它能處理鏈接原生代碼模塊的工作。WebAssembly能將其他語言編譯成一個執行速度很快的JavaScript子集。WebAssembly是一種可執行代碼的便攜式格式,可以在JavaScript引擎中運行。

富互聯網應用(RIA)

十年前軟件行業談論的話題就是,用快速的JavaScript引擎運行富互聯網應用,從而使得桌面應用失去存在的必要。

實際上,這件事情20年前就開始了。Sun和Netscape達成了一項協議,在Netscape瀏覽器中運行Java Applets。當時JavaScript語言是作為編寫Java Applets的腳本語言的一部分出現的。當時的希望是在服務器端運行Java Servlets,在客戶端運行Java Applets,從而達到前後端使用同一種語言的目的。但由於許多原因,這個目標並沒有實現。

十年前,JavaScript開始變得足夠強大,可以用來實現復雜的應用程序了。因此出現了RIA這個詞,而且RIA據稱將在客戶端應用的平臺上幹掉Java。

今天我們可以看到,RIA的想法已經實現了。通過服務器端的Node.js,我們終於可以實現了當年的目標,但兩側的語言卻都是JavaScript。

舉一些例子:

Google Docs(這篇文章的協作工具),類似於傳統的辦公套件,但完全在瀏覽器中運行。
強大的框架,如React、Angular、Vue.js,它們HTML/CSS進行渲染,極大地簡化了基於瀏覽器的應用開發。
Electron是個Node.js的Chromium瀏覽器的混合物,它支持桌面應用的跨平臺開發。許多非常流行的應用程序都是用Electron開發的,如Visual Studio Code、Atom、GitKraken、Postman等,性能都非常好。
由於Electron/NW.js使用了瀏覽器引擎,React/Angular/Vue等框架就都能在桌面應用中使用了。例如這篇文章:https://blog.sourcerer.io/creating-a-markdown-editor-previewer-in-electron-and-vue-js-32a084e7b8fe。
Java在桌面應用平臺上失敗的原因並不是JavaScript的RIA,主要原因是Sun微系統對於客戶端技術的無視。Sun專註於要求快速服務器端性能的企業客戶。當時我就在Sun,對此親眼目睹。真正殺死Applets的是幾年前在Java插件和Java Web Start中的一個嚴重的安全漏洞。那個漏洞造成了全世界的恐慌,於是人們都不再使用Java applets和Webstart應用了。

其他Java桌面應用依然能夠開發,而且NetBeans和Eclipse IDE之間的競爭現在依然火熱。但這個領域的Java開發已經是一潭死水了,而且除了一些開發者工具之外,已經很少見到基於Java的應用程序了。

一個例外是JavaFX。

JavaFX是十年前Sun為對抗iPhone而提出的方案。它計劃支持在手機中的Java平臺上開發富界面的應用程序,從而將Flash和iOS應用程序驅逐出市場。結果沒有發生。JavaFX依然有人使用,但並不像它宣稱的那麽火熱。

而這個領域的一切狂熱都來自於React、Vue.js和類似框架的出現。

因此,在這一點上JavaScript和Node.js獲得了碾壓式的勝利。

技術分享圖片

Java戒指,是早期的一次Java ONE會議的東西。這些戒指上包含芯片,內部完全是用Java實現的。在JavaONE上的主要用途是解鎖大廳裏的電腦。
技術分享圖片

Java戒指的說明書。

結論

如今,開發服務器端代碼有許多選擇。我們不必再被局限在“P語言”(Perl,PHP,Python)和Java中,因為我們有Node.js、Ruby、Haskell、Go、Rust以及其他很多語言。現在的開發者能享受到許多快樂。

至於為什麽我這個Java死忠倒向了Node.js,顯然是因為我喜歡使用Node.js編程時的自由感。Java已經成為負擔,而使用Node.js卻沒有這種負擔。如果有人雇我寫Java當然我還會接受,因為我想賺錢。

每個應用程序都有真實的需求。只因為喜歡Node.js就一直使用Node.js的態度顯然不可取,選擇一種語言或框架必然有技術上的原因。例如,我之前的一些工作涉及了XBRL文檔。由於最好的XBRL庫是用Python實現的,因此要完成項目,就必須學習Python。

所以,要誠實評價真實需求,然後根據結果進行選擇。

喜歡小編輕輕點個關註吧!

為什麽我這個 Java 死忠倒向了 Node.js?