over 2 years ago

這本書是主管送給部門的同事,一人一本,沒想到在零星的公車時間上意外地很快翻完了,也許,這可能跟主管想激勵的方向不太一樣,但我想我更確定我先前做的決定了!

前言

在公司創立之初,你可能除了一個好點子外,根本一無所有,甚至連一個好點子都沒有:你所擁有的只是超級自信,相信自己有朝一日總會想出一個好點子。 p. 15

努力工作雖然很重要,但創意與夢想才是真正的驅動力。創造力讓我們變得與眾不同、激情四射,最終讓夢想成真。 p. 21

我相信,一家企業既可以成就事業、承擔社會責人,也可以使其工作內容充滿趣味。當這三個願景交相輝映,才不會讓人利欲薰心。 p. 22

1. 這能有多難 無業人員初到Google

組一支曲棍球隊的決心給我上了一課,那就是───機會是自己爭取來的。
在我的字典哩,「機會」是一種環境,它讓某種事情成為一種可能。我們可以等待機會,去發現它,然後在某個正確的時間為之奮鬥。但如果機會就是環境,我們又為什麼要被動地等待?與其等待然後頂著高風險貿然一搏,你不如先發制人,創造屬於自己的機會。 p. 34

我後來明白,其實這就是創業的核心精髓───成為一個讓環境為你而改變的人。 p. 35

相信自己,也就是那個天才的你,這意味著你在擁有靈感之前先要擁有足夠的自信。為了激發靈感,就得先要為這個靈感安排足夠的空間。 p. 38

2. 每天都是全新的 成為圖書封面設計以及離開Google

這裡(Google)好像缺了點什麼,那就是我在第一份工作中所投入的熱情... p. 44

書籍封面設計讓我明白,完成一項設計有無數種方法。 p. 45

史提芬讓我明白,一個設計沒有被採納並不是什麼問題,這其實是個機會,因為我的工作並不是做一個藝術家,以完成一件作品來取悅我自己。真正的挑戰是我完成的設計不僅要讓自己滿意,而且行銷和編輯部的人也要認為這個方案很完美,這才是真正的目標。 p. 47

我很快學會不要太在意那些沒有被選中的設計方案,我從不把提案被駁回放在心上,因為我的創作力是無限的,我希望想出更多的新點子。我覺得自己有成百上千種靈感,我能這樣工作一整天!這就是一種做事的態度。 p. 48

3. Twitter的誕生 Twitter的輕鳴

我們缺少一樣東西,而它對於成功創業卻是不可或缺的,這種東西的重要性遠大於音訊的品質,它就是感情投入。如果你對你正在做的事情並不熱愛,如果你自己都不是自家產品的忠實粉絲,那麼即便事情都已經做得很到位,最終你還是會以失敗告終。 p. 62

雖然當時我還不能清晰地勾畫出產品的完整模樣,但我卻有一個不可動搖的意念,一種強烈的感覺。之後,我知道只有對工作的完全投入才能使我有如此的動力:但礙於我當時沉溺於工作,竟沒發現到。 p. 75

就這樣,兩周的駭客馬拉松項目讓Twitter誕生了。 p. 76

這次是深入的,因一項發明而產生的興奮,任憑創意自由流淌。這讓我感到自己逤做的事情都是有意義的,而且很酷───這有點像墜入愛河。 p. 80

在那一刻,我意識到為什麼自己其他的創業經歷都失敗了,但Twitter會成功───因為Twitter帶給了我快樂。p. 82

投入熱情去工作不一定能保證獲得成功,但如果沒有投入全部的心身,那麼你一定會失敗。 p. 83

當你對這份工作漸漸熟悉之後,你會開始覺得有些不對勁,這就像有人破壞了你手機裡面的GPS一樣───你被鎖定在某個方向上,但你並不知道自己要去哪裡。當你感覺路線不對,而你的自動導航系統引導你誤入歧途時,你必須搞清楚哪裡才是自己的終點。讓自己置身事外,從一個更高的位置思考你的生活方向,思考你真正想要去哪裡。全面地看看整張地圖───路線、交通、終點站。你喜歡去那裡嗎?那個終點是你喜歡的嗎?是不是應該調整你的座標?或者,你現在正在一個完全錯誤的地圖上前行? p. 84

4. 限制與創造力 魔法數字140和推特體

擁抱人生的各種限制吧。它們是創造力,是自然規律,符合成本效益的原則,並且能夠自我強化。它們富有煽動性,也極具挑戰性。它們讓你覺醒,讓你更有創造力,讓你遇見更好的自己。 p. 89

5. 大家都在用Twitter 西南偏南互動大會上的奇蹟

成群結隊、結社,還有一種現象叫做「湧現」───就是許多同類動物組成了一個「超個體」,並且比單獨的個體更聰明、更能幹。這在自然界中很常見。 p. 101

雖然有很多小錯誤,它的系統也非常脆弱,但我們這一小群人正在創造一個直到它誕生之前世人都不認為是必要的東西。而且,我們創造了一種不同形式的溝通方法,一種其潛力才剛剛被發現的新方法。 p. 102

在這之前,我從沒想過「科技」或是「商業」等字眼,但就在人們使用我們的產品的那一刻,我突然意識到了。 p. 102

從這一刻起,我們的工作就是去聆聽使用者的需求,同時提供相應的服務去滿足他們。這是靈感,也是一種對市場的順應。 p. 107

6. 當幸福來敲門 遇到我的麗薇亞

可問題是,除非你願意接紹最差的情況,否則你是得不到最佳結果的。如果想觸及你的夢想,那麼完成你真正的使命就需要你用盡全力。由此可見,擁有承擔風險的意願正式邁向成功途徑。 p. 117

我的建議是,去擁抱那些不可思議的、史詩般的,甚至會改變命運的失敗。如果你成功了,那麼失敗絕對是值得的。即使你失敗了,你也會擁有一個值得傳頌的故事,可以讓你在下一次嘗試時有更大的優勢。 p. 118

7. 失敗都是珍貴的財富 失敗鯨粉絲團的支持

大家都不願對外宣傳自己的失敗,甚至隱瞞大眾,在某種程度上可以說是一種欺騙。這我瞭解到弱勢的價值───當所有人都覺得你和他們一樣是凡人而非聖賢,充滿熱情但並不完美,擬將獲得更好的聲譽。 p. 126

即便如此,我仍然堅持誠信的原則。我認為,向用戶解釋我們到底哪裡出問題,才是維持長久用戶關係的祕笈。 p. 129

我們一直以來的誠信顯示出我們的人性化的一面,反而讓我們帶來了友善的回饋。 p. 131

我們的失敗都會變成珍貴的財富。 p. 132

我們就是那群在螢幕背後、想盡一切辦法幫助小鳥移動巨大鯨魚的人。我們不需要表現出完美無缺,每個人都有缺點,如果你硬要裝作完美,缺點反而更欲蓋彌彰。 p. 133

8. 樹立企業精神 尋找積極的亮點

在那一刻,我意識到樹立企業精神是我的工作職責之一。 p. 137

我意識到,積極性雖然很難衡量,但實際上卻非常重要。我不僅對外樹立公司的品牌,對內我也在負責形成公司的企業文化。 p. 138

當事情一團糟的時候,與其不停地去尋找那些地方出狀況,還不如找到哪些部分還在正常運轉,然後以此為中心建立或修復。在那些看似無邊無際的負面中找出正面積極的「亮點」,解決之道或許就在其中。 p. 139

如果這本書限制在一百四十頁,那內容就停在這裡啦。 p. 140 (笑~)

9. 小事情,大變化 Twitter小鳥的巨大潛力

保持和政府的中立關係就是我們的立場。 p. 168

我們希望任何地方的任何人都能隨時與他們認為最有意義的人、事、資訊產生連結。這了這個目標,言論自由是最基本的。 p. 168

10. 臉書奇遇記 馬克.祖克伯的五億美元收購計畫

這次會面一開始就是個錯誤。如果你不是真的想要賣掉公司,那就不必參加收購會談,因為一旦別人給出了收購合約,你就必須認真權衡利弊並向股東彙報。 p. 178

11. 群體的智慧 #話題標籤,@某人。轉發

這些功能是大眾智慧的結晶,新功能的出現都是群體選擇的結果,這種自然命名法的演進過程也是我要向所有Twitter新員工傳達的精髓。 p. 186

我想,我是在遵循自己的理想建立Twitter這間公司,但事實上,一路走來,我打造了一個品牌。 p. 187

我總是回答:「用戶數量並不重要,重要的是使用者覺得我們的服務有用且有趣。」堅持自己的理念是有收穫的───在我們的服務真正完備之前,我們已經擁有了一個超強的品牌。 p. 188

12. 你可以駕馭事實 越發嚴謹的職業態度

我想即使我已經三十五歲了,但有時仍會誇大其詞,所以給別人留下了我喜歡出風頭的印象,或者說是有些換來吧。但是,我在事業上可是越來越嚴謹了。 p. 190

13. 為自己設定原則 不做回家作業以及強闖畢業舞會

我並不是不遵守規則,我只是喜歡從廣泛的角度思考問題。 p. 194

不過,我的確喜歡以不同的方式思考事情,選擇對我更為有利的方式。 p. 194

雖然我沒辦法當個模範生,但是我有意識地選擇了自己的道路。從某種意義上來說,當一個人認為老師或其他人能夠知道什麼對自己比較好,這種想法本身就錯了。 p. 194

那些握有權力的人不應該強迫人們去固守某些規則。解決問題的方法永遠都是:認真傾聽自己內心的需要以及周圍人的心聲。 p. 195

打破規則並不是世界末日,我們堅持自己的立場,挑戰權威,而且獲得了勝利。沒有傷害,就不算犯規。 ... (中略) ... 制定規則的人也會犯錯,而我有權利挑戰他們。只要心甘情願地承受後果,你就可以按照自己的規則行事。 p. 201

14. Twitter的理念 改變世界既簡單又非常有趣

它意味著我與自己深深認同的公司一起成長。正如我之前所說,我是一個對產品極度狂熱的人,但我同時也在意公司的使命感。 p. 202

但我的理念卻恰好相反。一個企業服務了多少、或是其軟體有多複雜並非全部(當然那些也很重要),但真正讓技術產生實際意義的是:對用戶和創造者來說,他們究竟會如何使用這些技術,從而有效地改變這個世界。 p. 204

就算會感到害怕,我們也應該不斷探索並學習新貞,勇往直前。 p. 207

Twitter的理念
 1. 我們不知道未來會發生什麼
 2. 人外有人,天外有天
 3. 如果堅持為使用者提供正確的服務,那麼我終究會贏得市場
 4. 雙贏才是唯一的好買賣
 5. 我們的同事都很聰明,而且心地善良
 6. 我們可以建立一種商業模式,既能改變世界,又超級有趣 p. 208

不要只因為這是你的產品,就將自己的意志和期望強加於產品之上,要觀察用戶的使用習慣和他們如何操作,從而尋找更合適的功能。 p. 209

每當我們做出要增加、改變或是取消某項功能的決定時,我們考慮最多、也是最簡單的一個問題就是:這麼做會不會讓用戶的感覺更好? p. 211

這問題其實很難回答,有時候認為可以,但卻不是如此。

如果你有份清單,上面是你不惜任何代價都要留住的員工,那就不要在他們準備離職時才給他們更多的薪水和股票期權。 p. 215

我想對這種層級的員工,有時候給予更大的自主空間會比薪水或股票期權更有吸引力。但我不是說這兩者就不重要了。

15. 幫助別人就是幫助自己 二十五美元禮物卡的複利效應

同理心是一種理解他人情感的能力。這種能力是與生俱來的,但並不是每個人都能輕鬆掌握,或是有建設性的使用它。 p. 224

16. 慈善之舉 社會價值在利潤之上

人性都是善良的,如果有確的工具,他們就會使用這些工具去做正確的事情。 p. 233

我很早就認識到這些社交工具都有一個相同的特徵,那就是自律性。 p. 233

我們的承諾是在利潤之前傳遞價值,只要有機會我就會和同事討論這個觀點。我們在一起所做的事可能會全世界產生積極的、潛在的、持續的影響,我們的工作內容會影響到很多人的生活,從簡單的社會性工作到栽難援助。 p. 243

17. 權力的遊戲 離開Twitter

企業的創辦人很少能成功轉型為一個大型企業的首席執行長。 p. 248

在那段混亂的日子哩,我曾反思為什麼會發生這樣的事情。後來我想明白了:哦,原來是幾百萬美元在作怪。 p. 253

這樣好像正在融化的冰塊,如果你想讓冰塊融化得更快,可以把大冰塊打碎,讓更多的冰面接觸到空氣以加快融化的速度,而不是把大冰塊原封不動地放著。如果你想積極地進改變的話,也是類似的道理。 p. 254

現在輪到我去投入其他事業了 p. 260

我開始重新定義我的人生和事業。我知道我想要從事何種事業、我的人生方向,以及我的遺產將如何分配。我決定把我的生命奉獻給社會,但這必須透過我所擅長的工作來實現。 p. 262

18. 群體的力量 創立Jelly

我們每天都在做選擇,而每個選擇都會產生不同的結果。我最感興趣的是,當人們面對各種選擇時將會如何應對。 p. 266

網際網路和手機把整個世界連結再一起,社群媒體更是增強了這種連結。經過近十年的發展。我們已經習慣了「加為好友」、「關注」、「讚」等網路交流的方式,也累積了規模空前的虛擬網路人脈。但是,如果沒有一個遠大的願景,這種連結又有什麼意義呢? p. 266

早在很久以前,我就知道自己是個能夠成就大事的人,但那時我還不確定自己到底是誰、我的信仰是什麼、我想要達成什麼目標。 p. 275

後記 一同前行

激情、風險、創新、同理心、失敗、樂觀、幽默、智慧───這些才是驅動我們不斷前行的動力,定義成功的關鍵,並且自我實現。 p. 280

我誠懇地希望大家能打開絲路,堅信自己能夠夢想成真,並用天馬行空般的願景去創未來。 p. 280

讀後後記
每個人成功的方式都不同,其實不需要去追隨誰成功的方式,因為那絕對不會再次在自己身上成功,但如果自己不試著去找適合自己的道路,那就跟很多人一樣:原地踏步。

 
over 2 years ago

Translated from "Using Docker in Java Applications - The lightweight virtualization container is fast becoming the preferred way to package and deploy Java web apps" by Arun Gupta, Java Magazine, November/December 2015, page 51. Copyright Oracle Corporation.

應用Docker在Java應用程式上

輕量的虛擬容器快速成為打包與布署Java網頁程式更好的方式

軟體容器讓開發者能夠用新的方式將他們的程式與相依的底層一起打包,且容器是可攜的,在他們自己的機器上、正式的機器、資料中心裡或是在雲端上都可以一致地運作,在可攜的容器之中,Docker最廣泛被使用。

