前言
?
RIA ( Rich Internet Applications ),這個術語已經出現多年,隨著開源社區和廠商的不斷貢獻,涌現出許多 RIA 產品,幾個重要產品分別是:
Adobe Flex —— 基于 Flash 、 actionscript 、 MXML 。
OpenLaszlo —— 基于 Flash 、 JavaScript 、 LZX 。
?????? Dojo????? —— 基于 JavaScript 、 DHTML 、 XML 。
?????? XUL????? —— 基于 XUL 、 JavaScript 。
除了以上產品外,還有大量的 Ajax 、 Applet 開源項目。他們的目的只有一個:簡化并改善 Web 應用程序的交互操作,使應用程序可以提供更豐富、更具有交互性和響應性的用戶體驗。
本文 UI 方面,我們關注重點是 OpenLaszlo 。 OpenLaszlo 的前身是 LPS ( Laszlo Presentation Server ),由 Laszlo Systems 公司在 2002 年發布, LPS 是那個時代誕生的第一個 RIA 產品。根據市場和用戶的需要, Laszlo Systems 公司于 2004 年在 CPL 協議下發布了 LPS 的開源產品 OpenLaszlo 。目前,代號為“ Legals ”的 OpenLaszlo 即將在明年第一季度發布,它的出現使 OpenLaszlo 能夠支持 Ajax ( DHTML )。 “ Legals ”的下一個項目代號為“ Osprey ”,它的目標是支持 swf9 的運行目標,也就是當前 Flex 2 所采用的運行文件版本,預計發布時間為明年的第二或者第三季度。 代號為 “ Orbit ” 的項目是 Sun 和 Laszlo Systems 公司共同開發的產品 , 目的是能讓 OpenLaszlo 程序運行在包括 Java ME 在內的任何 Java 平臺上 , 比如移動電話、 PDA 、電視機頂盒、打印機 , 等等。
OpenLaszlo 優勢在于它是開源的 ( 遵循 CPL 協議 ), 基于開發者熟悉的技術 ( JavaScript 、 XML ) 。在 SOLO ( Standalone OpenLaszlo Output )方式下可部署到任意的 HTTP Web 服務器,在非 SOLO 方式( Proxied 方式)下可部署在裝有 OpenLaszlo Server 的 Linux 、 UNIX 、 Windows 、 Mac OS X 操作系統上,并支持任意的 Java Servlet 容器、 Java EE 應用服務器。只要您的瀏覽器支持 Flash 6 或者更高版本,那么就可以體驗 OpenLaszlo 了。
本文利用 db4o 作為數據庫, db4o 是一個開源的純面向對象數據庫引擎,對于 Java 與 .NET 開發者來說都是一個簡單易用的對象持久化工具。同時, db4o 已經被眾多的企業級應用驗證為具有優秀性能的面向對象數據庫。可以通過《 開源面向對象數據庫 db4o 之旅: 初識 db4o 》、《 開源面向對象數據庫 db4o 之旅: db4o 查詢方式 》系列文章來了解。
OpenLaszlo 安裝
以 Windows 2000/XP 安裝為例,首先安裝 Java 1.4 或以上版本,接著下載 OpenLaszlo Server 3.3.3 ,這個過程并不困難,安裝完畢會自動進入 OpenLaszlo 歡迎頁面,很漂亮的世界時鐘。
現在就可以立即體驗 OpenLaszlo 的魅力了,只要您有任何一款 XML 編輯器就行,哪怕是 Windows 自帶的記事本。轉到 OpenLaszlo 的默認安裝目錄“C:\Program Files\OpenLaszlo Server 3.3.3\Server\lps-3.3.3”,新建文件夾“hello”,然后在“hello”文件夾下新建“hello.lzx”文件,向其中寫入:
第一個程序完成 , 在 IE 瀏覽器輸入如下 地址 , 如圖一所示。 ????????????????
???????????????? 圖一 第一個 laszlo 頁面
“效果很不錯 …… ”,也許您還在欣賞自己地杰作,不過馬上感到這樣開發不太方便,好在 OpenLaszlo 為我們提供了 Eclipse 插件 IDE4Laszlo , IDE4Laszlo 以前是 IBM alphaWorks 項目,現在已經開源給 Eclipse ,可在 這里 找到,目前的版本是 0.2.0 。安裝插件之前還必須確保您的 Eclipse 安裝了 WTP1.0 SDK 。
接下來安裝插件,在 Eclipse 的菜單欄選擇“ Select Help >> Software Updates >> Find and Install ”再選中“ Search for new features to install ”,點擊“ New Archived Site ”按鈕選擇剛才下載的插件 zip 文件,最后根據提示即可順利完成插件安裝。
上面的 OpenLaszlo Server 依靠 Tomcat 服務器為運行環境,不過 OpenLaszlo 也可以運行在例如 Jboss 、 WebLogic 、 WebSphere 等服務器中,并構建自己的運行環境。本應用程序是在 OpenLaszlo 自己的運行環境中構建,可以在 這里 下載。
程序架構
與傳統 Web 應用一樣,RIA 應用也遵循 MVC 模式,只不過客戶端要依賴控制端和服務端提供數據來更新視圖,所以多了一個數據層。本實例為了簡化程序,使用 JSP 文件來作為控制層與數據層的橋梁,當用戶進行操作請求時,OpenLaszlo 向 JSP 發出請求,JSP 獲得請求并分析應該調用哪種業務操作,業務層根據所請求的方法與持久層進行交互,將獲取的數據以 XML 字符串的形式返回,并通過 JSP 頁面傳遞給 OpenLaszlo 應用。圖二展示了本實例的程序架構與數據流轉過程。
???????????????????????????? ?????????????????????????????????????????????? 圖二 OLJD架構圖
下面在 Eclipse 建立一個 Laszlo 工程以展示這個應用的程序結構(由于該應用程序是基于獨立的 OpenLaszlo 運行環境,所以未使用 IDE4Laszlo 提供的特性)。
打開 Eclipse,創建一個名為“openlaszloWITHdb4o”的 Web 應用程序。接著轉入該程序的所在目錄,為了能保證該應用程序能順利加入 RIA 特性,請結合圖三進行如下操作:
1、????????????? 從 OpenLaszlo 獨立運行環境的 war 包中拷貝“openlaszlo-3.3.3-servlet\lps”目錄到“openlaszloWITHdb4o\”下,該目錄是 OpenLaszlo 的組件庫。
2、????????????? 復制“openlaszlo-3.3.3-servlet\WEB-INF\lib”目錄到“openlaszloWITHdb4o\WEB-INF\”,該目錄是 OpenLaszlo 的支持庫,然后在該項目的“Java Build Path”中引入這些庫,同時再引入 dom4j 和 db4o 的支持庫,應用程序的服務器端會用到。
3、????????????? 復制“openlaszlo-3.3.3-servlet\WEB-INF\lps”目錄到“openlaszloWITHdb4o\WEB-INF\”,該目錄是 OpenLaszlo 的組件配置信息。
4、????????????? 在“openlaszloWITHdb4o\src”下,分別創建 bo、com 包和類文件,接著創建“helloDb4oUser.lzx”和“peopleoperat.jsp”文件。
5、????????????? 最后,把 war 包的 web.xml 文件拷貝過來,啟動 OpenLaszlo Server 引擎全靠它了。
????????????????????????????????????????????????????????
???????? 圖三 eclipse中的工程文件結構
服務器端代碼
應用程序使用了 Java 5 特性,請保證您的開發環境與本文一致。bo 包是整個用戶管理應用程序的數據模型,People 類定義了“用戶”,包含姓名、電話等基本信息,請 下載程序源碼查看 。
com 包是與持久層交互的重要部分,Db4oInit 類是一個 Servlet,它的作用是隨 Servlet 容器同時啟動 db4o 數據庫,并作為 Server 運行,以便在運行時通過 db4o 對象管理工具進行管理,了解 db4o 的 Server/ Client 模式的具體含義,請參考 《開源面向對象數據庫 db4o 之旅: db4o 查詢方式》 。最后,不要忘了在 web.xml 中加載 Db4oInit Servlet。??
Db4oInit 的 init 方法,打開一個名為“auto.yap”的 db4o 數據庫文件,不用擔心,如果是因為第一次運行而沒有該文件,db4o 會自動創建,除了數據庫文件,還要加上端口號,在這里我們使用 1010,最后進行口令授權。
ContactService 類是 db4o 的 Client 端,用于響應業務操作,包含了最基本的 CRUD 操作。值得注意的是 getPeopleXml 方法,我們利用 db4o 的 QBE 方式查詢數據庫,如果您愿意,還可以使用 SODA、NQ 方式查詢數據庫。之后利用 dom4j 進行對象的序列化,在此筆者還推薦 XStream 結合 java 5 的 annotation 進行序列化。新增、刪除、修改很簡單,不再敷述,請下載程序源碼查看,注意每次操作完之后 commit 事務即可。
peopleoperat.jsp 代碼也相對簡單,請 下載程序源碼查看 。
頁面代碼
要編寫 OpenLaszlo 服務器 能運行的代碼,就必須使用 LZX 語言,LZX 是一種面向對象的基于標記的語言 , 利用 XML 和 JavaScript 語法構建 RIA 表現層 , 通常這些編寫完成的代碼會經過 OpenLaszlo 編譯器進行編譯,最后可作為獨立的 swf 文件或部署在服務器上運行。 LZX 語言使用開發者熟悉的語法和命名方式,因此能較容易地融入現有編程環境。
LZX 語言具有繼承、封裝、多態等面向對象語言所具有的特性。通常,一個標簽就等于實例化了一個類,比如 <view> 就是 LzView 的實例。值得注意的是,和基于 DHTML/JavaScript 的程序有著概念上的不同,典型的 DHTML/JavaScript 程序是直接由瀏覽器解析、執行嵌入其中的 JavaScript;而 LZX 中的 JavaScript 則是在 OpenLaszlo 服務器端編譯成客戶端 Flash 解析引擎能運行的字節碼,再下載到瀏覽器執行的(swf 文件 )。
LZX 程序是運行在名為 canvas 的可視對象基礎上的,也就是以 <canvas> 開始并以 </canvas> 結束的標簽中。在 canvas 中,有許多 view,這些 view 嵌套著業務邏輯、可視化特性、可編程屬性、尺寸、位置、背景顏色、不透明性、可點擊性、伸縮性,等等信息。view 也可以包含資源,例如圖像或視頻,也可以動態綁定任意 XML 格式數據集。
現在我們就進入代碼部分,首先觀察前三行:
第一行代碼向我們表明了 LZX 語言的面向對象特性,canvas 對象的 debug 屬性為了讓程序調試時能在 debugger 窗口打印出調試信息,height、width 屬性分別指定了 canvas 對象的高和寬,超過這一大小,將不會被顯示,title 屬性和 HTML 中 title 標簽功能一樣,fontsize 屬性作為一個全局參數,如果沒有明確指定,那么在 canvas 中所有的 view 都將使用該字體大小。第二行代碼實例化 Debug 對象,并規定了 debugger 的高和寬,除此以外,還定義了窗口出現的 x(橫坐標)、y(縱坐標),這些都是相對坐標。第三行定義了 splash 對象,該對象是加載應用程序時的進度條。
LZX 還提供了強大的繪圖功能,下面就讓它小試牛刀吧:
在本應用中,我們要實現的是一個漸變的黃色 banner。drawview 類是專門繪圖的,handler 類捕獲了 oninit 內置事件。我們這樣設想:手中有只畫筆,從某點開始,然后按照一定路徑進行邊框描繪,最后填充顏色。同理,this.beginPath() 設置畫筆的開始點(0,0 坐標),然后繪制直線到 780,0 坐標,接著再繪制到 780, 50 坐標,然后閉合方框(this 表示 drawview 自身對象實例),this.createLinearGradient(0,0,0,50) 方法是在按照 x0, y0, x1, y1 這種路徑進行梯度過渡,this.globalAlpha、g.addColorStop() 是一系列的顏色填充設置,從 0x666666 顏色均勻過渡到 0xFFFF00 顏色,最后 this.fillStyle = g 應用該設置,this.fill() 填充即可。
有了以上基礎,我們開始解析可視化組件,下面是構造邊框為綠色的 bordergreenbox 類:
class 標簽是自定義類的基礎,name="bordergreenbox" 屬性為類命名,extends="view" 聲明 “bordergreenbox”類是繼承自“view”(view 是最常用的類,用于組織包含在其中的元素的交互),相應的也就繼承了“view”所有的屬性和方法。定義寬度時用到了“${}”約束符,這意味著“bordergreenbox”的寬度要受到“canvas.width-2”的約束,接著設置背景為綠色 bgcolor="0x9ae900"。為了能達到綠色邊框的效果,我們在其中嵌入另一個 view,再設置顏色、相對坐標、高、寬。
以下部分是在構造 newPeople 類,用于讓用戶輸入基本信息:
和 bordergreenbox 類不同的是,newPeople 類引入了新屬性 visible="false",它的作用是讓newPeople 不可見,如果不設置該屬性則默認為可見。我們還引入了 simplelayout 類,它的作用是均勻分布 newPeople 類中所有的對象,spacing="4" 是在定義間距為 4,axis="x" 定義按照橫向分布。text 類是用來嵌入文字的,且支持嵌入 <a>、<b>、<font> 等 HTML 標記,而 edittext 的作用則類似 HTML 中的 input 標簽。運行效果如圖四。????????????????
??????????????????????????????????? ? ? 圖四:newPeople 類
如何訪問 JSP 提供的數據源呢?在這個應用中,分別構建了兩個數據集,db4odata 用來獲取返回的列表數據,peopleoperate 進行業務操作,下面主要分析 db4odata:
dataset 是數據訪問類,src 作為資源訪問路徑指向 JSP 頁面,并規定type 為 http 協議,當request="false" 時,必須通過調用 doRequest() 方法才能訪問資源并獲取數據,而為“true”時,則會自動訪問資源,一般應設置為“false”。接下來用到了 datapointer 類,datapointer 是 dataset 的數據指針,并遵循 XPATH 語法規范訪問數據,xpath="db4odata:/*" 定義 XPATH 的路徑,既 db4odata dataset 的所有子路徑。handler 類捕獲 ondata 事件(ondata 是 datapointer 對象內置事件),并通過“.”運算符為 bordergreenbox 類的實例 bgview 設置數據源,再隱藏一個名為 loader 的對象,代碼如下:
不錯,loader 對象僅僅寫下“loading...”,這是為了改善程序的互動效果。接下來分析 bordergreenbox 類實例 bgview,bgview 包含了兩個 view 對象。以下代碼向我們展示了第一個 view,它的作用是提供列名:
第二個 view 對象 rowcontainer,剛才我們分析 db4odata 數據源集時捕獲的 ondata 事件嗎?不錯,一旦接收到數據,rowcontainer 也就從 name="db4odtpt" 的數據指針中獲取了數據:
clip="true" 屬性是為了讓包含在其中的子對象之間能夠很好的布局。rowcontainer 中又嵌套了 view 對象 columns,并通過父對象取得了數據路徑 datapath,接著按縱向均勻布局其中的行,selectionmanager 類似 HTML 中的 select 標簽,一旦選中某行將觸發為其內置屬性 selected 賦值為“true”。接下來的 view 才是真正顯示數據列表,onclick 事件也是 view 類的內置事件,當點擊了某條記錄,將聯合 selectionmanager 進行操作,所以在這里為 selectionmanager 傳入了選中的某條記錄。datapath="@userId" 用于綁定 XML 數據中的 userId 屬性,并自動顯示。剛才說到由于 selectionmanager 和 view 是緊密聯系的,所以在這里重寫了 view 的 setSelected 抽象方法,并傳入了布爾類型數據。在 <![CDATA[ ]]> 之間的代碼,OpenLaszlo 將忽略其中的特殊字符(例如“<”符號),np 是之前我們構造的 newPeople 類的實例,接下來會講到,而 canvas.np.sendit.userid = this.datapath.xpathQuery('@userId') 則是在進行賦值操作,雖然 newPeople 類的實例是不可見的,但卻隱含的傳入了數據,canvas.np.userName.setAttribute('enabled',false) 是動態創建的屬性,為了區分“新增”、“修改”操作(新增可編輯“用戶名”而修改則不能)。var bgcolor 是在聲明局部變量,和 Java 語言不同的是,if 語句內聲明的局部變量也能在語句之外調用,所以 this.setBGColor(bgcolor) 不會出現任何異常。
想必大家很關心 newPeople 類實例 np 是什么樣子,接下來會講解到如何添加和修改數據并提交給 JSP 處理:
在此我們引入了 button 類,接著構造了兩個屬性,userid 用于在修改時傳遞用戶 ID,而 action 則用于確定是何種業務操作。接著捕獲了 button 類的 onclick 事件,聲明局部變量以便接下來發送,function send(action){…} 是構造的內部方法,用于發送請求,peopleoperate.setQueryParam() 是在對 peopleoperate 數據集創建請求參數,最后 peopleoperate.doRequest() 發出請求。構造好內部方法后,開始判斷是哪種業務操作,并分別傳入 'addPeople' 和 'updatePeople' 參數,待提交完成后 parent.setVisible(false) 隱藏 np。
我們在前面提到了當點擊了 columns 對象中的某一行將自動為 np 對象填入值,而這個時候 np 是隱藏的,不錯,需要增加一個按鈕來顯示 np 以便進行修改:
新增和修改的操作差不多,而刪除則傳入用戶 ID, peopleoperate.setQueryParam('action','delPeople') 直接刪除。
到此,頁面代碼也完成得差不多了,啟動您的服務器。運行效果如圖五:?????????????????
???????????? 圖五: openlaszloWITHdb4o 應用 程序
Tips
在開發 OpenLaszlo 過程中會遇到不少意想不到的問題,最常見的是提交數據后 debugger 打印出“data conversion error……”信息,檢查程序后未發現任何異常,很讓人困惑,其實你只需在服務器端返回一個簡單的 XML 即可,哪怕是“<ok/>”標簽。
您的應用程序在運行時是可以隨時點擊鼠標右鍵查看源代碼的,如圖六:
???????????????????????????????????????????????????????????????
??????????????? ? 圖六:程序屬性
這對安全構成了威脅,特別是部署后實際運行的應用。OpenLaszlo 為我們考慮到了這點,只需修改被部署的應用的“WEB-INF\lps\config\lps.properties”文件,把“allowRequestSOURCE”參數設置為“false”即可。
提高 OpenLaszlo 開發效率原則:
1.??? 只做自己擅長的事
OpenLaszlo 只擅長做表現層,不要奢求在界面上處理一些復雜邏輯,這些要交給后臺處理。也不要用它來做類似于博客等以文字為主的應用,體現不出優勢,反而體現出劣勢。
2.??? 做好自己該做的事
類似于客戶端數據校驗的功能,前臺該做好的,就不要交給后臺去判斷數據是否有效,前臺每次提交都要保證自己是沒有問題的,和后臺的結合應該是無障礙的。
3.??? 不要過分追求效果
不要為了 RIA 而 RIA,界面的過多動態效果,只會讓用戶眼花繚亂,而且加大了代碼量,增加了性能問題,損害了用戶體驗,只有簡單中有一點新奇、靜中有動才是境界。
提高 OpenLaszlo 運行效率原則:
1.??? dataset 的請求(request)屬性永遠是 false
只有在事件中才進行 doRequest()。
2.??? 盡量少用約束符${}來指定視圖間的位置關系
比如:<view name="myview" x="${parent.b.x}" .../> 因為程序在初始化過程中要評估表達式的值,而且生成約束 constraints,這樣對性能產生很大影響。
3.??? 需要數據來生成視圖內容的組件盡量不用嚴格數據綁定
這樣做是為了減少程序編譯時間合減少服務期端傳送過來的文件尺寸。正確的做法是將組件的 datapath 置為空,即定義組件的 datapath="",而在用戶事件或者在 datapointer 中的 ondata 事件中用 component.datapath.setPointer(this.p) 中進行運行時綁定。
4.??? 通過事件給組件填充數據
combobox 、list 等組件的數據只在需要時填充,通過打開窗口或者點擊按鈕的事件來為這些組件填充內容。只要填充了一次,下次如果數據沒變,就不會再重復生成視圖。
5.??? 使用 initstage="defer"
OpenLaszlo 官方文檔推薦使用 initstage="defer" 來阻止某些不是馬上用到的視圖的生成,而在需要的時候才 complete,但筆者試過,不太好掌握時機,而且會讓程序變的互相耦合,效果不甚理想。
6.??? 使用 dataoption="lazy"
list 組件有一個很有用的屬性:dataoption="lazy",這個屬性可以使列表中的數據更新起來非常迅速,尤其是在大數據量的情況下。它的原理在于把部分視圖進行了緩存,而不是全部銷毀和新建,可惜 tree 組件對于這個屬性不管用。
7.??? 盡量降低視圖的層次
盡可能的減少視圖的嵌套層次,能直接放在 canvas 下,就不要放在其他視圖里面,而且window 和 modaldailog 應該嚴格的直接放在 canvas 里面。這樣做是為了加快程序的渲染速度,并且簡化編程時路徑引用,方便查看代碼結構。
8.??? 盡量少用 state/animator
用這些會增加內存占用和代碼量,而用 node 的 animate 方法卻可以輕松的控制視圖運動,而且沒有額外的內存占用。
9.??? 控制單個 canvas 文件的代碼規模
一個 canvas 文件代碼行數最好控制在 1000 行以內,其中 include 進來的方法、資源、自定義組件的代碼數目不算在內,這樣做是為了獲得較快的界面初始化時間,和方便的代碼維護。
10. 盡量采用 SOLO 方式部署
如果不需要 RPC 等特殊功能,建議采用 SOLO 方式部署應用,將已經寫好的 OpenLaszlo應用編譯成 swf 文件,在 html 文件中包裹,這樣可避免用戶第一次請求 lzx 文件的長時間等待,也避免了服務期重起后重新編譯 lzx 文件,還可以減少 OpenLaszlo Server 部署時對服務器空間的占用(盡管才十幾兆)。
11. 模塊化應用
將具有獨立功能的應用模塊單獨分離出來,即用一個包含 canvas 的 lzx 文件配合其他組件來實現一個小模塊,各個模塊之間導航用 loadURL() 來完成,各個模塊共享 Session 以及后臺數據,實現模塊之間的通訊。
結論
通過上面的介紹,想必您已經體會到 OpenLaszlo 與 db4o 結合起來做程序是多么的有趣。如果在不借助第三方框架,用傳統的 JSP+JavaScript 與 JDBC 技術開發,相信其代碼量會遠遠超過前者。OpenLaszlo與 db4o 的設計目標都是為節約開發周期而努力,相信他們也做到了,剩下的就看開發者是如何利用其優勢的,您準備好了嗎?
關于作者
Rosen Jiang 來自成都,是 db4o 和 OO 的忠實 fans,RIA 倡導者,是 2007 年 db4o 的 dvp 獲得者之一。你可以通過 rosener_722@hotmail.com 和他聯系。
Lwz7512 來自北京 ,是一名 OpenLaszlo Developer,他 是 OpenLaszlo 在中國的User Group負責人,也是Openlaszlo中國開發者社區創辦者, 你可以通過 rabbit69@126.com 和他聯系。
參考資料
資源
OpenLaszlo 官方網站 http://www.openlaszlo.org/
db4o 官方網站 http://www.db4o.com/
Openlaszlo 中國開發者社區 http://www.openria.cn/
中國RIA開發者論壇 http://www.riachina.com/
學習
Rich Internet Applications 的技術選項: http://www-128.ibm.com/developerworks/cn/web/wa-richiapp/index.html
Rich Internet Applications and AJAX - Selecting the best product
http://www.javalobby.org/articles/ajax-ria-overview
Introducing OpenLaszlo
http://www.xml.com/pub/a/2006/10/11/introducing-open-laszlo.html
OpenLaszlo 文檔中心
http://www.openlaszlo.org/documentation
在 OpenLaszlo 應用程序中使用 Apache Derby,第 1 部分:使用 Derby 提供數據
http://www-128.ibm.com/developerworks/cn/views/opensource/tutorials.jsp?cv_doc_id=110109
在 OpenLaszlo 應用程序中使用 Apache Derby,第 2 部分:存儲和嵌入數據
http://www-128.ibm.com/developerworks/cn/views/opensource/tutorials.jsp?cv_doc_id=110110
使用 OpenLaszlo 創建 Web 富客戶端
http://www-128.ibm.com/developerworks/cn/xml/wa-openlaszlo/index.html
Openlaszlo 開發規范
http://www.openria.cn/posts/list/49.page
面向對象數據庫 db4o 之旅,第 1 部分:初識 db4o http://www-128.ibm.com/developerworks/cn/java/j-lo-db4o1
面向對象數據庫 db4o 之旅,第 2 部分:db4o 查詢方式http://www-128.ibm.com/developerworks/cn/java/j-lo-db4o2
《程序員》雜志版權所有!引用、轉貼本文應注明本文來自《程序員》雜志。
Powered by: BlogJava Copyright © Rosen