你不知道的Java秘密
51CTO給各位讀者講過《Java的8大技術(shù)優(yōu)勢(shì)》,很多開發(fā)者覺得自己懂Java編程,事實(shí)是大多數(shù)開發(fā)人員都只領(lǐng)會(huì)到了Java平臺(tái)的皮毛,所學(xué)也只夠應(yīng)付工作。作者將深度挖掘Java平臺(tái)的核心功能,揭示一些鮮為人知的事實(shí),幫助您解決最棘手的編程困難。
當(dāng)應(yīng)用程序性能受到損害時(shí),大多數(shù)開發(fā)人員都驚慌失措,這在情理之中。跟蹤Java應(yīng)用程序瓶頸來源一直以來都是很麻煩的,因?yàn)镴ava虛擬機(jī)有黑盒效應(yīng),而且Java平臺(tái)分析工具一貫就有缺陷。
然而,隨著Java5中JConsole的引入,一切都發(fā)生了改變。JConsole是一個(gè)內(nèi)置Java性能分析器,可以從命令行或在GUIshell中運(yùn)行。它不是***的,但是當(dāng)尖頭老板來問你關(guān)于性能的問題時(shí),用它來應(yīng)對(duì)還是綽綽有余的——這比查詢PapaGoogle要好得多。
我們將向您展示5個(gè)方法,使您可以輕松地使用JConsole(或者,它更高端的“近親”VisualVM)來監(jiān)控Java應(yīng)用程序性能和跟蹤Java中的代碼。
1.JDK附帶分析器
許多開發(fā)人員沒有意識(shí)到從Java 5開始JDK中包含了一個(gè)分析器。JConsole(或者Java平臺(tái)***版本,VisualVM)是一個(gè)內(nèi)置分析器,它同Java編譯器一樣容易啟動(dòng)。如果是從命令行啟動(dòng),使JDK在PATH上,運(yùn)行jconsole即可。如果從GUIshell啟動(dòng),找到JDK安裝路徑,打開bin文件夾,雙擊jconsole。
當(dāng)分析工具彈出時(shí)(取決于正在運(yùn)行的Java版本以及正在運(yùn)行的Java程序數(shù)量),可能會(huì)出現(xiàn)一個(gè)對(duì)話框,要求輸入一個(gè)進(jìn)程的URL來連接,也可能列出許多不同的本地Java進(jìn)程(有時(shí)包含JConsole進(jìn)程本身)來連接。
使用JConsole進(jìn)行工作
在Java 5中,Java進(jìn)程并不是被設(shè)置為默認(rèn)分析的,而是通過一個(gè)命令行參數(shù)—-Dcom.sun.management.jmxremote——在啟動(dòng)時(shí)告訴Java 5 VM打開連接,以便分析器可以找到它們;當(dāng)進(jìn)程被JConsole撿起時(shí),您只能雙擊它開始分析。
分析器有自己的開銷,因此***的辦法就是花點(diǎn)時(shí)間來弄清是什么開銷。發(fā)現(xiàn)JConsole開銷最簡(jiǎn)單的辦法是,首先獨(dú)自運(yùn)行一個(gè)應(yīng)用程序,然后在分析器下運(yùn)行,并測(cè)量差異。(應(yīng)用程序不能太大或者太??;我最喜歡使用JDK附帶的SwingSet2樣本。)因此,我使用-verbose:gc嘗試運(yùn)行SwingSet2來查看垃圾收集清理,然后運(yùn)行同一個(gè)應(yīng)用程序并將JConsole分析器連接到它。當(dāng)JConsole連接好了之后,一個(gè)穩(wěn)定的GC清理流出現(xiàn),否則不會(huì)出現(xiàn)。這就是分析器的性能開銷。
JConsole或VisualVM?
JConsole從Java 5開始就隨著Java平臺(tái)版本一起發(fā)布,而VisualVM是在NetBeans基礎(chǔ)上升級(jí)的一個(gè)分析器,在Java 6的更新版12中***次發(fā)布。多數(shù)還沒有更新到Java 6,因此這篇文章主要介紹JConsole。然而,多數(shù)技巧和這兩個(gè)分析器都有關(guān)。
2.遠(yuǎn)程連接進(jìn)程
因?yàn)閃eb應(yīng)用程序分析工具假設(shè)通過一個(gè)套接字進(jìn)行連通性分析,您只需要進(jìn)行少許配置來設(shè)置JConsole(或者是基于JVMTI的分析器,就這點(diǎn)而言),監(jiān)控/分析遠(yuǎn)程運(yùn)行的應(yīng)用程序。
如果Tomcat運(yùn)行在一個(gè)名為“webserve”的機(jī)器上,且JVM已經(jīng)啟動(dòng)了JMX并監(jiān)聽端口9004,從JConsole(或者任何JMX客戶端)連接它需要一個(gè)JMX URL“service:jmx:rmi:///jndi/rmi://webserver:9004/jmxrmi”。
基本上,要分析一個(gè)運(yùn)行在遠(yuǎn)程數(shù)據(jù)中心的應(yīng)用程序服務(wù)器,您所需要的僅僅是一個(gè)JMX URL。
3.跟蹤統(tǒng)計(jì)
JConsole有許多對(duì)收集統(tǒng)計(jì)數(shù)據(jù)有用的選項(xiàng)卡,包括:
◆Memory:在JVM垃圾收集器中針對(duì)各個(gè)堆跟蹤活動(dòng)。
◆Threads:在目標(biāo)JVM中檢查當(dāng)前線程活動(dòng)。
◆Classes:觀察VM已加載類的總數(shù)。
這些選項(xiàng)卡(和相關(guān)的圖表)都是由每個(gè)Java 5及更高版本VM在JMX服務(wù)器上注冊(cè)的JMX對(duì)象提供的,是內(nèi)置到JVM的。一個(gè)給定JVM中可用bean的完整清單在MBeans選項(xiàng)卡上列出,包括一些元數(shù)據(jù)和一個(gè)有限的用戶界面來查看數(shù)據(jù)或執(zhí)行操作。(然而,注冊(cè)通知是在JConsole用戶界面之外。)
使用統(tǒng)計(jì)數(shù)據(jù)
假設(shè)一個(gè)Tomcat進(jìn)程死于OutOfMemoryError。如果您想要弄清楚發(fā)生了什么,打開JConsole,單擊Classes選項(xiàng)卡,過一段時(shí)間查看一次類計(jì)數(shù)。如果數(shù)量穩(wěn)定上升,您可以假設(shè)應(yīng)用程序服務(wù)器或者您的代碼某個(gè)地方有一個(gè)ClassLoader漏洞,不久之后將耗盡PermGen空間。如果需要更進(jìn)一步的確認(rèn)問題,請(qǐng)看Memory選項(xiàng)卡。
不要成為典型
發(fā)現(xiàn)應(yīng)用程序代碼中性能問題的常用響應(yīng)多種多樣,但也是可預(yù)測(cè)的。早期的Java編程人員對(duì)舊的IDE可能十分生氣,并開始進(jìn)行代碼庫(kù)中主要部分的代碼復(fù)查,在源代碼中尋找熟悉的“紅色標(biāo)志”,像異步塊、對(duì)象配額等等。隨著編程經(jīng)驗(yàn)的增加,開發(fā)人員可能會(huì)仔細(xì)研究JVM支持的-X標(biāo)志,尋找優(yōu)化垃圾收集器的方法。當(dāng)然,對(duì)于新手,直接去Google查詢,希望有其他人發(fā)現(xiàn)了JVM的神奇的“make it go fast”轉(zhuǎn)換,避免重寫代碼。
從本質(zhì)上來說,這些方法沒什么錯(cuò),但都是有風(fēng)險(xiǎn)的。對(duì)于一個(gè)性能問題最有效的響應(yīng)就是使用一個(gè)分析器——現(xiàn)在它們內(nèi)置在Java平臺(tái),我們確實(shí)沒有理由不這樣做!
4.為離線分析創(chuàng)建一個(gè)堆轉(zhuǎn)儲(chǔ)
生產(chǎn)環(huán)境中一切都在快速地進(jìn)行著,您可能沒有時(shí)間花費(fèi)在您的應(yīng)用程序分析器上,相反地,您可以為Java環(huán)境中的每個(gè)事件照一個(gè)快照保存下來過后再看。在JConsole中您也可以這樣做,在VisualVM中甚至?xí)龅酶谩?br /> 先找到MBeans選項(xiàng)卡,在其中打開com.sun.management節(jié)點(diǎn),接著是HotSpotDiagnostic節(jié)點(diǎn)?,F(xiàn)在,選擇Operations,注意右邊面板中的“dumpHeap”按鈕。如果您在***個(gè)(“字符串”)輸入框中向dumpHeap傳遞一個(gè)文件名來轉(zhuǎn)儲(chǔ),它將為整個(gè)JVM堆照一個(gè)快照,并將其轉(zhuǎn)儲(chǔ)到那個(gè)文件。
稍后,您可以使用各種不同的商業(yè)分析器來分析文件,或者使用VisualVM分析快照。(記住,VisualVM是在Java 6中可用的,且是單獨(dú)下載的。)
5.JConsole并不是高深莫測(cè)的
作為一個(gè)分析器實(shí)用工具,JConsole是極好的,但是還有更好的工具。一些分析插件附帶分析器或者靈巧的用戶界面,默認(rèn)情況下比JConsole跟蹤更多的數(shù)據(jù)。
JConsole真正吸引人的是整個(gè)程序是用“普通舊式Java”編寫的,這意味著任何Java開發(fā)人員都可以編寫這樣一個(gè)實(shí)用工具。事實(shí)上,JDK其中甚至包括如何通過創(chuàng)建一個(gè)插件來定制JConsole的示例。建立在NetBeans頂部的VisualVM進(jìn)一步延伸了插件概念。
如果JConsole(或者VisualVM,或者其他任何工具)不符合您的需求,或者不能跟蹤您想要跟蹤的,或者不能按照您的方式跟蹤,您可以編寫屬于自己的工具。如果您覺得Java代碼很麻煩,Groovy或JRuby或很多其他JVM語(yǔ)言都可以幫助您更快完成。
您真正需要的是一個(gè)快速而粗糙(quick-and-dirty)的由JVM連接的命令行工具,可以以您想要的方式確切地跟蹤您感興趣的數(shù)據(jù)。
#p#
5個(gè)命令行分析工具
全功能內(nèi)置分析器,如JConsole和VisualVM的成本有時(shí)比它們的性能費(fèi)用還要高—尤其是在生產(chǎn)軟件上運(yùn)行的系統(tǒng)中。因此,在聚焦Java性能監(jiān)控的第2篇中,我將介紹5個(gè)命令行分析工具,使開發(fā)人員僅關(guān)注運(yùn)行的Java進(jìn)程的一個(gè)方面。
JDK包括很多命令行實(shí)用程序,可以用于監(jiān)控和管理Java應(yīng)用程序性能。雖然大多數(shù)這類應(yīng)用程序都被標(biāo)注為“實(shí)驗(yàn)型”,在技術(shù)上不受支持,但是它們很有用。
1.jps(sun.tools.jps)
很多命令行工具都要求您識(shí)別您希望監(jiān)控的Java進(jìn)程。這與監(jiān)控本地操作系統(tǒng)進(jìn)程、同樣需要一個(gè)程序識(shí)別器的同類工具沒有太大區(qū)別。
“VMID”識(shí)別器與本地操作系統(tǒng)進(jìn)程識(shí)別器(“pid”)并不總是相同的,這就是我們需要JDKjps實(shí)用程序的原因。
在Java進(jìn)程中使用jps
與配置JDK的大部分工具及本文中提及的所有工具一樣,可執(zhí)行jps通常是一個(gè)圍繞Java類或執(zhí)行大多數(shù)工作的類集的一個(gè)薄包裝。在Windows®環(huán)境下,這些工具是.exe文件,使用JNIInvocationAPI直接調(diào)用上面提及的類;在UNIX®環(huán)境下,大多數(shù)工具是一個(gè)shell腳本的符號(hào)鏈接,該腳本采用指定的正確類名稱開始一個(gè)普通啟動(dòng)程序。如果您希望在Java進(jìn)程中使用jps(或者任何其他工具)的功能—Ant腳本—僅在每個(gè)工具的“主”類上調(diào)用main()相對(duì)容易。為了簡(jiǎn)化引用,類名稱出現(xiàn)在每個(gè)工具名稱之后的括號(hào)內(nèi)。
jps—名稱反映了在大多數(shù)UNIX系統(tǒng)上發(fā)現(xiàn)的ps實(shí)用程序—告訴我們運(yùn)行Java應(yīng)用程序的JVMID。顧名思義,jps返回指定機(jī)器上運(yùn)行的所有已發(fā)現(xiàn)的Java進(jìn)程的VMID。如果jps沒有發(fā)現(xiàn)進(jìn)程,并不意味著無(wú)法附加或研究Java進(jìn)程,而只是意味著它并未宣傳自己的可用性。
如果發(fā)現(xiàn)Java進(jìn)程,jps將列出啟用它的命令行。這種區(qū)分Java進(jìn)程的方法非常重要,因?yàn)橹灰婕安僮飨到y(tǒng),所有的Java進(jìn)程都被統(tǒng)稱為“java”。在大多數(shù)情況下,VMID是值得注意的重要數(shù)字。
使用分析器開始
使用分析實(shí)用程序開始的最簡(jiǎn)單方法是使用一個(gè)如在demo/jfc/SwingSet2中發(fā)現(xiàn)的SwingSet2演示一樣的演示程序。這樣就可以避免程序作為背景/監(jiān)控程序運(yùn)行時(shí)出現(xiàn)掛起的可能性。當(dāng)您了解工具及其費(fèi)用后,就可以在實(shí)際程序中進(jìn)行試用。
加載演示應(yīng)用程序后,運(yùn)行jps并注意返回的vmid。為了獲得更好的效果,采用-Dcom.sun.management.jmxremote屬性集啟動(dòng)Java進(jìn)程。如果沒有使用該設(shè)置,部分下列工具收集的部分?jǐn)?shù)據(jù)可能不可用。
2.jstat(sun.tools.jstat)
jstat實(shí)用程序可以用于收集各種各樣不同的統(tǒng)計(jì)數(shù)據(jù)。jstat統(tǒng)計(jì)數(shù)據(jù)被分類到“選項(xiàng)”中,這些選項(xiàng)在命令行中被指定作為***參數(shù)。對(duì)于JDK 1.6來說,您可以通過采用命令-options運(yùn)行jstat查看可用的選項(xiàng)清單。清單1中顯示了部分選項(xiàng):
清單1.jstat選項(xiàng)
- -class
- -compiler
- -gc
- -gccapacity
- -gccause
- -gcnew
- -gcnewcapacity
- -gcold
- -gcoldcapacity
- -gcpermcapacity
- -gcutil
- -printcompilation
實(shí)用程序的JDK記錄將告訴您清單1中每個(gè)選項(xiàng)返回的內(nèi)容,但是其中大多數(shù)用于收集垃圾的收集器或者其部件的性能信息。-class選項(xiàng)顯示了加載及未加載的類(使其成為檢測(cè)應(yīng)用程序服務(wù)器或代碼中ClassLoader泄露的重要實(shí)用程序,且-compiler和-printcompilation都顯示了有關(guān)Hotspot JIT編譯程序的信息。
默認(rèn)情況下,jstat在您核對(duì)信息時(shí)顯示信息。如果您希望每隔一定時(shí)間拍攝快照,請(qǐng)?jiān)?options指令后以毫秒為單位指定間隔時(shí)間。jstat將持續(xù)顯示監(jiān)控進(jìn)程信息的快照。如果您希望jstat在終止前進(jìn)行特定數(shù)量的快照,在間隔時(shí)間/時(shí)間值后指定該數(shù)字。
如果5756是幾分鐘前開始的運(yùn)行SwingSet2程序的VMID,那么下列命令將告訴jstat每250毫秒為10個(gè)佚代執(zhí)行一次gc快照轉(zhuǎn)儲(chǔ),然后停止:
- jstat -gc 5756 250 10
請(qǐng)注意Sun(現(xiàn)在的Oracle)保留了在不進(jìn)行任何預(yù)先通知的情況下更改各種選項(xiàng)的輸出甚至是選項(xiàng)本身的權(quán)利。這是使用不受支持實(shí)用程序的缺點(diǎn)。請(qǐng)參看Javadocs了解jstat輸出中每一列的全部細(xì)節(jié)。
3.jstack(sun.tools.jstack)
了解Java進(jìn)程及其對(duì)應(yīng)的執(zhí)行線程內(nèi)部發(fā)生的情況是一種常見的診斷挑戰(zhàn)。例如,當(dāng)一個(gè)應(yīng)用程序突然停止進(jìn)程時(shí),很明顯出現(xiàn)了資源耗盡,但是僅通過查看代碼無(wú)法明確知道何處出現(xiàn)資源耗盡,且為什么會(huì)發(fā)生。
jstack是一個(gè)可以返回在應(yīng)用程序上運(yùn)行的各種各樣線程的一個(gè)完整轉(zhuǎn)儲(chǔ)的實(shí)用程序,您可以使用它查明問題。
采用期望進(jìn)程的VMID運(yùn)行jstack會(huì)產(chǎn)生一個(gè)堆轉(zhuǎn)儲(chǔ)。就這一點(diǎn)而言,jstack與在控制臺(tái)窗口內(nèi)按Ctrl-Break鍵起同樣的作用,在控制臺(tái)窗口中,Java進(jìn)程正在運(yùn)行或調(diào)用VM內(nèi)每個(gè)Thread對(duì)象上的Thread.getAllStackTraces()或Thread.dumpStack()。jstack調(diào)用也轉(zhuǎn)儲(chǔ)關(guān)于在VM內(nèi)運(yùn)行的非Java線程的信息,這些線程作為Thread對(duì)象并不總是可用的。
jstack的-l參數(shù)提供了一個(gè)較長(zhǎng)的轉(zhuǎn)儲(chǔ),包括關(guān)于每個(gè)Java線程持有鎖的更多詳細(xì)信息,因此發(fā)現(xiàn)(和squash)死鎖或可伸縮性bug是極其重要的。
4.jmap(sun.tools.jmap)
有時(shí),您正在處理的問題是一個(gè)對(duì)象泄露,如一個(gè)ArrayList(可能持有成千上萬(wàn)個(gè)對(duì)象)該釋放時(shí)沒有釋放。另一個(gè)更普遍的問題是,看似從不會(huì)壓縮的擴(kuò)展堆,卻有活躍的垃圾收集。
當(dāng)您努力尋找一個(gè)對(duì)象泄露時(shí),在指定時(shí)刻對(duì)堆及時(shí)進(jìn)行拍照,然后審查其中內(nèi)容非常有用。jmap通過對(duì)堆拍攝快照來提供該功能的***部分。然后您可以采用下一部分中描述的jhat實(shí)用程序分析堆數(shù)據(jù)。
與這里描述的其他所有實(shí)用程序一樣,使用jmap非常簡(jiǎn)單。將jmap指向您希望拍快照的Java進(jìn)程的VMID,然后給予它部分參數(shù),用來描述產(chǎn)生的結(jié)果文件。您要傳遞給jmap的選項(xiàng)包括轉(zhuǎn)儲(chǔ)文件的名稱以及是否使用一個(gè)文本文件或二進(jìn)制文件。二進(jìn)制文件是最有用的選項(xiàng),但是只有當(dāng)與某一種索引工具結(jié)合使用時(shí)—通過十六進(jìn)制值的文本手動(dòng)操作數(shù)百兆字節(jié)不是***的方法。
隨意看一下Java堆的更多信息,jmap同樣支持-histo選項(xiàng)。-histo產(chǎn)生一個(gè)對(duì)象文本柱狀圖,現(xiàn)在在堆中大量引用,由特定類型消耗的字節(jié)總數(shù)分類。它同樣給出了特定類型的總示例數(shù)量,支持部分原始計(jì)算,并猜測(cè)每個(gè)實(shí)例的相對(duì)成本。
不幸的是,jmap沒有像jstat一樣的period-and-max-count選項(xiàng),但是將jmap(或jmap.main())調(diào)用放入shell腳本或其他類的循環(huán),周期性地拍攝快照相對(duì)簡(jiǎn)單。(事實(shí)上,這是加入jmap的一個(gè)好的擴(kuò)展,不管是作為OpenJDK本身的源補(bǔ)丁,還是作為其他實(shí)用程序的擴(kuò)展。)
5.jhat(com.sun.tools.hat.Main)
將堆轉(zhuǎn)儲(chǔ)至一個(gè)二進(jìn)制文件后,您就可以使用jhat分析二進(jìn)制堆轉(zhuǎn)儲(chǔ)文件。jhat創(chuàng)建一個(gè)HTTP/HTML服務(wù)器,該服務(wù)器可以在瀏覽器中被瀏覽,提供一個(gè)關(guān)于堆的object-by-object視圖,及時(shí)凍結(jié)。根據(jù)對(duì)象引用草率處理堆可能會(huì)非??尚?,您可以通過對(duì)總體混亂進(jìn)行某種自動(dòng)分析而獲得更好的服務(wù)。幸運(yùn)的是,jhat支持OQL語(yǔ)法進(jìn)行這樣的分析。
例如,對(duì)所有含有超過100個(gè)字符的String運(yùn)行OQL查詢看起來如下:
- select s from java.lang.String s where s.count >= 100
結(jié)果作為對(duì)象鏈接顯示,然后展示該對(duì)象的完整內(nèi)容,字段引用作為可以解除引用的其他鏈接的其他對(duì)象。OQL查詢同樣可以調(diào)用對(duì)象的方法,將正則表達(dá)式作為查詢的一部分,并使用內(nèi)置查詢工具。一種查詢工具,referrers()函數(shù),顯示了引用指定類型對(duì)象的所有引用。下面是尋找所有參考File對(duì)象的查詢:
- select referrers(f) from java.io.File f
您可以查找OQL的完整語(yǔ)法及其在jhat瀏覽器環(huán)境內(nèi)“OQL Help”頁(yè)面上的特性。將jhat與OQL相結(jié)合是對(duì)行為不當(dāng)?shù)亩堰M(jìn)行對(duì)象調(diào)查的有效方法。
結(jié)束語(yǔ)
當(dāng)您需要近距離觀察Java進(jìn)程內(nèi)發(fā)生的事情時(shí),JDK的分析擴(kuò)展會(huì)非常有用。本文中介紹的所有工具都可以從命令行中由其自己使用。它們還可以與JConsole或VisualVM有力地結(jié)合使用。JConsole和VisualVM提供Java虛擬機(jī)的總體視圖,jstat和jmap等有針對(duì)性的工具支持您對(duì)研究進(jìn)行微調(diào)。
#p#
Java 平臺(tái)上更簡(jiǎn)單的腳本編寫方法
現(xiàn)在,許多 Java 開發(fā)人員都喜歡在 Java 平臺(tái)中使用腳本語(yǔ)言,但是使用編譯到 Java 字節(jié)碼中的動(dòng)態(tài)語(yǔ)言有時(shí)是不可行的。在某些情況中,直接編寫一個(gè) Java 應(yīng)用程序的腳本 部分 或者在一個(gè)腳本中調(diào)用特定的 Java 對(duì)象是更快捷、更高效的方法。
這就是 javax.script 產(chǎn)生的原因了。Java Scripting API 是從 Java 6 開始引入的,它填補(bǔ)了便捷的小腳本語(yǔ)言和健壯的 Java 生態(tài)系統(tǒng)之間的鴻溝。通過使用 Java Scripting API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語(yǔ)言,這使您能夠在解決一些很小的問題時(shí)有更多可選擇的方法。
1. 使用 jrunscript 執(zhí)行 JavaScript
每一個(gè)新的 Java 平臺(tái)發(fā)布都會(huì)帶來新的命令行工具集,它們位于 JDK 的 bin 目錄。Java 6 也一樣,其中 jrunscript 便是 Java 平臺(tái)工具集中的一個(gè)不小的補(bǔ)充。
設(shè)想一個(gè)編寫命令行腳本進(jìn)行性能監(jiān)控的簡(jiǎn)單問題。這個(gè)工具將借用 jmap(見本系列文章 前一篇文章 中的介紹),每 5 秒鐘運(yùn)行一個(gè) Java 進(jìn)程,從而了解進(jìn)程的運(yùn)行狀況。一般情況下,我們會(huì)使用命令行 shell 腳本來完成這樣的工作,但是這里的服務(wù)器應(yīng)用程序部署在一些差別很大的平臺(tái)上,包括 Windows® 和 Linux®。系統(tǒng)管理員將會(huì)發(fā)現(xiàn)編寫能夠同時(shí)運(yùn)行在兩個(gè)平臺(tái)的 shell 腳本是很痛苦的。通常的做法是編寫一個(gè) Windows 批處理文件和一個(gè) UNIX® shell 腳本,同時(shí)保證這兩個(gè)文件同步更新。
但是,任何閱讀過 The Pragmatic Programmer 的人都知道,這嚴(yán)重違反了 DRY (Don't Repeat Yourself) 原則,而且會(huì)產(chǎn)生許多缺陷和問題。我們真正希望的是編寫一種與操作系統(tǒng)無(wú)關(guān)的腳本,它能夠在所有的平臺(tái)上運(yùn)行。
當(dāng)然,Java 語(yǔ)言是平臺(tái)無(wú)關(guān)的,但是這里并不是需要使用 “系統(tǒng)” 語(yǔ)言的情況。我們需要的是一種腳本語(yǔ)言 — 如,JavaScript。
清單 1 顯示的是我們所需要的簡(jiǎn)單 shell 腳本:
清單 1. periodic.js
- while (true)
- {
- echo("Hello, world!");
- }
由于經(jīng)常與 Web 瀏覽器打交道,許多 Java 開發(fā)人員已經(jīng)知道了 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 開發(fā)的一種 ECMAScript 語(yǔ)言)。問題是,系統(tǒng)管理員要如何運(yùn)行這個(gè)腳本?
當(dāng)然,解決方法是 JDK 所帶的 jrunscript 實(shí)用程序,如清單 2 所示:
清單 2. jrunscript
- C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- Hello, world!
- ...
注意,您也可以使用 for 循環(huán)按照指定的次數(shù)來循環(huán)執(zhí)行這個(gè)腳本,然后才退出?;旧?,jrunscript 能夠讓您執(zhí)行 JavaScript 的所有操作。惟一不同的是它的運(yùn)行環(huán)境不是瀏覽器,所以運(yùn)行中不會(huì)有 DOM。因此,最頂層的函數(shù)和對(duì)象稍微有些不同。
因?yàn)?Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript 可以執(zhí)行任何傳遞給它的 ECMAScript 代碼,不管是一個(gè)文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環(huán)境。運(yùn)行 jrunscript 就可以訪問 REPL shell。
2. 從腳本訪問 Java 對(duì)象
能夠編寫 JavaScript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們?cè)?Java 語(yǔ)言中使用的所有代碼 — 這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪問整個(gè) Java 生態(tài)系統(tǒng),因?yàn)楸举|(zhì)上一切代碼都還是 Java 字節(jié)碼。所以,回到我們之前的問題,我們可以在 Java 平臺(tái)上使用傳統(tǒng)的 Runtime.exec() 調(diào)用來啟動(dòng)進(jìn)程,如清單 3 所示:
清單 3. Runtime.exec() 啟動(dòng) jmap
- var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
- p.waitFor()
數(shù)組 arguments 是指向傳遞到這個(gè)函數(shù)參數(shù)的 ECMAScript 標(biāo)準(zhǔn)內(nèi)置引用。在最頂層的腳本環(huán)境中,則是傳遞給腳本本身的的參數(shù)數(shù)組(命令行參數(shù))。所以,在清單 3 中,這個(gè)腳本預(yù)期接收一個(gè)參數(shù),該參數(shù)包含要映射的 Java 進(jìn)程的 VMID。
除此之外,我們可以利用本身為一個(gè) Java 類的 jmap,然后直接調(diào)用它的 main() 方法,如清單 4 所示。有了這個(gè)方法,我們不需要 “傳輸” Process 對(duì)象的 in/out/err 流。
清單 4. JMap.main()
- var args = [ "-histo", arguments[0] ]
- Packages.sun.tools.jmap.JMap.main(args)
Packages 語(yǔ)法是一個(gè) Rhino ECMAScript 標(biāo)識(shí),它指向已經(jīng) Rhino 內(nèi)創(chuàng)建的位于核心 java.* 包之外的 Java 包。
3. 從 Java 代碼調(diào)用腳本
從腳本調(diào)用 Java 對(duì)象僅僅完成了一半的工作:Java 腳本環(huán)境也提供了從 Java 代碼調(diào)用腳本的功能。這只需要實(shí)例化一個(gè) ScriptEngine 對(duì)象,然后加載和評(píng)估腳本,如清單 5 所示:
清單 5. Java 平臺(tái)的腳本調(diào)用
- import java.io.*;
- import javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("javascript");
- for (String arg : args)
- {
- FileReader fr = new FileReader(arg);
- engine.eval(fr);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
eval() 方法也可以直接操作一個(gè) String,所以這個(gè)腳本不一定必須是文件系統(tǒng)的一個(gè)文件 — 它可以來自于數(shù)據(jù)庫(kù)、用戶輸入,或者甚至可以基于環(huán)境和用戶操作在應(yīng)用程序中生成。
4. 將 Java 對(duì)象綁定到腳本空間
僅僅調(diào)用一個(gè)腳本還不夠:腳本通常會(huì)與 Java 環(huán)境中創(chuàng)建的對(duì)象進(jìn)行交互。這時(shí),Java 主機(jī)環(huán)境必須創(chuàng)建一些對(duì)象并將它們綁定,這樣腳本就可以很容易找到和使用這些對(duì)象。這個(gè)過程是 ScriptContext 對(duì)象的任務(wù),如清單 6 所示:
清單 6. 為腳本綁定對(duì)象
- import java.io.*;
- import javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("javascript");
- for (String arg : args)
- {
- Bindings bindings = new SimpleBindings();
- bindings.put("author", new Person("Ted", "Neward", 39));
- bindings.put("title", "5 Things You Didn't Know");
- FileReader fr = new FileReader(arg);
- engine.eval(fr, bindings);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
訪問所綁定的對(duì)象很簡(jiǎn)單 — 所綁定對(duì)象的名稱是作為全局命名空間引入到腳本的,所以在 Rhino 中使用 Person 很簡(jiǎn)單,如清單 7 所示:
清單 7.
- println("Hello from inside scripting!")
- println("author.firstName = " + author.firstName)
您可以看到,JavaBeans 樣式的屬性被簡(jiǎn)化為使用名稱直接訪問,這就好像它們是字段一樣。
5. 編譯頻繁使用的腳本
腳本語(yǔ)言的缺點(diǎn)一直存在于性能方面。其中的原因是,大多數(shù)情況下腳本語(yǔ)言是 “即時(shí)” 解譯的,因而它在執(zhí)行時(shí)會(huì)損失一些解析和驗(yàn)證文本的時(shí)間和 CPU 周期。運(yùn)行在 JVM 的許多腳本語(yǔ)言最終會(huì)將接收的代碼轉(zhuǎn)換為 Java 字節(jié)碼,至少在腳本被***次解析和驗(yàn)證時(shí)進(jìn)行轉(zhuǎn)換;在 Java 程序關(guān)閉時(shí),這些即時(shí)編譯的代碼會(huì)消失。將頻繁使用的腳本保持為字節(jié)碼形式可以幫助提升可觀的性能。
我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實(shí)現(xiàn)了 Compilable 接口,那么這個(gè)接口所編譯的方法可用于將腳本(以一個(gè) String 或一個(gè) Reader 傳遞過來的)編譯為一個(gè) CompiledScript 實(shí)例,然后它可用于在 eval() 方法中使用不同的綁定重復(fù)地處理編譯后的代碼,如清單 8 所示:
清單 8. 編譯解譯后的代碼
- import java.io.*;
- import javax.script.*;
- public class App
- {
- public static void main(String[] args)
- {
- try
- {
- ScriptEngine engine =
- new ScriptEngineManager().getEngineByName("javascript");
- for (String arg : args)
- {
- Bindings bindings = new SimpleBindings();
- bindings.put("author", new Person("Ted", "Neward", 39));
- bindings.put("title", "5 Things You Didn't Know");
- FileReader fr = new FileReader(arg);
- if (engine instanceof Compilable)
- {
- System.out.println("Compiling....");
- Compilable compEngine = (Compilable)engine;
- CompiledScript cs = compEngine.compile(fr);
- cs.eval(bindings);
- }
- else
- engine.eval(fr, bindings);
- }
- }
- catch(IOException ioEx)
- {
- ioEx.printStackTrace();
- }
- catch(ScriptException scrEx)
- {
- scrEx.printStackTrace();
- }
- }
- }
在大多數(shù)情況中,CompiledScript 實(shí)例需要存儲(chǔ)在一個(gè)長(zhǎng)時(shí)間存儲(chǔ)中(例如,servlet-context),這樣才能避免一次次地重復(fù)編譯相同的腳本。然而,如果腳本發(fā)生變化,您就需要?jiǎng)?chuàng)建一個(gè)新的 CompiledScript 來反映這個(gè)變化;一旦編譯完成,CompiledScript 就不再執(zhí)行原始的腳本文件內(nèi)容。
結(jié)束語(yǔ)
Java Scripting API 在擴(kuò)展 Java 程序的范圍和功能方面前進(jìn)了很大一步,并且它將腳本語(yǔ)言的編碼效率的優(yōu)勢(shì)帶到 Java 環(huán)境。jrunscript — 它顯然不是很難編寫的程序 — 以及 javax.script 給 Java 開發(fā)人員帶來了諸如 Ruby (JRuby) 和 ECMAScript (Rhino) 等腳本語(yǔ)言的優(yōu)勢(shì),同時(shí)還不會(huì)破壞 Java 環(huán)境的生態(tài)系統(tǒng)和可擴(kuò)展性。
關(guān)于作者
Ted Neward是Neward&Associates的總裁,從事關(guān)于Java、.NET、XML Services以及其他平臺(tái)方面的咨詢、指導(dǎo)和演示等工作。他居住在華盛頓西雅圖。
【編輯推薦】
