在這有二篇文章的系列中,首篇文章解釋Docker的關鍵概念與它如何運作,其中,我展示如何用Toolbox開始使用Docker,並用一個簡單的“Hello World”程式來驗證,接著解釋Docker映像檔的概念及它如何被建立。透過打包Java應用程式成為一個Docker映像檔並讓它執行成為一個容器將幫助您了解整個基礎,一些用來檢閱映像檔與容器的基礎指令也同樣會介紹到。最後,我會示範如何用Docker布署一個Java EE (WildFly)應用程式。這系列的第二部分將會展示如何建立需要多個Docker容器(包含叢集)的應用程式,並用其他工具檢查Docker的整合。

Docker是什麼?

一個應用程式一般需要一個特定版本的作業系統、JDK、應用程式伺服器、資料庫伺服器與一些基礎架構的元件,為了提供最佳化的體驗,它可能須要綁定特定的通訊埠、一定數量的記憶體與針對不同元件有各自的配置設定。應用程式、基礎架構的元件與配置合起來是一個應用程式運作系統。[譯註:這裡有點刻意,若翻成應用程式作業系統擔心造成不必要的誤會]

一個用來建置應用程式運作系統的安裝腳本通常會執行下載、安裝與配置需要的組件,Docker用建立包含所有元件的映像檔來簡化這過程並當成一個單位來管理,然後,用映像檔來建立執行期的容器,在Docker提供的虛擬化平台上執行,這些容器可視為輕量化的虛擬機器(VMs),作業系統由虛擬化平台提供,以不用包含完整的作業系統複本來看,容器是輕量的。

Docker容器提供如隔離、沙箱、可重現性、資源限制與快照等其他許多優點,不需要虛擬機器監視器(hypervisor)就能執行,由於相當輕量,容器執行密度可以比標準的虛擬機器要高。[譯註:在一台實體機器上運行較虛擬機器更多數量的容器]

Docker有三個主要元件:

  • 映像檔:Docker的建構元件,包含一個唯讀的應用程式運作系統範本,例如一個映像檔可以是一個Fedora作業系統並安裝有WildFly及您的Java EE應用程式。您可以簡易地建立一個新的映像檔或是更新既有的映像檔。
  • 容器:一個由映像檔建立的執行期,容器是Docker的運行元件,可以被運轉、啟動、停止、搬移與刪除,每個容器都是被隔離與安全的應用程式平台。
  • 倉庫[譯註:這名詞參考《Docker —— 從入門到實踐》正體中文版的翻譯]:Docker的散布元件,映像檔可以上傳到這或從這下載,倉庫可是公開的,例如Docker’s own registry,或在您的防火牆內架設私有的倉庫。

Docker如何運作?

Docker使用client/server架構,其中Docker daemon是執行在host機器上的server,持續地維持Docker容器的執行,而Docker client可以安裝在您的機器上,透過傳送管理指令(例如取得一個映像檔或是執行一個容器)與Docker daemon溝通。Docker倉庫是儲存所有映像檔的地方,Figure 1顯示它的設計配置。

Figure 1. Docker的結構

在開發初期Docker host可能與Docker client在同台機器,但一般來說為了擴展性,它通常會移至獨立的機器。一般工作流程需要如下步驟:

  • Docker client請求以某個給定的映像檔執行容器
  • Docker daemon檢查host機器上是否有指定的映像檔,如果有,運轉成為容器,如果沒有,會從Docker倉庫下載映像檔後運轉成為容器。同個映像檔可以輕鬆運轉成多個容器。

Docker client提供建立與更新映像檔、下載或上傳映像檔到倉庫、運轉、查詢、觀測、刪除運轉中的容器等指令,與Docker daemon之間可以用socket或REST API溝通[譯註:傳送指令]。如果有必要,Docker daemon與Docker倉庫溝通以完成需要的操作。

身為開發者,為您的應用建立映像檔,用映像檔運轉成為容器,然後上傳映像檔到Docker倉庫讓其他人可以試用它。

開始使用Docker

Linux機器原生支援Docker,且很容易用預設的套件管理員安裝,如果您使用Windows或Mac機器,Docker Toolbox提供開始使用Docker的另類必要工具,它包含:

  • Docker client (即docker執行檔),它就和先前討論過的client元件一樣,透過與Docker daemon溝通來操作映像檔與容器。
  • Docker Machine (即docker-machine執行檔),讓您在自己電腦上、雲端提供者或自己資料中心裡的虛擬機器中建立Docker host,接著Docker daemon會安裝在虛擬機器中,然後client可以設定成與這個host溝通。

Docker Machine使用driver建立虛擬機器,driver是一個多重意義的術語,這裡指的是一個虛擬環境,例如:Oracle VM VirtualBox driver可以用在Mac或Windows系統上,AWS、Microsoft Azure和其他driver可以用在雲端上建立host。[譯註:這裡就不把driver翻譯成驅動程式,不然整段話讀起來就失去味道了]

  • Docker Compose (即dockercompose執行檔),通常,您的應用程式由多個容器組成,例如:WildFly、MySQL與Apache網站伺服器,Docker Compose能讓您用單一配置檔定義與運轉多個容器組成的應用程式。
  • Kitematic,一個功能強大的容器管理GUI,它提供在command-line介面與GUI畫面間一個無縫的體驗,而且還提供與Docker Hub的整合。
  • Docker Quickstart Terminal,一個終端介面的應用程式,建立預設的Docker Machine並配置一個Docker client與預設的Docker host (Toolbox安裝時建立)溝通。
  • Oracle VM VirtualBox 5.0.0,虛擬化技術的提供者,用來在本機上建立Docker host。

這些元件,每一個都可以單獨下載,Docker Toolbox將它們巧妙地打包成單一一個下載,且使用Docker Toolbox是開始使用Docker最簡單的方式。

要開始使用,請下載Docker Toolbox並安裝在您的機器上,執行Docker Quickstart Terminal建立預設的Docker host並配置Docker client與這個Docker host溝通,應該會產生如下的輸出:

Creating Machine default...
Creating VirtualBox VM...
Creating SSH key...
Starting VirtualBox VM...
Starting VM...
To see how to connect Docker to this machine,
run: docker-machine env default
Starting machine default...
Setting environment variables for machine default..

這輸出顯示Docker host在一個VirtualBox的虛擬機器中被建立、SSH金鑰被建立、虛擬機器被啟動,然後Docker client被配置成於這個Docker host溝通。client以環境變數如DOCKER_HOSTDOCKER_CERT_PATH配置,如前所示,這些配置是用docker-machine env default指令完成,這(Docker) machine名字是default

eval $(docker-machine env default)指令可以用來配置任何shell與此host溝通,最後,它會顯示如下的輸出:

docker is configured to use the default machine with IP 192.168.99.100

docker-machine ip default指令顯示指派給此host的IP位址,這是在/etc/hosts或在您作業系統中等效的檔案中映對名稱與IP位置的一個好方法,例如:可以加入以下這行:

192.168.99.100 dockerhost

然後執行ping dockerhost確認Docker host的映對是正確的。現在,Docker client已經準備完畢可以和Docker host溝通了。

Docker Toolbox是開始使用Docker最簡單的方式。

Docker Hello World

在我們開始使用Docker執行Hello World例子之前,先來看些基本的指令:

docker images條列在host可以使用的映像檔。

docker ps條列執行中的容器列表。目前,容器列表是空的,因為沒有容器被啟動。如果您想看到先前已結束的容器,需要加上-a選項。這指令的輸出最合適在128字元寬的畫面上觀看。

docker --help條列全部的指令,與此類似,docker ps --help條列這指令可接受的選項,隨意嘗試不同指令,試著調整選項以符合您的需求。

現在,開始執行可以在Docker Hub上找到預先建立的“Hello World”映像檔,只需用下列的指令:

docker run hello-world

這指令的輸出如下(有些在此文字之前或之後的冗詞被省略):

Hello from Docker.

這輸出確認

  • Docker client和daemon正確被安裝
  • hello-world映像檔雖不存在此Docker daemon上,但Docker可以從Docker Hub上下載
  • 從下載的映像檔啟動容器,並將輸出串流到Docker client上

如您所見,可以很輕鬆地執行您的第一個容器。

建立您第一個使用Java的Docker映像檔

Docker映像檔為唯讀的範本可以啟動Docker容器,每個映像檔由好幾層組合起來,Docker使用union檔案系統結合成單一的映像檔,Union檔案系統可以覆蓋在不同檔案系統的檔案和目錄上,形成單一一致的檔案系統。

這分層結構讓Docker相當輕量化,對映像檔的任何變更,例如:更新一個應用程式到新版本或是更換JDBC driver,只需重建受影響的那一層,因此,不像VM那樣,需換掉整個映像檔或整個重建,只需新增或更新某一層,這讓布署變得更快更簡單。

每個映像檔都從基礎的作業系統映像檔開始,例如fedora是一個基礎的Fedora映像檔,然後許多層加在它之上,例如,如Figure 2所示,jboss/wildfly由多層映像檔建構而成:

Figure 2. 建立一個Java EE的映像檔

一個Docker映像檔是透過從一個通常稱作Dockerfile的文字檔讀取指令來建構,這檔案包含建立映像檔所需的所有指令。例如,它指定基礎的作業系統、JDK、應用伺服器與其他相依套件。JDK與應用伺服器一般是用shell指令下載與安裝,像是GETCOPYRUNCOPY指令可以從local檔案系統複製檔案到容器中。或者,您可以在已經含JDK與
WildFly的基礎映像檔上建構新的映像檔,Docker Hub上有許多您需要的基礎映像檔。

每個Dockerfile可以有一個CMD指令,提供啟動容器的執行檔,若指定多個CMD指令,只有最後一個會生效。

網路上有完整的語法參考手冊,與最佳實務

一個只顯示JDK版本的簡單Dockerfile看起來如下:

FROM java:8
CMD ["java", "-version"]

將上述內容複製到一個檔案,並將檔案命名為Dockerfile,然後建立映像檔:

docker build java-version .

build指令建立一個名為java-version的映像檔, . 指示建構映像檔指令的檔案在目前的目錄中。

當映像檔建立完成,可以看到(下面輸出沒有顯示所有欄位):

> docker images
REPOSITORY  TAG    IMAGE ID     VIRTUAL SIZE
java-sample latest 53bd2cdf4aa2 425.4 MB

docker run java-sample執行容器可以看到如下輸出:

openjdk version "1.8.0_66-internal"

單獨使用docker ps將不會再輸出看到容器,因為這容器沒有正在執行,但是可以透過docker ps –a指令看到已經結束的容器。

如果想在這容器中執行一個JAR檔,可以用從local檔案系統COPY複製該檔案到容器中,或是用GET下載JAR檔,然後將JAR加到CMD指令列中,任何JVM的組態設定也用這方式套用。

一個Docker映像檔是透過從一個通常稱作Dockerfile的文字檔讀取指令來建構,這檔案包含建立映像檔所需的所有指令。

使用Docker布署一個Java EE應用程式

我們已經跑過一個非常基礎的例子,現在,我們開始布署一個Java EE應用程式到一個WildFly容器中,以下是Dockerfile的內容:

FROM jboss/wildfly
CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-c", "standalone-full.xml", "-b", "0.0.0.0"]
RUN curl -L https://github.com/javaee-samples/javaee7-hol/raw/master/solution/movieplex7-1.0-SNAPSHOT.war –o /opt/jboss/wildfly/standalone/deployments/movieplex7-1.0-SNAPSHOT.war

這檔案使用稱作jboss/wildfly的基礎映像檔,在其中WildFly已經預設安裝在/opt/jboss/wildfly目錄裡,這目錄用來啟動WildFly容器[譯註:這指的應該是Java EE容器],網路介面用-b與公開的IP位置綁定,從repository下載一個WAR到WildFly監控布署用的目錄。

用下列指令建立映像檔:

docker build –t javaee-sample .

現在,我們來執行這映像檔!一個Docker容器預設在前景執行且不允許透過terminal互動,-i選項允許透過stdin互動,-t選項則是替process繫上TTY (一個console介面),選項可以合併,-i-t可以合併成-it。用下列指令啟動容器:

docker run -it -p 8080:8080 javaee-sample

WildFly映像檔暴露8080通訊埠可以存取,但這個通訊埠需要映對到我們的host,透過-p選項映對,第一個“8080”是host端的通訊埠,而第二個“8080”則是容器內的通訊埠。

容器啟動後,布署到WildFly的Java EE應用程式可以在本機端用dockerhost:8080/movieplex7存取,要注意的是host到IP位置的映對在更早之前就介紹過了,Figure 3顯示URL的執行結果。

Figure 3. 在8080上的範例應用程式

當容器以互動模式啟動,它可以用Ctrl+C組合鍵停止,下面輸出可以確認容器已經停止。

docker ps –a
CONTAINER ID IMAGE         COMMAND                CREATED            STATUS                          PORTS NAMES
1efa5d6f618d jboss/wildfly "/opt/jboss/wildfly/b" About a minute ago Exited (130) About a minute ago       compassionate_mestorf

輸出的每一欄傳達關於容器相當有用的資訊:

  • 第一欄顯示指派給每個容器唯一的ID
  • 用來啟動容器的映像檔名稱
  • 啟動容器的指令
  • 容器建立的時間
  • 任何容器暴露的通訊埠 (在此例中,因為容器已經終止,所以此欄是空的)
  • 目前的狀態
  • 指派給容器的隨機名稱,除非您用--name選項給予一個名稱

在Linux/UNIX的系統上,容器的ID可以用下列指令取得:

docker ps | grep jboss/wildfly | awk '{ print $1 }'

然後,可以用docker stop 終止容器,接著用docker rm 移除容器,或是用docker rm -f 終止與移除容器。同樣,可以用docker start 重新啟動容器。

或是,您可以將-it選項改成-d選項,讓容器以單獨(背景)模式啟動。

docker inspect是另一個重要的指令,顯示更多關於容器的細節,例如,透過以下指令可以條列容器的網路通訊埠:

docker inspect --format '{{ .Config.ExposedPorts }}' <CONTAINER_ID>

更進一步,-P選項可以映對容器內的通訊埠到local host較高的通訊埠(一般來說從23768到61000),可以看到容器通訊埠與host通訊埠的映對:

docker port <CONTAINER_ID>
8080/tcp -> 0.0.0.0:32768

在這情況下,應用程式可以在dockerhost:32768/movieplex7上存取。

結論

本文解釋Docker的關鍵概念,以及如何用它打包您的Java應用程式,Docker開啟打包一次布署到任何地方(Package Once Deploy Anywhere, PODA)的典範,且改變應用程式如何建立、布署與拓展的方式,Docker減少開發環境、測試環境與正式環境之間不匹配的阻抗。

準備好了嗎?Docker已經在此,而且將會以輕量化的容器技術和我們在一起很久。此系列的下一篇文章,我將探討多重容器的應用程式與在叢集上執行容器。

LEARN MORE
Getting started with Docker
Overview of containers
Kubernetes: Docker orchestration tool

