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


2025-03-19 08:40:00