譯者的告白
過去沒特別計算複製原文(含調整格式)到部落格花的時間,這次嘗試用番茄時鐘法,將翻譯文章切成好幾個tasks,但複製只切成一個,最後這個task用了三顆番茄才完成,意思是超過一小時,這讓我有點意外。

 
almost 3 years ago

Translated from "Part 1: Building Apps Using WebSockets - The easy-to-use API for long-lived connections" by Danny Coward, Java Magazine, November/December 2015, page 58. Copyright Oracle Corporation.

首部曲:使用WebSockets建構應用程式

簡易使用的持續連線API

不等客戶端請求,就能推播資料給網頁客戶端,因此Java WebSocket異於Java EE其他網站元件。我在本文中探討WebSocket協定,WebSocket如何運作,以及在一個簡單的專案中如何使用它們,您僅需要對網站應用程式有非常基本的理解及它們如何在Java EE上運作就能跟上。

Java WebSocket從以HTTP為基礎的互動模型出發,提供一個方法讓Java EE應用程式能以非同步方式更新瀏覽器或非瀏覽器客戶端,長久以來,網站互動模型即是HTTP請求/回覆的互動模型,這模型豐富且考量到許多複雜的瀏覽器為基礎的應用程式。但是,每個互動都是由瀏覽器以使用者的某些動作發起,例如載入頁面、更新頁面、點擊一個按鈕,或看某個連結等等。

對許多網站應用程式而言,總是讓使用者主控一切不是令人滿意的。從需要即時市場資訊的金融應用,到全世界的人們對物品出價的拍賣應用,或普通的聊天與監控應用,網站應用程式一直在找尋伺服器端可以推播資料給客戶端的方法,這需求產生點對點機制的混用,但不論是保持HTTP持久連線或是客戶端輪詢,都無法對這問題提供一個完整的方案,一個想要新方法的需求引領了WebSocket協定的開發。

WebSocket協定簡介

WebSocket協定以TCP協定為基礎,在單一連線上提供全雙工的溝通管道,簡單來說,它使用與HTTP相同的底層網路協定,在單一個WebSocket連線上的雙方可以同時傳送訊息。WebSocket定義一個簡單的連線生命週期與資料表達的機制,支援二進制與文字為基礎的訊息。與HTTP不同,連線是持續的,這意味著因為不需要不斷地為每次訊息傳輸重新建立連線,所以WebSocket協定中資料訊息不用在夾帶關於連線的中介資訊(meta-information),換句話說,相較HTTP需要重建連線與夾帶中介資訊,當連線建立後,訊息傳輸比HTTP協定要輕量許多。

但是,相較於建構在HTTP之上的輪詢框架,這不是WebSocket更適合用在推播訊息的主要原因,有一個到客戶端的專屬連線,在本質上讓WebSockets成為伺服器更新客戶端更有效率的方式,因為只有當需要時,資料才會被送出。

要了解為什麼?想像一下,一個線上拍賣會有10個人在12小時中為物品出價,假設平均每個競標者都成功為該物品出價兩次,那物品的價格在拍賣過程中變動20次。現在假定競標者必須知道最新的出價資訊,因為您無法知道競標者何時會出價,或是目前最新的價格,因此支援拍賣的網站應用程式需要確保每個客戶端每分鐘能更新一次或甚至更多次,這意味每個客戶端需每小時問60次,總共60 × 10 × 12 = 7,200次更新,換句話說,需產生7,200則更新訊息。

但是,如果伺服器能夠透過WebSocket在資料實際有變動時推播資料給客戶端,只需要送20則訊息給每個客戶端,總共20 × 10 = 200則訊息。在整個應用程式的生命週期中,因為客戶端的數量增加,或是伺服器資料可能變動的時間,您很可能看到相關數字更加發散。WebSocket提供的伺服器推播模型在本質上是比輪詢機制更有效率。

WebSocket生命週期

在WebSocket協定中,客戶端與伺服器端扮演的角色幾乎是相同的,協定中唯一非對稱 [譯註:antisymmetry是數學裡的專有名詞,但我覺得這裡是用錯字了,應該是下面提到的非對稱asymmetric]的地方是連線建立的初始階段,它在意誰創建連線[譯註:交握中,只有發起者會帶Sec-WebSocket-Key,回覆者會用Sec-WebSocket-Key的值加上一個UUID後,以Base64編碼作為Sec-WebSocket-Accept回覆給發起者,所以才會這麼關心誰是發起者],這很像打電話,要能打電話,某人必須撥號,然後某人必須接聽,但一旦電話接通了,它不在意誰撥號的。

WebSocket在Java EE平台中,一個WebSocket客戶端永遠是瀏覽器或在筆電、智慧型手機或桌機上執行的rich client [譯註:這硬翻很詭異,簡單說就是可以單獨執行且有漂亮使用者介面的應用程式],然後WebSocket伺服器端是在Java EE應用伺服器中執行的Java EE網站應用程式。

現在來看WebSocket連線典型的生命週期,首先,客戶端發起連線請求,客戶端傳送一個特殊格式化的HTTP請求到網站伺服器,您不需要瞭解每個交握請求(handshake request)的細節,識別WebSocket交握請求與一般HTTP連線是透過Connection: UpgradeUpgrade: websocket標頭,以及最重要資訊的是請求的URI,/mychat,如下方所示的交握請求:

GET /mychat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: megachat, chat
Sec-WebSocket-Extensions : compress, mux
Sec—WebSocket-Version: 13
Origin: http://example.com

網站伺服器決定是否支援WebSockets (所有Java EE容器都會做),如果支援,在請求URI所指的位置是否有個端點滿足請求的需求,如果都沒問題,支援WebSocket的網站伺服器回覆一個特殊格式化的HTTP回應,稱作WebSocket起始的交握回應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sM1YUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: compress, mux

這回應證實伺服器將會接受接下來客戶端的TCP連線請求,以及加註連線如何被使用的限制,當客戶端處理完回應,且樂於接受限制,則TCP連線就建立了,如Figure 1所示,連線的兩端都可能繼續相互傳送訊息。

當連線建立完成,幾種事情可能發生:

  • 連線的任一端可能傳送訊息給另一端。在連線開啟的狀態下,任何時間點都可能發生,訊息在WebSocket協定下有兩種偏好:文字或二進制內容。
  • 可能在連線中產生錯誤,在這情況中,假設該錯誤不會導致連線中斷,連線的兩端會被知會,這種非中斷式的錯誤可能發生,例如,交談中的一方傳送一個壞掉的訊息。
  • 連線自發性關閉。這指連線的某方認為交談已經結束,所以關閉連線,在連線關閉前,連線另一方會被知會。

Figure 1. 建立一個WebSocket連線

Java WebSocket API概要

Java WebSocket API提供一組Java API類別與Java annotation [譯註:這次嘗試不翻這個單字,希望文章讀起來不會很破碎]讓在Java EE網站容器中建立WebSocket端點變簡單,整體的概念是在實作伺服器端邏輯的類別上加註類別層級的Java WebSocket API annotation @ServerEndpoint;接下來,在類別中的函式上加註生命週期相關的annotation,例如,@OnMessage,讓討論中的函式充滿特殊的能力:每當有WebSocket客戶端送訊息到這個端點時都會被呼叫;接下來,將他打包到WAR檔中的WEB-INF/classes目錄中,List 1提供一個的例子。

Listing 1. The EchoServer sample
import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class EchoServer {

    @OnMessage
    public String echo (String incomingMessage){
        return "I got this (" + incomingMessage + ")" + " so I am sending it back !";
    }
}

這個WebSocket端點被映對到/echo這個網站應用程式的URI空間,每當有一個WebSocket客戶端送一則訊息,它會立即將收到的訊息調整後送回。Java WebSocket API包含方法攔截所有WebSocket生命週期事件,且提供方法能以同步及非同步模式傳送訊息,它能讓您使用編碼器與解碼器類別在WebSocket訊息與任何Java類別之間轉譯。

Java WebSocket API同樣提供方法建立WebSocket客戶端端點,WebSocket協定唯一非對稱(asymmetric)的是關心誰建立連線,Java WebSocket API能讓客戶端連線到伺服器端,所以相當合適用來讓Java客戶端連到在Java EE 網站容器中執行的WebSocket端點,事實上,可連到任何WebSocket伺服器端點。

在我們看Java WebSocket真實的例子前,我們先看一遍Java WebSocket API的annotations和主要類別,別擔心等太久才能開始寫程式,Java WebSocket API是Java EE平台上較小的API之一。

Java WebSocket API提供一組Java API類別與Java annotation讓在Java EE網站容器中建立WebSocket端點變簡單。

WebSocket Annotation

Java WebSocket annotation有兩個主要的用途:第一,它們能讓您將任意Java類別變成WebSocket端點;第二,讓您加註該類別的函式以攔截WebSocket端點的生命週期事件。首先,我們先看類別層級的annotation。

@ServerEndpoint這是API中吃苦耐勞的annotation,假如您建立許多WebSocket端點,你常看到它。這類別層級的annotation唯一必填的屬性是value屬性(見Table 1),用來指定URI路徑,指向您希望這個端點在網站應用程式中註冊的URI空間。

屬性 功能 必填
value 定義端點所註冊的URI路徑

Table 1. @ServerEndpoint的屬性

@ClientEndpoint您可以加註@ClientEndpoint在您希望成為客戶端端點的類別上,用來建立連線到伺服器端點,它沒有必填屬性,通常用在連線到Java EE網站容器的rich client應用程式。

@ServerEndpoint@ClientEndpoint的非必須屬性列於Table 2,這些類別層級的annotation有幾個共用屬性,為所修飾的WebSocket端點定義其他組態選項。現在,我們將焦點轉到生命週期的annotation。

@ServerEndpoint
@ClientEndpoint 屬性
功能 必填
configurator 開發者能用來動態設定端點的類別名稱
decoders 條列將進來的Websocket訊息轉成對應Java類別的編碼器
encoders 條列將Java類別轉成作為Websocket送出訊息的編碼器
subprotocols 條列端點能支援的副協定,例如"chat"

Table 2. 類別層級annotation的屬性

@OnOpen這函式層級的annotation告知Java EE網站容器:當有人連到WebSocket端點時必須呼叫此函式,這個函式可以不帶參數;或是帶一個非必需的Session,其型別為javax.websocket.Session代表剛建立的WebSocket連線;或一個非必須的組態參數,型別為javax.websocket.EndpointConfig代表該端點的組態資訊;或一個非必需的WebSocket路徑參數,待會很快會提到。

@OnMessage這個函式層級的annotation告知Java EE網站容器:當有訊息透過該連線送達時必須呼叫此函式,這函式必須有某些類型的參數列,不過幸運的是,有部分是非必須的。參數列必須包含一個變數持有送進來的訊息,可以包含Session及路徑參數,訊息的變數類型有許多選項,包含最常使用的String作為文字訊息,及ByteBuffer作為二進制訊息。函式可以指定回傳的型別或是void,如果有回傳型別,Java EE網站容器會解讀成回傳值就是要立即回送給客戶端的訊息。

@OnError這個函式層級的annotation告知Java EE網站容器:當連線發生錯誤時必須呼叫此函式,這函式的參數列中必須有一個Throwable參數,也可以有非必須的Session參數與路徑參數。

@OnClose針對WebSocket生命週期中的最後一個事件,這個函式層級的annotation告知Java EE網站容器:當連到此端點的WebSocket連線將要終止時必須呼叫此函式,這函式的參數列可以有Session參數及路徑參數,如果需要的話,一個javax.websocket.CloseReason參數,代表連線將結束的原因說明。

Java WebSocket API類別

Java WebSocket開發者會用到最重要的API有SessionRemoteWebSocketContainer介面。

Session Session物件是一個實際連到此端點的WebSocket連線的抽象呈現,它在任何WebSocket生命週期處理函式中都是可存取的,它包含連線如何被建立的資訊,例如,另一方是用哪個URI建立連線,以及連線若保持閒置多久會逾時。它提供以程式關閉連線的方法。它持有一個映對讓應用程式可用來關聯連線與程式資料,例如,可能是端點從另一方收到訊息的完整副本。雖然和HttpSession物件不同,但可比擬成它呈現另一方與存取此Session物件的端點之間一連串的互動。此外,它提供存取此端點的RemoteEndpoint介面的方式。

RemoteEndpointSession物件可以取得RemoteEndpoint介面,用來表達該連線的另一端,實際上,當您想送訊息給連線的另一端時可呼叫此物件,RemoteEndpoint有二種子型別,第一個是RemoteEndpoint.Basic,提供所有以同步方式送WebSocket訊息的函式,另一個是RemoteEndpoint.Async,提供所有以非同步方式送WebSocket訊息的函式。許多應用程式只用同步方式送WebSocket訊息是因為許多應用程式只有小訊息要送,因此同步與非同步的差異不大。大多數應用程式只送簡單的文字與二進制訊息,所以要知道RemoteEndpoint.Basic介面有二個您常會用的函式:

public void sendText(String text) throws IOException;
public void sendBinary(ByteBuffer bb) throws IOException;

WebSocketContainer就像ServletContext與Java servlet的關係,WebSocketContainer與Java WebSocket的關係也是如此,它表達裝載WebSocket端點的Java EE網站容器,有許多關於WebSocket功能性的組態屬性,例如訊息緩衝區的大小及非同步傳送的逾時時間。

開始建造些東西吧:一個WebSocket時鐘

我們已經結束Java WebSocket API的導覽,因此知道足夠的資訊來看我們的第一個WebSocket應用程式。這個時鐘應用程式是一個簡單的網站應用程式,當您執行這應用程式,您會看到index.html如Figure 2所示的網頁。

Figure 2. 未啟動的WebSocket時鐘

當您按下Start按鈕,時鐘從目前時間開始,如Figure 3所示,時間每秒更新。

Figure 3. 已啟動的WebSocket時鐘

當您按下Stop按鈕,時鐘停止直到您再次啟動它,如Figure 4所示。

Figure 4. 已停止的WebSocket時鐘

這應用程式由一個簡單的網頁(index.html)與一個稱作ClockServer的Java WebSocket端點所組成,當Start被按下,index.html用JavaScript程式建立WebSocket連線到ClockServer端點,它每秒傳送時間更新訊息回給瀏覽器客戶端,JavaScript程式處理收到的訊息並顯示在網頁上。按下Stop讓在index.html網頁中的JavaScript程式送一個stop訊息給ClockServer,因此停止傳送時間更新,程式架構如Figure 5所示。

Figure 5. 程式架構

我們來看一下程式,首先是客戶端[編按:完整的程式可以從本期的下載區取得],Listing 2是WebSocket客戶端的程式片段。

Listing 2. WebSocket client code (JavaScript)
...
function start_clock() {
    var wsUri = "ws://localhost:8080/clock-app/clock";
    websocket = new webSocket(wsUri);
    websocket.onmessage = function (evt) {
        last_time = evt.data;
        writeToScreen("<span style='color: blue;'>" + last_time + "</span>");
    };
    websocket.onerror = function (evt) {
        writeToScreen ('<span style="color: red;"> ' + 'ERROR:</span> ' + evt.data);
        websocket.close();
    };
}

function stop_c1ock() {
    websocket.send("stop");
}

這網頁的HTML是相對簡單的,注意到JavaScript的WebSocket API使用完整的URI指向WebSocket端點,其中clock-appws://localhost:8080/clock-app/clock網站應用程式的context路徑[譯註:將context翻成上下文或是情境,恐怕都不合適,就想成整個環境吧]。

start_clock()函式完成建立WebSocket連線的所有工作,並以JavaScript風格加事件處理器,特別是處理收到來自伺服器的訊息。stop_clock()函式單純傳送stop字串給伺服器。

現在將焦點轉向ClockServer端點,如Listing 3所示。 [編按:同樣,完整的程式可以在本期的下載區取得]

Listing 3. The server endpoint
...imports...

@ServerEndpoint ("/clock")
public class ClockServer {

    Thread updateThread;
    boolean running = false;

    @OnOpen
    public void startClock(Session session) {
        final Session mySession = session;
        this.running = true;
        final SimpleDateFormat sdf = new SimpleDateFormat("h:mm:ss a");
        this.updateThread = new Thread() {

            public void run() {
                while (running) {
                    String dateString = sdf.format(new Date());
                    try {
                        mySession.getBasicRemote().sendText(dateString);
                        sleep(1000);
                    } catch (IOException | InterruptedException ie) {
                        running = false;
                    }
                }
            }
        };
        this.updateThread.start();
    }

    @OnMessage
    public String handleMessage(String incomingMessage) {
        if ("stop".equals(incomingMessage)) {
            this.stopClock();
            return "clock stopped";
        } else {
            return "unknown message: " + incomingMessage;
        }
    }

    @OnError
    public void clockError(Throwable t) {
        this.stopClock() ;
    }

    @OnClose
    public void stopClock() {
        this.running = false;
        this.updateThread = null;
    }
}

注意到ClockServer使用@ServerEndpoint宣稱自己是一個WebSocket端點,對應到所在網站應用程式context相對的URI /clock。由於@OnOpen,每當有新的客戶端連線,startClock()函式就會被呼叫,完成大部分的工作:建立一個執行緒,使用Session物件取得代表客戶端的RemoteEndpoint實體的reference,然後將現在的時間格式化後以文字傳送。如果端點收到一則訊息,它會傳遞給handleMessage()函式,因為該函式用@OnMessage加註,此函式的String參數告知您這端點選擇收到簡單的文字訊息(以最簡單的Java字串形式)。這函式回傳一個字串,Java EE容器將轉成WebSocket訊息,並立即送回給客戶端。

會有多少WebSocket實體?

即使在這簡單的例子中,一個疑問產生:像ClockServer這樣的WebSocket端點類別會有多少個實體產生?答案是每個客戶端連線時,就會有一個WebSocket端點類別的實體產生,每個客戶端有唯一的端點實體,更進一步,Java EE網站容器保證,不會有二個WebSockets同時送到同一個端點實體。所以,相對於Java servlet模型,您在撰寫WebSocket程式時,可以知道同時只會有一個執行緒呼叫倒它。

結論

WebSocket協定給予我們二種原生格式可以使用:文字與二進制,這對單純在客戶端與伺服器端簡單交換訊息的應用程式來說是足夠使用的,例如,我們的時鐘應用程式,透過WebSocket訊息互動機制交換的資料只有從伺服器廣播出去的時間資訊(文字格式)以及客戶端用來結束更新的stop字串。但很快地,應用程式有更複雜東西透過WebSocket連線傳送與接收,會發現需要一個結構存放這些資訊,作為Java開發者,我們習慣以物件的形式處理應用程式資料:不論是使用Java API標準的類別,或是我們自己建立的類別,這表示當您被Java WebSocket API低階的訊息功能困住,和想用物件寫程式而不是用字串或位元陣列時,您需要寫程式將字串或位元陣列轉換成您的物件,反之亦然,我將會在本文的第二期討論這個主題[譯註:所以標題才會加上首部曲]。

This article was adapted from the book Java EE 7: The Big Picture with kind permission from the publisher, Oracle Press. The book was reviewed on page 10 of the September/October issue.

LEARN MORE
Oracle’s Java WebSockets tutorial

譯者的告白
中文不太用子句,所以當遇到英文的子句時,翻譯就有點頭痛,偏偏這位作者非常愛用子句,一個句子中常常用超過二個that或which,讓我思考許久如何翻成「通順的中文句子」,如果有讀起來不通順的地方,不用客氣,歡迎指教。

 
almost 3 years ago

Translated from "How the JVM Locates, Loads, and Runs Libraries - Class loaders are the key to understanding how the JVM executes programs" by Oleg Šelajev, Java Magazine, November/December 2015, page 30. Copyright Oracle Corporation.

JVM如何取得、載入並執行函示庫

類別載入器是了解JVM如何執行程式的關鍵

類別不只是建構Java型別系統的積木,也提供另一個基礎的用途:類別是一個編譯的單位,可以被獨立載入並執行的最小程式片段。早在JDK 1.0,Java開始時,類別載入機制就已經確立,且大大影響Java的普及,使其成為跨平台的解決方案。編譯過的Java程式,以類別檔及包裝成JAR檔的形式,能在許多支援的作業系統上被載入到執行中的JVM程序中,正是這能力,讓開發者輕易散布編譯過的二進制函式庫,因為散布JAR檔比散布原始碼或是平台相依的二進制檔要容易許多,這能力使Java普及,特別是開放原始碼專案。

在本文中,我詳細解釋Java類別載入機制及它如何運作,我同時解釋類別是如何從classpath中被找到,及如何被載入到記憶體中並完成初始化。

載入類別到JVM的機制

想像您有一個簡單的Java程式如下:

public class A {
    public static void main(String[] args) {
        B b = new B();
        int i = b.inc(0);
        System.out.println(i);
    }
}

當您編譯並執行這片段程式碼,JVM正確判斷程式的進入點然後開始執行類別Amain函式,然而,JVM不需要立即載入所有匯入的類別或甚至直接參考到的類別,尤其是,這指只有當JVM遇到new B()述句的bytecode指令時,才開始尋找並載入類別B,除了呼叫類別的建構子之外,還有其他幾個方式會啟動載入類別的流程,例如,存取類別的靜態成員或是透過Reflection API存取該類別。

為了實際載入一個類別,JVM使用classloader物件,任何已經載入的類別,都擁有載入自己的類別載入器的參照(reference),而該類別載入器用來載入所有被該類別參考到的所有類別。在前個例子中,這意味著載入B這個動作大概可以轉譯成類似A.class.getClassLoader().loadClass("B")的Java述句。

這出現一個悖論:每個類別載入器本身都是java.lang.Classloader型別的物件,開發者可以以名字用它來尋找並載入類別。如果您對這蛋生雞雞生蛋的問題感到困惑,想知道載入所有JDK類別(例如java.lang.String)的第一個類別載入器是如何建立的?那您在對的方向上思考了。確實,最初的類別載入器,稱作bootstrap class loader [譯註:這應該不用硬翻成啟動類別載入器吧?],是用原生平台相依的程式所寫成,由JVM的核心提供,它載入JVM本身必要的類別,例如java.lang套件中的類別、與基礎型別相關的類別等等,應用程式的類別是用一般以Java程式寫成的類別載入器來載入,如果有需要,開發者是可以影響載入器的處理流程。

類別載入器階層

在JVM中,類別載入器以樹狀階層的方式組織,即每個類別載入器都有一個父節點 [譯註:我暫時想不到可以排除性別的名詞],在開始尋找與載入一個類別前,最好的方式是檢查父節點是否能載入或已經載入需要的類別,這可以避免重複的工作及不停地載入類別。有個規則,父節點所擁有的類別對子類別載入器是可見的(visible),但對其他人是不可見的。基於委託與類別可見度的結構能讓階層中的類別載入器責任分離,使類別載入器能專責載入特定路徑的類別。

來看一下在一個Java應用程式中的類別載入器階層及探討它們平常載入什麼類別,在階層中的跟節點是bootstrap class loader,它載入執行JVM本身所需的系統類別,您可以預期隨著JDK發布的類別都是由這個類別載入器載入(開發者可以使用JVM的-Xbootclasspath選項擴充bootstrap class loader載入類別集合)。

注意即使函式庫可能放在啟動的classpath中,它並不會自動被載入跟初始化,類別都是經請求載入到JVM中,所以即使類別對bootstrap class loader而言是可用的,程式需要存取它們才會觸發實際的載入(一個關於這載入流程奇怪的觀點是您可以覆寫JDK,如果您的JAR預先被附加到啟動的classpath,儘管這幾乎總是一個爛點子,它確實為潛在更具威力的工具開啟了一扇門)。

bootstrap class loader的一種子載入器是extension class loader,用來載入擴充目錄(等會解釋)中的類別,這些類別用來描述特定機器的組態,例如語系(locales)、安全防護提供者(security providers)等等。這擴充目錄的位置則是透過java.ext.dirs系統屬性指定,在我的機器上是被設成:

/Users/shelajev/Library/Java/Extensions:/Library/
Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/
Home/jre/lib/ext:/Library/Java/Extensions:/Network/
Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java

透過修改這屬性,您可以改變哪些額外的函式庫要被載入到JVM的行程(process)中。

接著是system class loader,負責載入應用程式的類別與classpath中可用的類別,使用者可以用-cp指定classpath。extension class loader和system class loader都屬URLClassloader型別且行為一樣:先委託父載入器,如果需要,才會自己接著尋找與解析類別。

網頁應用程式的類別載入器階層稍微複雜點,因為多個應用程式可以被部署到同個應用程式伺服器,他們的類別需能夠被彼此區別,所以,每個網頁應用程式使用自己專屬的類別載入器,負責載入自己的函式庫。這隔離確保被部署到單一伺服器的不同網頁應用程式能擁有同個函式庫的不同版本而不會出現衝突,因此應用程式伺服器自動提供每個網頁應用程式專屬的類別載入器負責載入該應用程式的函式庫。這安排之所以能運作是因為網頁應用程式類別載入器會先試著從應用程式的WAR檔中尋找類別,接著才將搜尋委託給父類別載入器。

在JVM中,類別載入器以樹狀階層的方式組織,即每個類別載入器都有一個父節點,在開始尋找與載入一個類別前,最好的方式是檢查父節點是否能載入或已經載入需要的類別。

找到對的類別

一般而言,如果JVM中有多個類別有相同的全名(fully qualified name) [譯註:類別名稱加上所屬的package名稱,例如:java.lang.String],衝突解決策略是簡單且直覺的:第一個合適的類別優先。大多數類別載入器都繼承至URLClassloader,它會依序走訪classpath路徑上的每個目錄,然後載入第一個符合類別名稱要求的類別。

相同名稱的JAR檔也是用相同策略,JAR檔會依出現在classpath的順序被掃描,而不是他們的名稱,如果第一個JAR檔包含一個指定類別的入口,這類別就會被載入,如果沒有,classpath的掃描會繼續到第二個JAR檔。自然地,如果在classpath的任何位置都找不到類別,會拋出ClassNotFound例外。

通常,仰賴classpath目錄順序是一個脆弱的方法,所以取而代之的是開發者可以將類別加到-Xbootclasspath中,確保它們優先被載入,這方法沒什麼特別不好,只是要維護一個仰賴維boot classpath調整的專案需要額外的工作,關於類別從哪裡被載入的直覺會被打破,每個人都會為此感到困惑。一個比較好的方式是從根源解決衝突,找出為什麼classpath中有多個類別擁有相同的名稱,也許升級某些相依性版本、清除快取、執行一個乾淨的建置就足以擺脫這些副本。

解析、鏈結與驗證

當一個類別被找到且在JVM程序中的初始記憶體配置建立完成,它會被檢驗(verified)、做好準備(prepared)、解析(resolved)並初始化(initialized)。

  • 檢驗(Verification)確保類別沒被破壞且結構是正確的:執行環境的常數池(runtime constant pool)是有效的、變數的型別是正確的,且變數在被存取前是初始化過的。透過-noverify選項可以關閉檢驗,如果JVM程序不是執行潛在的惡意程式,嚴格的檢驗也許不是必要的。關閉檢驗可以加速JVM的啟動,另一個好處是某些類別,特別是用工具在執行其中建立的類別,無法通過嚴格的檢驗程序,可以讓它們變成對JVM是有效地且安全的,為了使用這些工具,開發者應該關掉檢驗,一般來說這在開發環境中可以被接受的。
  • 準備(Preparation)對一個類別而言會將其靜態成員根據型別進行初始化(當準備完成,int型別的成員會為0,參照會設為null等等)
  • 解析(Resolution)對一個類別而言是指檢查執行環境常數池中所有參照是否都指向有效的指定型別,參照的解析是透過載入參考的類別觸發,根據JVM規格,這解析的過程可以延遲進行,因此它會被延到真正使用這類別的時候。
  • 初始化(Initialization)預期一個驗證過且準備好的類別,然後執行類別的初始化,初始化過程中靜態成員會被設為程式碼中指定的值,程式中靜態初始化的片段也會被執行,這初始化的程序對每個已載入的類別應該只能執行一次,所以是同步的(synchronized),特別是這初始化會觸發其他類別的初始化,因此應該小心處理避免死結(deadlocks)。

關於JVM如何載入、鏈結與初始化類別更詳細的說明請參考Chapter 5 of the JavaVirtual Machine Specification.

關於類別載入器的其他考量

類別載入器模型是Java平台動態運作的核心,它不只允許在執行期中動態尋找與鏈結類別,更能提供一個介面讓各式工具掛載到應用程式中。此外,許多安全性功能仰賴類別載入器階層進行許可的檢查,例如,只有當由一個bootstrap class loader載入的類別呼叫時,才能成功用著名的sun.misc.Unsafe.getUnsafe()函式取得Unsafe類別的實體,因為只有系統類別會由此載入器回傳,任何函式庫想使用Unsafe API,都必須依賴Reflection API從私有欄位中取得該參照。[譯註:sun.misc.Unsafe.getUnsafe()是私有函式,無法直接呼叫,只能透過Reflection API呼叫,但如果不是由bootstrap class loader載入的類別呼叫會拋出SecurityException]。

許多安全性功能仰賴類別載入器階層進行許可的檢查。

結論

當您在開發一個函式庫或框架,多數情況下,您不需擔心任何關於類別載入的議題,它是執行期間發生的一個動態程序,所以您很少會需要影響它,同樣地,調整類別載入機制很少會對一般的Java函式庫帶來好處。

但是,若您建立期望彼此獨立的系統模組或外掛,加強類別載入機制也許是個好主意,要記住,客製類別載入器從根本上影響所有類別,可能帶入難以觀察的臭蟲到您應用程式中的每個角落,所以要額外小心設計您的類別載入機能。

在本文中,我們了解JVM如何載入類別到執行環境中,Java使用的類別載入器階層模型,以及一般Java應用程式使用的階層模型。

從各方面來說,即使您不會每天與類別載入的問題奮鬥或是建立外掛架構,了解類別載入的機制幫助您了解您的應用程式發生什麼事,還提供許多Java工具如何運作的洞察力,且它真的展示保持classpath檢查與更新的好處。

Oleg Šelajev (@shelajev) is an engineer, author, speaker, lecturer, and developer advocate at ZeroTurnaround. He enjoys spending time tinkering with Clojure, Git, and MacVim and is pursuing a PhD in dynamic software updates and code evolution at the University of Tartu.

LEARN MORE
• Information on controlling class loaders
• Class loaders in the JVM Specification

譯者的告白
中文跟英文有個明顯的不同是中文的動詞與名詞大多相同,例如verify和verification都翻成檢驗,但動詞在英文中是無法擔任主詞的,所以句型上動作的名詞當作主詞時,硬翻成中文讀起來確實怪怪的。當然,有時候我刻意分開,文中refer我會翻成參考,reference我翻成參照,主要是reference在Java程式語言中有特殊的意義,我不希望跟動詞搞混了,但也許之後reference也維持原文不特別翻譯了。

 
almost 3 years ago

Translated from "Runtime Code Generation with Byte Buddy - Create agents, run tools before main() loads, and modify classes on the fly" by FABIAN LANGE, Java Magazine, November/December 2015, page 19. Copyright Oracle Corporation.

用Byte Buddy於執行期生成程式碼

建立代理人、在main()之前執行工具與程式執行中修改類別

能在JVM直譯器或JIT編譯器執行前修改程式的bytecode是Java平台常被遺忘的一項能力,當監控工具(profilers)或是物件關係對應(object-relational mapping)函式庫使用這能力時,應用程式開發者卻鮮少使用它。這突顯出未開發的潛能,因為於執行期生成程式碼讓橫切關注點(cross-cutting concerns)如記錄(log)與安全檢查(security)的實作變容易,或是改變第三方套件的行為(例如提供測試用的模擬物件),又或者是撰寫效能資料收集的代理人。

可惜的是,直到今日,在執行期間產生bytecode是困難的,現有三個生成bytecode的函式庫:

這些函式庫的設計是用來撰寫或修改Java程式中特定的bytecode指令,要使用它們,您需要了解bytecode是如何運作的,與Java原始碼相比這是相當難以瞭解的,此外,這些函式庫較Java原始碼難以使用與測試,因為Java編譯器無法檢驗,例如呼叫一個函式時參數順序是否合乎它的signature,又或者是否違反Java語言格規。最後,因為它們的歲數,這些函式庫不支援Java的新特性,像是annotation、泛型(generics)、預設函式及lambdas。

下述的例子說明您如何使用ASM函式庫實作一個函式呼叫另一個僅一個字串參數的靜態函式:

methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(
  Opcodes.INVOKESTATIC
  "com/instana/agent/Agent"
  "record"
  "(Ljava/lang/String;)V"
)

cglib與Javassist沒有差太多,它們都需要使用bytecode與以字串表達signatures,如您所見,它們看起來像組合語言而非Java。

Byte Buddy是採用不同作法解決此問題的函式庫,Byte Buddy的使命是讓不懂Java (bytecode)指令的開發者也能使用執行期間程式生成。此函式庫亦支援所有的Java特性,且不限於動態產生介面的實作(JDK內建的proxy utilities也是用這方式)。Byte Buddy的API將一般Java函式呼叫背後的bytecode運算元全部抽象化,不過,仍保留Byte Buddy用來實作的建築在ASM函式庫之上的後門。

注意: 本文中所有範例都是用Byte Buddy 0.6的API

Hello World, Byte Buddy

下面來自Byte Buddy文件的HelloWorld例子(Listing 1)呈現出在執行期間簡潔地建立一個新類別所需的一切。

Listing 1.
Class<? extends Object> clazz = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.named("toString"))
  .intercept(FixedValue.value("Hello World!"))
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();
assertThat(clazz.newInstance().toString(), is("Hello World!"));

Byte Buddy所有API都是可串聯的建構者風格(builder-style fluent) API並支援函數式編程。從告訴Byte Buddy您想繼承某個類別開始,就本例來說,您單純繼承Object,當然您可以繼承任何非final的類別,Byte Buddy保證回傳像Class<? extends SuperClass>這樣的泛型型別。現在,您擁有子類別的建構者(builder),您可以請Byte Buddy攔截(intercept)名為toString的函式,當函式被呼叫時,回傳一個固定值而非原先java.lang.Object所定義的值。

您可能會想知道這裡所謂的攔截(intercept),通常,當您繼承某個類別,您一般會用覆寫(override)的方式改變繼承來的函式,攔截是從AOP (aspect-oriented programming)來的術語,描述當一個函式被呼叫時要做什麼事的一種強而有力的概念。

當您完成宣告這子類別該怎麼運作,您呼叫make取得所謂Unloaded的類別表述,這表述的行為和.class檔是一樣的,事實上,它甚至能存成class檔。

最後,如Listing 1中所示,您可以用類別載入器載入這類別並取得載入類別的參照(reference),當以Byte Buddy開始,載入時使用的ClassLoadingStrategy通常沒這麼重要,但是,某些情況下,為了可見度因素或是想以特定順序載入,您需要特定的類別載入器來載入類別。

要注意的是Byte Buddy建立的類別與一般類別無異,不像其他函式庫或代理,沒有留下任何痕跡,產生的程式就像是Java編譯器為了實現此子類別所產生的一樣。

ElementMatchers and Implementations

當您使用Byte Buddy替類別新增或修改行為,最常做的事是找尋成員變數(fields)、建構子(constructors)或是函式(methods)。為簡化這些作業,Byte Buddy提供像是hasParameter()isAnnotatedWith()等大量的元素比對器(ElementMatchers)檢查函式的signature。它也有isEquals()isSetter()等別名用慣用的Java命名規則來比對函式名稱,使用這些預先定義的比對器,可以簡單扼地要描述要攔截的函式而不會很瑣碎。此外,可以客製化實作想要的元素比對器以涵蓋更複雜的使用情境。

另外,還有許多預先定義的替換用的Implementations可以在intercept()中使用,舉二個例子,MethodCall用參數呼叫別的函式,Forwarding用相同的參數呼叫另一個物件的相同函式。

更有力的攔截機制是MethodDelegation:當委託給一個函式,您可以先執行一段自訂的程式然後再轉給原先的實作,此外您仍可以用@Origin annotation動態取得原先呼叫者的資訊。如Listing 2所示,當委託給另一個函式,您仍可以動態取得原先呼叫者的資訊,待會說明。

Listing 2.
public static class Agent {
  public static String record(@Origin Method m) {
    System.out.println(m + " called");
  }
}

Class<?> clazz = new ByteBuddy()
  .subclass(Object.class)
  .method(ElementMatchers.isConstructor())
  .intercept(MethodDelegation.to(Agent.class).andThen(SuperMethodCall.INSTANCE))
// & make instance;

若有多個攔截標的,MethodDelegation自動查找最符合函式signature的代理,雖然查找是強而有力且可以客製化,我建議讓查找間單易於理解,在函式被執行後,感謝andThen(SuperMethodCall.INSTANCE),原先的呼叫繼續執行。

查找到的函式可以接受大量被標註(annotated)的參數,您可以用@Argument(position)@AllParameters取得原先函式的參數,用@Origin取得原先函式的資訊,參數的型別可以是java.lang.reflect.Methodjava.lang.Class甚至是java.lang.invoke.MethodHandle (若使用Java 7或之後的版本),這些參數提供關於函式從哪被呼叫的資訊,對於除錯相當有幫助;假設不同的函式被相同的攔截函式所攔截,也能提供不同程式路徑的資訊。

Byte Buddy提供@DefaultCall@SuperCall參數,用來呼叫原先的函式或上層類別的函式。

模擬 (Mocking)

有時您想為某個可能在執行期間發生的情境寫單元測試,但您無法為測試確實觸發那個情境,例如,如Listing 3所示,亂數產生器必須產生一個特定的結果才能讓您測試程式流程。

Listing 3.
public class Lottery {
  public boolean win() {
    return random.nextInt(100) == 0;
  }
}

Random mockRandom = new ByteBuddy()
  .subclass(Random.class)
  .method(named("nextInt"))
  .intercept(value(0))
  // & make instance;

Lottery lottery = new Lottery(mockRandom);
assertTrue(lottery.win());

Byte Buddy提供各式攔截器,所以很容易撰寫模擬物件(mocks)或探針(spies)。但是,為了更多的模擬物件,我建議改用專屬的模擬函式庫,事實上,極受歡迎的模擬函式庫Mockito目前已用Byte Buddy重新改寫。

到目前為止,我已經用subclass()建立一個實質上像打類固醇的子類別。Byte Buddy有另外二種操作模式:rebaseredefine,二者都可以改變指定類別的實作:rebase保有既有的程式,而redefine整個覆寫,但是,這些調整有個限制:只能更改已載入的類別,Byte Buddy須以Java agent的形式運作。

在單元測試或其他特殊情況,您可以確信Byte Buddy初次載入一個類別,在載入過程中修改其實作。針對這情況,Byte Buddy支援一個稱為TypeDescription的概念,表達Java類別處於未載入的狀態,您能為(尚未載入的)classpath設置一個類別池(pool),然後在載入類別前修改它們,例如,我可以用Listing 4的程式修改Listing 3的Lottery類別。

Listing 4.
TypePool pool = TypePool.Default.ofClassPath();
new ByteBuddy()
  .redefine(pool.describe("Lottery").resolve(), ClassFileLocator.ForClassLoader.ofClassPath())
  .method(ElementMatchers.named("win"))
  .intercept(FixedValue.value(true))
  // & make and load;

assertTrue(new Lottery().win());

注意:您不能在呼叫describe時用Lottery.class,因為這會在Byte Buddy修改前載入這類別,一旦Java類別被載入,已經不可能正常地卸載該類別。

使用Byte Buddy建立AOP代理人

在接下來的例子中,我建立一個監控效能與紀錄的代理人,它會攔截對JAX-WS端點的程式呼叫,然後印出這個程式呼叫花費多少時間。類似這樣的代理人,需要遵守在java.lang.instrument的Javadoc所描述的公約。使用命令列參數-javaagent來啟動它,且會在真正的main函式之前被執行(因此,稱為premain),通常,代理人會為自己安裝一個掛鉤(hook),使其能在一般程式載入類別前被觸發,這繞過無法修改已載入類別的限制。代理人是可以疊加的,且您能愛用多少就用多少。Listing 5展示一個代理人的程式。

Listing 5.
public class Agent {
  public static void premain(String args, Instrumentation inst) {
    new AgentBuilder.Default()
      .rebase(isAnnotatedWith(Path.class))
        .transform((b, td) ->
          b.method(
            isAnnotatedWith(GET.class)
            .or(isAnnotatedWith(POST.class)))
          .intercept(to(Agent.class)))
        .installOn(inst);
  }

  @RuntimeType
  public static Object profile(@Origin Method m, @SuperCall Callable<?> c) throws Exception {
    long start = System.nanoTime();
    try {
      return c.call();
    } finally {
      long end = System.nanoTime();
      System.out.println("Call to " + m + " took " + (end - start) +" ns");
    }
  }
}

取得預設的AgentBuilder後,我告訴它該rebase哪個類別。這例子只修改有javax.ws.rs.Path annotation的類別,接著,我告訴它如何轉換這些類別,例子中,代理人會攔截有GETPOST標註的函式,然後委託給profile函式。要讓這能運作,必須用installOn()將代理人掛載在Instrumentation中。

profile函式本身使用三個annotations:RuntimeType告知Byte Buddy將回傳型別Object調整成跟被攔截函式真正的回傳型別一樣;Origin取得真正被攔截函式的參照,好用來列印函式名稱;SuperCall執行原有的函式。和前個例子相較,我需要自己執行super call (原有的函式),因為我希望程式能在原有的函式呼叫前與呼叫後都被執行,如此才能量測時間。

比較Byte Buddy實作函式攔截的方法與Java預設的InvocationHandler,您會發現Byte Buddy的方法優化許多,因為攔截過程中會帶入必要的參數,但InvocationHandler卻需要滿足下列的介面:

Object invoke(Object proxy, Method method, Object[] args)

若參數與回傳值是基礎型別(primitive type),這優化帶來的好處更是顯而易見的,因為基礎型別都需要自動包成物件(autobox)才能使用,另外RuntimeType讓Byte Buddy盡可能減少包裝(boxing)的次數。雖然JVM的最佳化盡可能避免包裝,但對於像InvocationHandler這樣複雜的介面,卻不是總是適用的。

不借助-javaagent的方式使用代理人

使用代理人在程式執行期間產生或修改程式是相當有用的,但是,強制使用-javaagent參數有時候反而是不便的,借助原先設計用來在執行期間載入檢測(diagnostic)工具的Java Attach API,Byte Buddy提供一個便利的功能,在正在執行的JVM裝上(attach)代理人。您額外需要包含ByteBuddyAgent工具類別的byte-buddy-agent.jar檔案,您呼叫ByteBuddyAgent.installOnOpenJDK(),它所做的事和用-javaagent啟動JVM所做的事一模一樣。唯一不同的是,用這個方法您不是呼叫installOn(inst)而是改呼叫installOnByteBuddyAgent()。[譯註:參考Listing 5]

結論

除了既有的JDK動態代理與三個常見的第三方bytecode修改函式庫,Byte Buddy填補了一個很重要的缺口,它可串聯的API使用泛型,因此您不會遺失您正在修改的型別,若用其他方法很容易發生 [譯註:指遺失這件事]。Byte Buddy提供豐富的比對器(matchers)、轉換器(transformers)與實作(implementations) [譯註:要再查查更好的翻譯]集合,且允許藉由lambdas使用這些功能,讓程式相對簡潔易讀。

就結果來說,Byte Buddy對不熟悉讀bytecodes與低層運作的開發者來說是可以完全理解的,隨著接下來的0.7版,Byte Buddy將會支援泛型的全部基礎建設,如此,Byte Buddy將能在執行期間更輕易地與泛型及型別annotation互動,身為一個寫過許多bytecode操作(bytecode-handling)程式的開發者,我使用也推薦這個函式庫。 [編輯:Byte Buddy獲得2015 Java研討會的Duke’s Choice Award]

LEARN MORE
JVM Specification for Java 8
Byte Buddy on Stack Overflow

譯者的告白
術語或專有名詞恐怕是翻譯時最頭痛的事之一,例如前篇文章出現的annotation翻成註釋,後來想想還是改回英文,因為看到「註釋」真的可以聯想回annotation嗎?我不太肯定。這篇文章中的一些術語,像builder-style fluent APIs,其實也沒有慣用的中文翻譯,這讓我想起侯捷在《C++ Primer中文版》的導讀中對於術語翻譯還是不翻譯的抉擇,最後我決定採取的方式:除非有非常慣用的中文術語或造成讀起來不通順,不然還是保留英文術語吧!

 
almost 3 years ago

Translated from "JCommander: A Better Way to Parse Command Lines - An easy-to-use library that exploits annotations to parse the most-complex command lines" by CÉDRIC BEUST, Java Magazine, November/December 2015, page 13. Copyright Oracle Corporation.

JCommander:解析命令列有更好的方法

利用annotations解析複雜命令列的簡易函式庫

幾年前,我發現我須寫一個能在命令列中使用的應用程式,那是極具挑戰的專案,須解析複雜的命令列參數。因此,很自然地,我第一個直覺是尋找是否有函式庫能讓我描述應用程式所需的命令列語法又保持彈性。就在我找到一些符合目的的函式庫時,它們全因太過老舊讓我深受打擊,它們使用甚至比Java更早之前的概念與做法,此外,它們並沒有善用Java新功能的優點。

因此,我開始嘗試一些想法,然後我知道我要做的下一件事是完全拋棄我的初始想法,取而代之,我創建了JCommander:一個能輕易分析命令列參數的現代化開放原碼函式庫,並盡可能涵蓋各種風格的參數語法。不限於字串,參數不僅可以是數字或命令,也可以是lists、密碼或任何Java物件。讓我們開始瞧瞧吧!

快速總覽

設計JCommander時,我完成的第一個實現是當所有命令列上的選項都被解析後,結果最終會是一個Java物件。一般來說,結果是一個非常單純的物件,或稱作POJO (Plain Old Java Object),通常是一個沒有任何邏輯函式的容器,只有成員變數(fields)與對應的getters和setters。

我們開始迅速寫一個可以解析如下命令列的“hello world”程式:

tool --name Cedric --verbose

下列的類別可以捕捉解析後的資訊:

class Args {
    boolean verbose;
    String name;
}

為了做到這點,我們需要用告訴JCommander如何初始化此類別的物件。

class Args {
    @Parameter(names = "--verbose")
    boolean verbose;

    @Parameter(names = "--name")
    String name;
}

現在,我們只需以這類別物件建立並初始化JCommander,接著將命令列參數交由JCommander解析,當解析完成,所有解析的結果會被正確地指派給該物件中對應的成員變數:

public static void main(String[] argv) {
    Args args = new Args();
    new JCommander(args).parse(argv);
    System.out.println("Hello " + args.name + ", verbose is: " + args.verbose)
}

幾個原因讓使用annotation是非常恰當的方式:語法能相當清晰地標註在參數類別中,且閱讀原始碼時就能明白程式可以接受的參數,還有其他幾項優勢我很快就會解釋。

Annotation的威力

JCommander的方式中最顯著的方面是使用annotation,我是Java annotation的愛用者,但或許我有點偏袒它,因為我是設計它的委員會成員之一。甚至十年後的今日,我仍認為它開啟Java語言更具表達力的一種撰寫風格。

記住一個重點,annotation是一種十分合適用在替類別、成員變數或函式等Java元素添加額外意義的方式。任何無法明確與Java元素聯繫的資訊像package資訊、主機名稱或連接埠名稱都應外部描述,當有這一個簡單的規則,JCommander使用annotation很明顯是個正確的選擇。

此外,annotation可擁有多項屬性,讓您精煉添加到Java元素的中介資料(metadata),剛剛的範例程式中,我只使用一個屬性names,但還可以給予其他幾個屬性:

@Parameter(names = { "--output", "-o" }, required = true, description = "The output file")
String file;

由於我指定required屬性,所以當該參數被省略時,JCommander會拋出例外表示錯誤:

Exception in thread "main" com.beust.jcommander.ParameterException:
The following option is required:
--output

注意到names是複數型:您可以指定多個名字,這也表示以下二個命令列效果是相同的:

tool --out file
tool -o file

這項能力解決了使用者描述命令列選項不同風格偏好的常見問題。

JCommander的方法中最顯著的方面是使用annotation,它開啟Java語言更具表達力的一種撰寫風格。

使用說明書

我剛提到description屬性,因為JCommander會特別對待它:不論哪個參數有此屬性,它都會被自動收集並用來呈現完整的語法描述,如果您曾想在使用者打錯指令時顯示些訊息幫助他們,您只需要呼叫JCommander的usage()就可以看到下列資訊被顯示在畫面上:

Usage: <main class> [options]
  Options:
    --debug Debug mode (default: false)
  * --groups Group names to be run
    --log, -verbose Level of verbosity (default: 1)
    --long A long number (default: 0)

這描述幾乎包含所有JCommander能從選項的annotation中收集到的所有語法資訊:選項名稱、選項是否為必填(*符號表示為必填)、選項的預設值,當然還有他們的說明。這功能同時表達另一個寫程式的重要原則:不做重複的事 (don’t repeat yourself),如果您在參數類別中描述語法,您不應該為了顯示協助訊息再做一次相同的事,JCommander自動替您留意這件事。

型別

JCommander預設能處理許多型別,所有型別都被帶向元數(arity)的概念,元數定義一個參數需要幾個值,例如:

  • 一個布林(boolean)參數不需要任何值:當它呈現時即為true,若被忽略就是false
  • 一個純量參數(int, long, string, and so on)需要一個數值,就像--logLevel 3
  • 一個list參數需要多個值

JCommander能根據參數的型別自動推論元數的型別,此外,您若不滿意預設的元數,您可以自己定義,這允許使用其他形式的語法,例如,您可以定義一個元數為1的布林參數,因此可支援類似tool --verbose true的語法。JCommander也支援可變的元數,所以一個參數可以接受多個值,例如:--files file1 file2 file3

有時候,預設型別是不足夠的,您的應用程式需要更複雜的選項。例如,我先前用字串型別描述輸出檔案,如果JCommander能提供一個真正的java.io.File物件而不是一個字串,這不是不方便嗎?這是型別轉換器方便的地方。我們開始修改先前的例子,用真的Java File取代字串:

@Parameter(names = "-file", converter = FileConverter.class)
File file;

要注意多一個額外的converter屬性,我們需要實作它

public class FileConverter implements IStringConverter<File> {

    @Override
    public File convert(String value) {
        return new File(value);
    }
}

您可以指定任何轉換器,JCommander根據屬性自動使用對應的轉換器,就是這麼簡單。

JCommander可以支援複雜的程式與語法風格,許多功能協助您組織出簡潔的程式。

語法的彈性

為了盡可能支援許多種語法風格,JCommander允許您使用空白以外的分隔符號,例如可以透過指定separators屬性,用java Main -log=3java Main -log:3取代java Main -log 3

@Parameters(separators = "=")
public class SeparatorEqual {
    @Parameter(names = "-level")
    private Integer level = 2;
}

驗證

當您的命令漸漸變得複雜,判斷一個給定的命令列是否是有效會變得難以處理,因為多個選項會以各式各樣的方式與其他選項互相影響。JCommander可以提供一些協助讓驗證參數變簡單,語法相當接近剛剛提到的轉換器:

@Parameter(names = "-age", validateWith = PositiveInteger.class)
private Integer age;

下列程式一個驗證器的實作:

public class PositiveInteger implements IParameterValidator {
    public void validate(String name, String value) throws ParameterException {
        int n = Integer.parseInt(value);
        if (n < 0) {
            throw new ParameterException("Parameter " + name + " should be positive" + (found " + value +")");
        }
    }
}

複雜的命令

您可能很習慣一些工具使用子命令表達複雜的調用語法,例如,當子命令可以擁有自己的語法時,git提供如此類型的語法:當git add能接受-i參數,您也可以在呼叫git commit時,指定如--author–amend等參數,用JCommander能輕易實現這樣的語法。

首先,您定義您的命令類別,下面範例是commit命令:

@Parameters(separators = "=", commandDescription = "Record changes")
private class CommandCommit {

    @Parameter(description = "The list of files")
    private List<String> files;

    @Parameter(names = "--amend", description = "Amend")
    private Boolean amend = false;

    @Parameter(names = "--author")
    private String author;
}

接著是add命令:

@Parameters(commandDescription = "Add file to the index")
public class CommandAdd {

    @Parameter(description = "File patterns for the index")
    private List<String> patterns;

    @Parameter(names = "-i")
    private Boolean interactive = false;
}

然後,您將這些命令加入JCommaner,在下列程式碼中,我在結尾加了一些驗證來展示這些命令如何運作:

JCommander jc = new JCommander();
CommandAdd add = new CommandAdd();
jc.addCommand("add", add);
CommandCommit commit = new CommandCommit();
jc.addCommand("commit", commit);
jc.parse("-v", "commit", "--amend", "--author=cbeust", "A.java", "B.java");
Assert.assertTrue(cm.verbose);
Assert.assertEquals(jc.getParsedCommand(), "commit");
Assert.assertTrue(commit.amend);
Assert.assertEquals(commit.author, "cbeust");
Assert.assertEquals(commit.files, Arrays.asList("A.java", "B.java"));

如您從驗證中所見,程式正確解析命令列並將參數放到預期的變數中。

架構

JCommander可以支援複雜的程式與語法風格,許多功能協助您組織出簡潔的程式。多參數物件,隨著您的語法演進,您可能自己發現有一個巨大且有點難以維護的參數類別,JCommander能讓您將這個類別拆開成多個類別,因此您可以用直覺的方式組織這些選項:

CommandRead argRead = new CommandRead();
CommandWrite argWrite = new CommandWrite()
JCommander jc = new JCommander(argRead, argWrite);
jc.parse(argv);
// argRead and argWrite are now both initialized

參數代理人,您自己可能發現想要重複利用既有的參數類別,您可以使用參數代理人處理這情況,簡言之,參數代理人是一個可以指向其他類別的指標。在前面的例子,我決定建立二個不同的參數類別並直接宣告在JCommander中,但相反地,我可能想委託給它們,這可以用@ParameterDelegate annotation達成:

class MainParams {

    @Parameter(names = "-v")
    private boolean verbose;

    @ParametersDelegate
    private ArgRead argRead = new ArgRead();

    @ParametersDelegate
    private ArgWrite argWrite = new ArgWrite();
}

如此宣告,我需要宣告一個MainParams參數類別,它會包含並聚合ArgReadArgWrite兩者。

支援多種多語言

感謝JVM能支援多種語言的能力,其他JVM支援的語言也能使用JCommander,我正在Kotlin專案中使用它:

class Args {

    @Parameter(names = arrayOf("--buildFile"))
    var buildFile: String? = null

    @Parameter(names = arrayOf("--tasks"))
    var tasks: Boolean = false
}

fun main(argv: Array<String>) {
    val args = Args()
    JCommander(args).parse(*argv)
    println("Args: ${args}"")
}

這是另一個Groovy的例子:

import com.beust.jcommander.*
class Args {
  @Parameter(names = ["-f", "--file"], description = "File to load.")
  List<String> file
}
new Args().with {
  new JCommander(it, args)
  file.each {
    println "file: ${new File(it).name}"
  }
}

然後這是使用Scala的相同例子:

import java.io.File
import com.beust.jcommander.JCommander
import com.beust.jcommander.Parameter
import collection.JavaConversions._

object Main {
  object Args {
    @Parameter(names = Array("-f", "--file"), description = "File to load.")
    var file: java.util.List[String] = null
  }

  def main(args: Array[String]): Unit = {
    new JCommander(Args, args.toArray: _*)
    for (filename <- Args.file) {
      val f = new File(filename)
      printf("file: %s\n", f.getName)
    }
  }
}

結論

JCommander還有其他許多功能,包含:

  • 國際化,支援在地化的描述文字
  • 參數隱藏
  • 允許選項縮寫
  • 可選擇是否忽略大小寫
  • 預設值與預設值工廠
  • 動態參數 (能解析在編譯時還不知道的參數)

整體來說,JCommander是一個彈性的命令列解析函式庫,協助讓您的解析程式易於維護與演進。

LEARN MORE
JCommander on GitHub
JCommander discussion group
JCommander example file

譯者的告白
翻譯時常在想:翻譯時能帶入譯者的風格嗎?像是標題「JCommander: A Better Way to Parse Command Lines」,是一個很單純的名詞說明,可以翻成「JCommander:一個解析命令列更好的方法」,或是「JCommander:解析命令列有更好的方法」,我知道就精確性來說前者較佳,但我選擇後者,因為比較像中文。又例如,文章第二段結尾「Let’s have a look.」,我想了一下,決定用比較俏皮一點的翻譯「讓我們開始瞧瞧吧!」,讀者覺得呢?

 
almost 3 years ago

Translated from "Test JavaFX Apps with TestFX - Simple JUnit-style testing of JavaFX UIs" by Bennet Schulz, Java Magazine, September/October 2015, page 14. Copyright Oracle Corporation.

使用TestFX測試JavaFX應用程式

JUnit風格的JavaFX UIs測試

TestFX是測試JavaFX使用者介面的API,透過模擬使用者的互動,例如點擊按鈕、在文字框中輸入文字,和其他可以在JavaFX應用程式中執行的操作,來自動化測試JavaFX應用程式。這篇文章會先以TestFX簡短的背景開始,讓您理解TestFX的基礎與目標;接著,文章會說明如何從Maven專案開始使用TestFX,並對一個簡單的應用程式撰寫測試;我同樣會討論在使用TestFX前您必須先知道的限制。TestFX 4.0.x的版本目前是alpha階段,因此,這篇文章涵蓋3.1.2穩定版本。我假定您已使用過JavaFX並對其運作有相當程度的理解,包含對FXML很熟悉。

背景

TestFX建築在廣為人知的單元測試框架JUnit之上,就如同JUnit一樣,它相當容易學習與使用,使用TestFX,可以用與寫JUnit測試相似的方式撰寫測試。有幾個元件開發者需要添加,例如TestFX使用Hamcrest matchers在JUnit之上做測試驗證(test assertions)。與JUnit相比,使用Hamcrest matchers的好處是您可以寫出較貼近一般描述的驗證,且當驗證失敗時能取得較有用的錯誤息,提升測試程式的品質,同時也減低因需額外選項進行驗證所帶來的複雜度。

從使用Maven開始

將TestFX加到Maven專案中就如同將JUnit加入一樣簡單,在您的pom.xml中加入如下的片段:

<dependency>
    <groupId>org.loadui</groupId>
    <artifactId>testFx</artifactId>
    <version>3.1.2</version>
</dependency>

初始化一個簡單的測試案例

當您想寫TestFX測試,您的測試類別(test class)須繼承org.loadui.testfx.GuiTest,接著須覆寫getRootNode函式以提供待測畫面(view),如下列的程式片段,函式回傳一個FXML的畫面,但也可以回傳用程式建立(Swing風格)的待測畫面。

public class SampleTest extends GuiTest {

    @Override
    protected Parent getRootNode() {
        Parent parent = null;
        try {
            parent = FXMLLoader.load(getClass().getResource("sample.fxml"));
            return parent;
        } catch (IOException ex) {
            // ...

        }
        return parent;
    }
    // ...

}

透過繼承GuiTest,您同時獲得一些模擬使用者互動的UI測試函式,例如,用來找按鈕或文字輸入框等UI元件的函式,以及點擊按鈕、捲動捲軸等操作的函式。

待測範例應用程式

範例應用程式是一個基於GNOME gcalctool的計算機(Figure 1),它看起來像gcalctool,行為也像gcalctool,但它是用JavaFX開發而不是用gcalctool的原生語言Vala。

Figure 1. 範例應用程式的畫面

和其他的實作相較,這計算機會在輸入框中顯示使用者所有的輸入,當等於(=)按鈕被點擊時,結果才被計算。其他計算機只顯示最後輸入的數值,當等於按鈕被點擊時,只顯示結果,中間輸入的步驟不會被顯示。

這程式畫面是用Gluon Scene Builder 8.0.0建立的FXML檔案,所有按鈕套用CSS線性漸層風格,有趣的部分是對應的controller有三個函式:handleButtonActionhandleRemoveButtonActionhandleCalculationAction,前二個函式非常簡單,最後一個是當=按鈕被點擊時計算結果的函式,我將專注於測試這個函式。List 1是該函式根據使用者的輸入計算的片段程式碼。

Listing 1.
@FXML
private void handleCalculationAction(ActionEvent e) {
    String displayText = display.getText();
    int textLength = displayText.length();
    String result = "";
    if (displayText.contains("+")) {
        int plusIndex = displayText.indexOf("+");
        Double a = Double.valueOf(displayText.substring(0, plusIndex));
        Double b = Double.valueOf(displayText.substring(plusIndex + 1, textLength));
        result = String.valueOf((a + b));
    } else if (displayText.contains("x")) {
        int multiplyIndex = displayText.indexOf("x");
        Double a = Double.valueOf(displayText.substring(0, multiplyIndex));
        Double b = Double.valueOf(displayText.substring(multiplyIndex + 1, textLength));
        result = String.valueOf((a * b));
        display.setText(result);
    }
    // ...

}

這函式進行乘、除、加等運算,在函式的結尾,計算的結果會被設定成畫面上方文字輸入框中顯示的文字,這函式在下一節會被用來介紹如何用TestFX撰寫這範例程式的測試。為了教學的簡潔,省略某些功能(例如:減、二次方、小括弧、小數點等),若想完整功能的計算機,可以在這一期的Java Magazine中下載。

為範例程式寫測試

開始寫一個測試吧!模擬使用者想計算1加2的和:使用者依序點擊1、+及2的按鈕,接著按下=按鈕,計算機會在UI上方的文字輸入框顯示結果3。為了使用自動化測試來測試這情境,如前所述先在Maven中加入相依性,並完成測試類別的初始化(Listing 2)。

Listing 2.
public class CalculatorControllerTest extends GuiTest {

    @Override
    protected Parent getRootNode() {
        Parent p = null;
        try {
            p = FXMLLoader.load(getClass().getResource("gcalctoolFX.fxml"));
        } catch (IOException ex) {
            Logger.getLogger(CalculatorControllerTest.class.getName()).log(Level.SEVERE, null, ex);
        }
        return parent;
    }

下一步是從gcalctoolFX.fxml初始化我們想在這測試類別中測試的畫面(Listing 2),初始化是藉由載入對應的畫面,並將載入的畫面作為測試類別中getRootNode函式的回傳值來完成。完成初始化步驟後,可以開始撰寫測試案例的測試程式碼,一個TestFX測試案例(函式)必須遵循JUnit的公約(或稱慣例),包含使用@Test annotation,必須是public函式、回傳型別為void,Listing 3提供一個簡單的測試。

Listing 3.
@Test
public void testAddition() {
    Button one = find("#one");
    Button plus = find("#plus");
    Button two = find("#two");
    Button equalSign = find("#equal");
    click(one);
    click(plus);
    click(two);
    verifyThat("#display", hasText("1+2"));
    click(equalSign);
    verifyThat("#display", hasText("3.0"));
}

如您在Listing 3中所見,這測試和一般的JUnit測試一樣單純,在測試開始執行前,會透過Listing 2中的getRootNode函式建構待測試畫面,你唯一需要做的是使用從繼承類別GuiTest得來的find函式逐字尋找要點擊的按鈕。在使用FXML建構畫面的案例中,搜尋的文字即UI元件的辨識碼(fx:id),為了在controller類別中連結UI元件(bindings)與處理事件,必須設定辨識碼。辨識碼可以在Scene Builder中設定或直接編輯FXML檔案,若以撰寫程式的方式建構畫面,可以用setId()函式設定測試所需UI元件的辨識碼。

在用find()函式找到所需按鈕後,這些按鈕須以1、+、2、=的順序被點擊,透過呼叫click()函式的方式來點擊按鈕。在點擊=按鈕前,計算機的文字輸入框應該顯示“1+2”,點擊後,顯示的內容應該變成“3.0”作為加法運算的結果。可以用verifyThat()函式驗證顯示的文字是否正確,搭配hasText()函式指定預期顯示的文字內容。提醒一點,TestFX相容JaCoCo與其他涵蓋率計算工具,以紀錄多少行程式碼被您的TestFX測試所涵蓋,因此您可以像使用JUnit測試的方式來追蹤測試涵蓋率。

測試JavaFX應用程式,TestFX是相當不錯的測試框架,簡單、直覺,讓初學者可以快速上手,此外,提供簡潔的API讓測試易於理解,當測試失敗時,能更容易找出錯誤。

未通過的測試

單元測試的另一個重點,特別是當您在測試使用者介面時,是當測試未通過時,能提供什麼資訊。未通過的測試必須是可以被解譯、理解且容易重現,因此,重要的是,透過清楚的驗證才可以讓發生錯誤時明顯地看出什麼東西錯了。在TestFX中,Hamcrest matchers相當有用,因為它們能提供較易讀的驗證。從Listing 4可以看到TestFX測試的簡潔與Hamcrest matchers驗證的使用方式。

Listing 4.
@Test
public void testMultiplication() {
    Button two = find("#two");
    Button times = find("#times");
    Button three = find("#three");
    Button equalSign = find("#equal");
    click(two);
    click(times);
    click(three);
    verifyThat("#display", hasText("2x3"));
    click(equalSign);
    verifyThat("#display", hasText("6.0"));
}

Listing 4中的測試期望結果為6,透過修改controller的乘法運算來模擬臭蟲:將兩個運算元相乘後再加1,這個臭蟲用來說明TestFX如何處理錯誤,由於這個故意埋入的臭蟲,導致測試未通過,對應的堆疊軌跡顯示如下:

testMultiplication(...CalculatorControllerTest)
    Time elapsed: 2.434 sec <<< FAILURE!
java.lang.AssertionError:
Expected: Node should have label "6.0"
     but: Label was "7.0" Screenshot saved as /home/…/screenshot1436949687849.png
 at ...testfx.Assertions
             .verifyThat(Assertions.java:38)
 at ...testfx.Assertions
             .verifyThat(Assertions.java:26)
 at ...
...
Caused by: java.lang.AssertionError:
Expected: Node should have label "6.0"
     but: Label was "7.0"
  at org.hamcrest.MatcherAssert
        .assertThat(MatcherAssert.java:20)
  at org.loadui.testfx.Assertions
        .verifyThat(Assertions.java:33)
... 35 more

這(簡略的)堆疊軌跡可以提供我們清楚的資訊:哪一行程式驗證失敗了與失敗的條件,同時提供對應的螢幕截圖儲存位置。

限制

儘管TestFX是測試JavaFX應用程式相當有用的專案,但它仍然有些您需要注意的限制。第一個重大的限制是很難對TestFX撰寫的測試進行除錯(debugging),TestFX不允許在測試執行中移動滑鼠,由於TestFX無法回應中斷點,在除錯模式執行測試的過程中,如果您想在NetBeans畫面中觀察變數的值,會因為移動滑鼠導致測試中止。

還有一個主要的限制是當使用CSS修改元件外觀,就如同我對範例計算機的按鈕所做的一樣,套用風格後的元件有二個辨識碼:controller ID與CSS ID。這個版本的TestFX會搜尋id而不是fx:id,因此,它會使用CSS ID而不是controller ID,因為TestFX不會找到對應的按鈕而導致錯誤,繞過這問題的方法是每一個元件使用不同的CSS ID,這意味必須個別為每個按鈕套用相同的漸層風格。

另一個限制是截圖為全螢幕截圖。TestFX擷取整個螢幕畫面包含其他運作中的視窗而不僅僅是待測試程式視窗,大多數情況下,其它視窗對還原未通過的測試是不重要的,全螢幕截圖反而提供太多資訊,而且可能洩漏測試者或開發者的隱私。除此之外,截圖這功能無法開啟或關閉:每次測試未通過都會截圖,即便有時候您不需要。

還有,最近TestFX的開發已經漸緩,TestFX早在2014年就進入4.0的pre-release開發。更多的主動開發者加入有助於確保這相當有用的軟體能有後續版本。

結論

測試JavaFX應用程式,TestFX是相當不錯的測試框架,簡單、直覺,讓初學者可以快速上手,此外,提供簡潔的API讓測試易於理解,當測試失敗時,能更容易找出錯誤。然而,使用目前版本的TestFX時要記住:它仍存在一些限制。

LEARN MORE
Download the calculator
Video lecture on TestFX, with additional usage details
Gluon Scene Builder 8.0.0

譯者的告白
幾次信件的往返,取得Java Magazine編輯的同意,可以把喜歡的文章翻譯成中文後放在自己的部落格,這篇文章是首次嘗試,希望翻譯的品質別砸了自己與人家的招牌!我會陸續翻譯覺得有趣的文章。

這次翻譯發現:中英文在斷句與分段上其實不太一樣。若保留原有段落,有些段落會超級短,所以後來有進行些微的段落調整。另外,英文每一句的結尾都是句點,但翻成中文時不全是句點,有時候反而應該用逗點,這是我第一次翻這麼長的英翻中(過去考試大多是中翻英,念原文書時只需知道意思就好,也沒有特別想該用句點還是逗點),所以逗句都需要思考一下。最後是英文某些句子的被動形式與子句在翻成中文時,若完整照翻反而讀起來不通順,所以某些被動式會被轉成主動式,子句也會調整,盡可能讓中文讀起來通順。若有人覺得翻譯錯誤或是錯字,歡迎告知我。

 
almost 3 years ago

忘記是從哪裡看來CodeFights這網站了,這網站的概念很簡單,就是試著把寫程式變成挑戰遊戲一樣,印象中剛開始玩時,是每題有n秒解題,看能連續在時間內解多少題,玩了幾次常常在第七或第八題就超過時間了(當然有時候第一題或第二題就掛了XD),後來一忙也就沒再光顧了。

最近看到CodeFights的信,似乎有點不一樣,於是再次到CodeFights玩玩,多了幾種模式:挑戰最少程式碼解題、對戰(這好像之前也有,只是沒玩過),對戰就是隨機配對,然後解相同的題目數題,越早完成分數越高,全部結束後比誰分數高,目前只玩一次,挺刺激的。

另外就是網友發挑戰,給予特定的獎金,在特定實現之前解出來可以獲得獎金(獎金可以在挑戰結束後用來看別人的答案),若在特定語言用最少字元(扣除空白)完成,另可得到最短優勝者,其實挺有意思的,所以連續玩了幾天,有些感想。第一個想法是,沒想到數學題蠻多的,然後雖然要想比較久,但我還蠻喜歡用遞迴解題。

最近一個我覺得有意思的題目是Doubling

Lonerz got some crazy growing plants and he wants to grow them nice and well. Initially, the garden is completely barren. Each morning, lonerz can put any number of plants into the garden to grow. And at night, each plant mutates into two plants. lonerz really hopes to see n plants in his garden.

Your task is to find the minimum number of plants lonerz has to plant to get n plants one day?

Example:
Doubling(5) = 2
Lonerz hopes to see 5 plants. He adds 1 plant on the first morning and on the third morning there would be 4 plants in the garden. He then adds 1 more and sees 5 plants. So, lonerz only needs to add 2 plants to his garden.

Doubling(8) = 1
lonerz hopes to see 8 plants. Thus, he just needs to add 1 plant in the beginning and wait for it to double till 8.

一開始確實想用遞迴去解題,但在幾次排列後,突然發現根本沒有那麼複雜:

1 -> 1 (第一天種就得到1)
2 -> 10 (第一天種,第二天就得到2)
3 -> 11 (第一天種,第二天再種就得到3)
4 -> 100 (第一天種,第三天就得到4)
5 -> 101 (第一天種,第三天再種就得到5)
6 -> 110 (第一天種,第二天再種,第三天就得到6)
7 -> 111 (第一天種,第二天再種,第三天再種一次就得到7)

到這裡,我想學過二進制的都已經看出來,這題該怎麼解了,只需要算出n的二進制表示中有幾個1即可,本來想自己寫算1的數量,但後來想到Java有內建的Integer.bitCount(n)可以用,於是把答案送交了,當時還以45個字元與很多人並列最少字元,隔陣子收到信說挑戰結束了,看看誰是最少字元,沒想到還有42個字元的答案,當時花了金幣去看對方的答案,看到後有點傻眼,想法一樣,但確實比我短,有興趣的人就猜猜看吧!

看完答案的感想是,追求卓越的人還真的不少,到目前為止,我最有把握拿到最少字元的解就這題,其他不是比領先者多一倍,或是差個位數個字元,例如這題計算數字金字塔某一列的總和,我已經找出數學式,但並沒有拿到最少字元,差2個字元,這挑戰還沒結束,所以暫時我也不知道對方是怎麼完成的。

第三個感想是,問題的表達方式可以很有趣,如果剛剛所列的問題直接寫成:計算出某個整數在二進制表示中1的數目,我想應該不會有人覺得很有趣吧!雖然CodeFights中也有很平舖直述的挑戰題目,但我特別喜歡這類有趣的題目,例如這題摺紙

話說摺紙題當初也是用遞迴的方式解,但中了陷阱,CodeFights也是有TDD概念的,只是測試是CodeFights幫大家寫好了,但寫完即跑的和送交答案的測試案例數量不同,當初寫完跑測試一下就過了,於是很高興地送交,結果被退,看了好久才發現少考慮一個條件,因此,真的,測試案例的數量和品質很重要!

不過既然問題的表達方式可以有很多種,是不是也表示,當面臨一個問題時,有時候想的太複雜了,有時候又沒有真正理解背後的問題,不管是客戶開的需求也好,或是自己創業從客戶得到的回饋也好,若只考量到問題的表象,事實上不見得能真正地幫助到客戶,甚至會用很複雜的方式去處理一個問題。

最後,曾經有人用不可思議的短程式解決某個問題,在好奇心驅使下,我花了金幣去看對方的答案,第一個瞬間是三字經,看了很久後終於懂了,但對我來說,我寧願寫長一點,讓我的語意清楚一點,讓以後維護的人好懂一點。程式是寫給人看的,簡單易讀比較重要!

 
about 3 years ago

隨著團隊組織方式的改變,個人在團隊中的角色也有了改變,改變至今大概有三個多月,從不同的角度在看事情時,也能得到許多有趣的想法,加上幾次參加C.C. Agile也都能得到意外的驚喜,讓自己反省:自己真的Agile了嗎?真的了解scrum?

敏捷宣言 (Manifesto for Agile Software Development)可能很多人都能朗朗上口(事實上,若隨堂考我,書上或網路上能查到的東西,我通常背不出來,除非我是一個指導別人怎麼跑敏捷開發的顧問,不然背的滾瓜爛熟有什麼意義?),但其中一句:Responding to change over following a plan,就這樣的一句,都不見得是容易做到的,有時內心其實是在抗拒的,因為人本質(well,我不是心理學家,我可能無法證明這件事)上是有點害怕改變,因此做任何事前,都會有計畫並希望所有的事情都照計畫走。

過去的軟體開發便是想管控變化,很多團隊都使用瀑布模型、使用CMMI...,有趣的是,Royce在論文中提瀑布模型的目的,是要拿它當例子來說明軟體開發不能這樣做!有了完整的分析、設計和計畫,然後盡可能避免變化,但大家都知道這不可行,於是有人提倡了敏捷開發,但即便用了某某敏捷開發方法,擁抱改變依舊不是件簡單的事,因為內心還是有waterfall依舊陰魂不散地在很多小地方出現。在敏捷團隊中,還是有可能聽到這些對話:

  「因為沒有細部設計,到時候可能會重工...」
  「因為沒有細部設計,我無法決定...」
  「某某某又改了什麼設計,重工浪費很多時間...」

回頭想想,敏捷精神到底是什麼?我覺得最核心的部分是快速取得回饋並盡快做調整(或稱trial-and-error process),如果在這個核心上來看scrum的幾個活動、產出或實踐,也許就能知道自己是否做得好或不好?

Planning meeting - 重點不在於設計是否一定要細到如規格書一樣才能估時數,細節足於開工就夠了,重點在於團隊的理解是否偏離使用者的需求,一些細節在實作的過程中都還能跟PO確認與調整

Daily scrum - 重點不在於昨天做了什麼?今天要做什麼?還需要多少時間?重點在於我遇到什麼困難,是否需要援助,在會後,scrum master或團隊是否能幫助有困難的人解決問題

Burn down chart - 重點不在於進度是在線上或線下,重點是在線上時團隊能做出怎樣的調整?在線下時,是否有細節被隱藏,當提前完成時,是否還能再加入新的story

Review meeting - 前陣子曾在C.C. Agile聽到一個團隊,因為平時story一完成馬上就跟PO驗證,因此除非真的要展示給stakeholder看,或是要跟團隊討論product backlog的優先順序,不然是不舉行review meeting,若核心精神是快速取得回饋並盡快做調整,那當一個story完成馬上與PO確認,有問題在還有時間的情況下,可以調整不是更好嗎?所以重點還是在於如何快速驗證story是否完成

Retrospective meeting - 重點在於團隊是多認真看待持續改善這件事,願不願意說真話,能不能提出有意義的建言。更重要的是團隊能獲得多少自主調整的空間,在獲得這空間後,團隊是否努力改善自我

Continuous integration and regression testing - 這幾乎是最能反映核心精神的實踐,由於就是有持續整合與回歸測試,因此能快速取得回饋(建置失敗或無法通過測試),才能快速調整(修正)。

在原始的scrum中並沒有明確提到user story refinemenet meeting,但在後來很多scrum教材或實施上,都會有這個會議來釐清stakeholder的需求,團隊過去也在這個會議的進行方式上做過多次調整,在團隊分組後,到目前也調整了兩次進行方式,但運作起來總覺得哪裡卡卡的,最誇張的一次是分組後的第一次refinement meeting:全員加班到隔天凌晨,那一次對團隊士氣的殺傷力其實很大,個人認為PO與團隊之間出現了些小裂痕。

因為團隊成員或多或少對於設計有高度的興趣,因此後面的幾次調整還是讓團隊能參與what的設計,個人提出了一些建議,至今我仍然還在想:以我的角色,我當時應該出手建議嗎?後來這建議和其他團隊成員與PO的意見變成了一個新流程。即使是分組後新的流程,回頭看看過去執行的情況,腦海中浮現過去scrum master說過的一句話:我們該不會是mini waterfall吧

當refinement meeting的重點不在於釐清問題與背後的情境,不在溝通和確認需求,不在找出what,馬上投入到how的細節設計,這設計真的滿足stakeholder需求嗎?當設計細節已經細到UI上的某個元件如何呈現、要有哪些多國語系的key時,這和過去waterfall的差別又是什麼?當為了避免開始施工時發現問題,回頭大幅修改設計(這個自己可能也要反省,因為自己也曾抱怨類似的問題),因此希望能夠在refinement時確認所有細節時,這樣真的算是擁抱改變嗎?

過去我很喜歡《搶救貧窮大作戰》這個節目,節目尋找經營不善的店家,協助老闆到其他的店進行非常嚴苛的訓練,在獲得技能後改善原有的經營方式,這節目後來有回去追蹤過去參加受訓的店家,有的能延續好的成績,有的則變回跟受訓前沒兩樣,現在想想,這會不會是在守、破、離的成長過程中,其實並沒有在守的過程中,真正理解那些規則、教條背後的意義,並自以為是地進到破與離的階段?或是只在守這個階段原地踏步?

回到敏捷開發或是scrum的方法中,這些活動或教條,依樣畫葫蘆,反覆練習,直到熟練,但真的了解背後意涵嗎?當要根據團隊的情況對現行方法進行調整,進一步到破的階段時,是否沒有偏離原先的價值?是否能擺脫waterfall的心魔?這或許是轉換角色後,我思考最多的問題。當然,如果一開始抓住的核心觀念是錯的,那自然會練成九陰白骨爪或是逆練九陰真經。

內心獨白:這篇其實很早就想寫了,只是角色的定位讓我猶豫要不要寫,有些角色看起來好樣可以為所欲為,但其實綁手綁腳...

 
over 3 years ago


前言:老實說,從中文書名無法聯想回原文書是《The Elements of Scrum》,雖然書名翻譯沒有太離譜(和內容無關之類的),但總覺得貼近原意會好一點。

『Scrum團隊週記』這一章,整個讀完,其實就差不多可以了解Scrum的大部分,所以,若要讀這本書,又沒有太多時間,就先看這一章吧!

第一章 起點:瀑布方法

有點諷刺的是,Royce之所以提供這個模型(瀑布模型),是要拿它當例子來說明軟體開發不能這樣做! p. 24

第二章 加入敏捷實踐者行列

我們所設計的複雜系統,主要是由可變性和高度非線性的人類組成,但一直以來,我們在設計系統時卻從未把人類或人類對系統的影響考慮進去。回想起來,雖然覺得很荒謬,但在這行業中,投入了大量精力致力於理解人們對軟體開發所造成影響的人員,真的是少得可憐。 p. 31

文件邊做邊寫,只寫必須的文件。將文件作業融入流程,只寫有關的、有效用的文件。 p. 33

第三章 敏捷價值觀與原則

大多數的人普遍誤以為敏捷團隊是不寫文件不做計劃的,但實際上,敏捷團隊為規劃(planning)和文件工作所花費的時間、精力遠多於傳統團隊,因為計畫(plan)需要不斷地進行詳細劃分和更新。 p. 39

敏捷計畫在整個專案生命期間是一直在累積和演變的,是有生命的,而不是奉為圭臬的厚重書籍。這意味者,不管在任何的時間點,計畫都能代表我們當時對專案的最佳理解,但我們仍然期待,在我們檢驗和調整的同時,它也在演變和改進p. 39

在團隊內部,效率最高且效果最佳的資訊傳遞方式,就是面對面的交談。 (中略) 敏捷團隊是在更加開放的共用空間中工作,且只要有可能都會選擇以口頭交流的方式。 p. 47

最佳的架構、需求與設計都源自於能夠自我組織的團隊。 (中略) 在scrum團隊中,她可不是『那個架構師』(The Architect)而已,團隊會將她的專業技能視為非常重要的資源,也經常會尋求他的指導,架構工作也會有她的參與和貢獻,旦絕不會是由她來『負責架構』,而是由團隊平均分擔這個責任。 p. 51

第五章 Scrum歷史簡述

Schwaber覺得,新方法成功的秘訣在於,他使用了經驗式(empirical)流程(檢驗和調整),而不是自定義流程(計畫和執行)。 p. 70

第六章 Scrum角色

產品負責人(Product owner)的責任就在於,幫公司得到最高投資回報,作法是引導團隊做最有價值的工作,並遠離價值較低的工作。也就是,產品負責人控制著團隊待辦清單(product backlog)上的優先順序。在scrum中,產品負責人是唯一有權要求團隊做(什麼)事以及改變待辦清單修先順序的人。 p. 74

後註:我還是不喜歡專有名詞硬要翻譯成中文。

scrum master總是和團隊間『保持著一定的距離』,不至於太過親密,但會密切注意流程和進度的情況,獻計獻策幫助團隊解決小問題,有需要時還會扮演共鳴板角色p. 76

一個高產出和更為自我管理的團隊,他們可能已經不再需要scrum master來主持scrum會議。稱職的scrum master會退居幕後並鼓勵他們放手去做。事實上,稱職的scrum master會不斷地調整自己的風格去適應團隊的需要。 p. 77

scrum master還有另一個關鍵的作用,為團隊移除障礙。任何拖累團隊的都是障礙,它們的形式多樣、規模各異。 p. 77

一個scrum團隊應該有多少個隊員呢?經驗法則通常是七個人,再或多或少兩個人,也就五到九個人。如果團隊人數再少,可能會欠缺多樣化的足夠技能,而難以全部完成使用者故事(user story)所需的工作。如果團隊人數更多,溝通開銷會開始過度地增長。務必牢記,這只是參考而已,人數更多或更少的成功團隊我們都見過p. 79

Scrum並不追求做到團隊所有成員都可以互換,只要他們在團隊需要時,願意選擇舒適區以外的工作即可p. 80

第七章 Sprint週期

一個典型團隊要掌握敏捷流程的精髓大約需要六個sprint,至於使用的是一天還是四週的sprint則無關緊要。 p. 84

我們建議,按照每個開發週期2小時的比例來安排sprint規劃會議(sprint planning meeting),一週的sprint適合開2小時的會,而一個二週的sprint開4個小時的會應該也夠了。 p. 85

即使團隊已經全力以赴地確認澄清需要完成的所有工作(task),但實際上得到的清單仍可能有欠完整。當實際工作展開後,新工作就會顯露出來,此時,把新工作記錄下來,並添加到目前sprint的工作清單(sprint backlog)即可。高效率團隊通常能在sprint規劃會議上確認60%到70%左右的工作。 p. 87

那工作估算該怎麼做呢?用什麼單位?推薦三種常用方式:工作小時(用工作小時做估算單位,有時候會造成混亂,因人們總是期待著,工作可以在估計的時間內完成,這就是估算的本質,也是估大小往往比估時間更有效的原因)、工作點(sprint過程中,團隊只要追蹤需要完成的剩餘點數即可)和工作數(缺點是他無法在一開始就發現工作過大或過多的情況並警示團隊)。哪種方式最好?團隊得自己拿主意。要記住,最終目標還是在團隊偏移原計畫時程能及早發現,如此就還來得及採取行動。 p. 88

其一,清單修整(user story refinement)屬於事前行工作,在週期中要趁早完成以便有充足的前置時間,讓產品負責人可以在他們的計畫和優先順序排序工作中用上這些估算。其二,在sprint長度超過一週的情況下,1小時的會議多開幾次,效果會比一次開幾個小時更好。(中略)其三,你應該不想將sprint結尾弄得跟馬拉松會議一樣長(結尾有sprint review和自省會議,再開refinement會議實在太長)。 p. 91

第八章 Scrum產出物

在產品待辦清單上方的,大多是較小型且定義完善的項目。這樣正好,因為團隊接下來要實做的就是這些故事。清單再往下,故事的規模更大,定義也更粗略。沒關係,只要在把它們挪到上方之前,好好地改善即可。當特性(feature)在產品待辦清單中不斷地往上移動,就代表越來越接近它將被實現的時刻,而團隊對它的檢查也會更加詳細,估算和驗收標準也更精確,大故事也會被拆解成小故事。(中略)清單的優點就在於,絕不會浪費時間為那些可能永不見天日的特性撰寫詳細規格書p. 102

值得一提的是,完成之定義(Definition of Done, DoD)和驗收標準(Acceptance Criteria)是有區別的。驗收標準是屬於產品負責人或客戶的領域,明確定義了被視為『可接受』(的)產品必須滿足的條件並記錄在案。但像是回歸測試或版本整合這些內容,則不屬於驗收標準的範疇。完成之定義歸開發團隊所有,關注的不是產品的使用者導向的功能,而是若產品要能交付,必須要做完那些工作。(可參考《Executable Specifications with Scrum》書摘) p. 113

第九章 使用者故事

使用者故事是對談的敲門磚。使用者故事不是完整的需求或說明書,它們是佔位關係(placeholder)。它們的資訊量足以提醒團隊有東西要完成,但我們刻意地不過多探討細節......直到必須(開發)之時。 p. 118

對談中,如果某一刻大家覺得對使用者故事已有共識,那就該寫驗收標準了。(中略)完善驗收標準也是修整清單工作(user story refinement)的一部分。專家級產品負責人應符合以下要求,在sprint開始前他們就已定義所有驗收標準並和團隊達成一致,而且sprint中不做改變p. 119

第十章 用故事大小值估算工作

產生估算的真正目的是要提供進度的可預測性度量。 p. 123

團隊估算遊戲規則
第一部分:集體排隊
所有玩家輪流上場,輪到自己時可以做以下任何一件事:
- 在牆上放置新的故事卡。
- 移動已在牆上的故事卡。保持其他卡片的順序,即使為了給那張換位卡留出空間,而移動很多卡片也完全沒關係。
- 移交行動權給下一個玩家。
卡片請依從左到右且從小到大的順序放置。可以將它們調鬆些,後面在調整順序時才方便。所有人都輪流上場待(故事卡)選擇完時遊戲宣告結束。

第二部分:你的數字是?
所有玩家輪流上場,輪到自己時可以做以下任何一件事:
- 在某章故事卡上方放下一張費式數列卡片,代表此處的故事大小增加了。
- 將費式數列卡片換到另一個故事的位置。(移動時必須維持數字的順序,也就是說1在2之前,13在21之前。)
- 像第一部分那樣移動故事卡片。
- 移交行動權給下一個玩家。
所有人都輪流上場待選擇完後遊戲宣告結束,代表故事的順序和大小分配都無須再做調整。 p. 133

-- End --