你不知道的5個(gè)Java有用的工具
編者按:本文為 Neward & Associates 的主管Ted Neward 撰寫的5件你不知道的事情系列的最后一篇(至少目前是這樣)。Ted Neward 在Neward & Associates負(fù)責(zé)有關(guān) Java、.NET、XML 服務(wù)和其他平臺的咨詢、指導(dǎo)、培訓(xùn)和推介。他現(xiàn)在居住在華盛頓州西雅圖附近。以下為本文的譯文:
很多年前,當(dāng)我還是高中生的時(shí)候,我曾考慮以小說作家作為我的職業(yè)追求,我訂閱了一本 Writer's Digest 雜志。我記得其中有篇專欄文章,是關(guān)于 “太小而難以保存的線頭”,專欄作者描述廚房儲物抽屜中放滿了無法分類的玩意兒。這句話我一直銘記在心,它正好用來描述本文的內(nèi)容,本Java 平臺就充滿了這樣的 “線頭” — 有用的命令行工具和庫,大多數(shù) Java 開發(fā)人員甚至都不知道,更別提使用了。其中很多無法劃分到之前的 5 件事 系列 的編程分類中,但不管怎樣,嘗試一下:有些說不定會在您編程的廚房抽屜中占得一席之地。
1. StAX
在千禧年左右,當(dāng) XML 第一次出現(xiàn)在很多 Java 開發(fā)人員面前時(shí),有兩種基本的解析 XML 文件的方法。SAX 解析器實(shí)際是由程序員對事件調(diào)用一系列回調(diào)方法的大型狀態(tài)機(jī)。DOM 解析器將整個(gè) XML 文檔加入內(nèi)存,并切割成離散的對象,它們連接在一起形成一個(gè)樹。該樹描述了文檔的整個(gè) XML Infoset 表示法。這兩個(gè)解析器都有缺點(diǎn):SAX 太低級,無法使用,DOM 代價(jià)太大,尤其對于大的 XML 文件 — 整個(gè)樹成了一個(gè)龐然大物。
幸運(yùn)的是,Java 開發(fā)人員找到第三種方法來解析 XML 文件,通過對文檔建模成 “節(jié)點(diǎn)”,它們可以從文檔流中一次取出一個(gè),檢查,然后處理或丟棄。這些 “節(jié)點(diǎn)” 的 “流” 提供了 SAX 和 DOM 的中間地帶,名為 “Streaming API for XML”,或者叫做StAX。(此縮寫用于區(qū)分新的 API 與原來的 SAX 解析器,它與此同名。)StAX 解析器后來包裝到了 JDK 中,在 javax.xml.stream 包。
使用 StAX 相當(dāng)簡單:實(shí)例化 XMLEventReader,將它指向一個(gè)格式良好的 XML 文件,然后一次 “拉出” 一個(gè)節(jié)點(diǎn)(通常用 while 循環(huán)),查看。例如,在清單 1 中,列舉出了 Ant 構(gòu)造腳本中的所有目標(biāo):
清單 1. 只是讓 StAX 指向目標(biāo)
- import java.io.*;
- import javax.xml.namespace.QName;
- import javax.xml.stream.*;
- import javax.xml.stream.events.*;
- import javax.xml.stream.util.*;
- public class Targets
- {
- public static void main(String[] args)
- throws Exception
- {
- for (String arg : args)
- {
- XMLEventReader xsr =
- XMLInputFactory.newInstance()
- .createXMLEventReader(new FileReader(arg));
- while (xsr.hasNext())
- {
- XMLEvent evt = xsr.nextEvent();
- switch (evt.getEventType())
- {
- case XMLEvent.START_ELEMENT:
- {
- StartElement se = evt.asStartElement();
- if (se.getName().getLocalPart().equals("target"))
- {
- Attribute targetName =
- se.getAttributeByName(new QName("name"));
- // Found a target!
- System.out.println(targetName.getValue());
- }
- break;
- }
- // Ignore everything else
- }
- }
- }
- }
- }
StAX 解析器不會替換所有的 SAX 和 DOM 代碼。但肯定會讓某些任務(wù)容易些。尤其對完成不需要知道 XML 文檔整個(gè)樹結(jié)構(gòu)的任務(wù)相當(dāng)方便。
請注意,如果事件對象級別太高,無法使用,StAX 也有一個(gè)低級 API 在 XMLStreamReader 中。盡管也許沒有閱讀器有用,StAX 還有一個(gè) XMLEventWriter,同樣,還有一個(gè) XMLStreamWriter 類用于 XML 輸出。
#p#
2. ServiceLoader
Java 開發(fā)人員經(jīng)常希望將使用和創(chuàng)建組件的內(nèi)容區(qū)分開來。這通常是通過創(chuàng)建一個(gè)描述組件動(dòng)作的接口,并使用某種中介創(chuàng)建組件實(shí)例來完成的。很多開發(fā)人員使用 Spring 框架來完成,但還有其他的方法,它比 Spring 容器更輕量級。
java.util 的 ServiceLoader 類能讀取隱藏在 JAR 文件中的配置文件,并找到接口的實(shí)現(xiàn),然后使這些實(shí)現(xiàn)成為可選擇的列表。例如,如果您需要一個(gè)私仆(personal-servant)組件來完成任務(wù),您可以使用清單 2 中的代碼來實(shí)現(xiàn):
清單 2. IPersonalServant
- public interface IPersonalServant
- {
- // Process a file of commands to the servant
- public void process(java.io.File f)
- throws java.io.IOException;
- public boolean can(String command);
- }
can() 方法可讓您確定所提供的私仆實(shí)現(xiàn)是否滿足需求。清單 3 中的 ServiceLoader 的 IPersonalServant 列表基本上滿足需求:
清單 3. IPersonalServant 行嗎?
- import java.io.*;
- import java.util.*;
- public class Servant
- {
- public static void main(String[] args)
- throws IOException
- {
- ServiceLoader<IPersonalServant> servantLoader =
- ServiceLoader.load(IPersonalServant.class);
- IPersonalServant i = null;
- for (IPersonalServant ii : servantLoader)
- if (ii.can("fetch tea"))
- i = ii;
- if (i == null)
- throw new IllegalArgumentException("No suitable servant found");
- for (String arg : args)
- {
- i.process(new File(arg));
- }
- }
- }
假設(shè)有此接口的實(shí)現(xiàn),如清單 4:
清單 4. Jeeves 實(shí)現(xiàn)了 IPersonalServant
- import java.io.*;
- public class Jeeves
- implements IPersonalServant
- {
- public void process(File f)
- {
- System.out.println("Very good, sir.");
- }
- public boolean can(String cmd)
- {
- if (cmd.equals("fetch tea"))
- return true;
- else
- return false;
- }
- }
剩下的就是配置包含實(shí)現(xiàn)的 JAR 文件,讓 ServiceLoader 能識別 — 這可能會非常棘手。JDK 想要 JAR 文件有一個(gè) META-INF/services 目錄,它包含一個(gè)文本文件,其文件名與接口類名完全匹配 — 本例中是 META-INF/services/IPersonalServant。接口類名的內(nèi)容是實(shí)現(xiàn)的名稱,每行一個(gè),如清單 5:
清單 5. META-INF/services/IPersonalServant
- Jeeves # comments are OK
幸運(yùn)的是,Ant 構(gòu)建系統(tǒng)(自 1.7.0 以來)包含一個(gè)對 jar 任務(wù)的服務(wù)標(biāo)簽,讓這相對容易,見清單 6:
清單 6. Ant 構(gòu)建的 IPersonalServant
- <target name="serviceloader" depends="build">
- <jar destfile="misc.jar" basedir="./classes">
- <service type="IPersonalServant">
- <provider classname="Jeeves" />
- </service>
- </jar>
- </target>
這里,很容易調(diào)用 IPersonalServant,讓它執(zhí)行命令。然而,解析和執(zhí)行這些命令可能會非常棘手。這又是另一個(gè) “小線頭”。
#p#
3. Scanner
有無數(shù) Java 工具能幫助您構(gòu)建解析器,很多函數(shù)語言已成功構(gòu)建解析器函數(shù)庫(解析器選擇器)。但如果要解析的是逗號分隔值文件,或空格分隔文本文件,又怎么辦呢?大多數(shù)工具用在此處就過于隆重了,而 String.split() 又不夠。(對于正則表達(dá)式,請記住一句老話:“ 您有一個(gè)問題,用正則表達(dá)式解決。那您就有兩個(gè)問題了。”)
Java 平臺的 Scanner 類會是這些類中您最好的選擇。以輕量級文本解析器為目標(biāo),Scanner 提供了一個(gè)相對簡單的 API,用于提取結(jié)構(gòu)化文本,并放入強(qiáng)類型的部分。想象一下,如果您愿意,一組類似 DSL 的命令(源自 Terry Pratchett Discworld 小說)排列在文本文件中,如清單 7:
清單 7. Igor 的任務(wù)
- fetch 1 head
- fetch 3 eye
- fetch 1 foot
- attach foot to head
- attach eye to head
- admire
您,或者是本例中稱為 Igor的私仆,能輕松使用 Scanner 解析這組違法命令,如清單 8 所示:
清單 8. Igor 的任務(wù),由 Scanner 解析
- import java.io.*;
- import java.util.*;
- public class Igor
- implements IPersonalServant
- {
- public boolean can(String cmd)
- {
- if (cmd.equals("fetch body parts"))
- return true;
- if (cmd.equals("attach body parts"))
- return true;
- else
- return false;
- }
- public void process(File commandFile)
- throws FileNotFoundException
- {
- Scanner scanner = new Scanner(commandFile);
- // Commands come in a verb/number/noun or verb form
- while (scanner.hasNext())
- {
- String verb = scanner.next();
- if (verb.equals("fetch"))
- {
- int num = scanner.nextInt();
- String type = scanner.next();
- fetch (num, type);
- }
- else if (verb.equals("attach"))
- {
- String item = scanner.next();
- String to = scanner.next();
- String target = scanner.next();
- attach(item, target);
- }
- else if (verb.equals("admire"))
- {
- admire();
- }
- else
- {
- System.out.println("I don't know how to "
- + verb + ", marthter.");
- }
- }
- }
- public void fetch(int number, String type)
- {
- if (parts.get(type) == null)
- {
- System.out.println("Fetching " + number + " "
- + type + (number > 1 ? "s" : "") + ", marthter!");
- parts.put(type, number);
- }
- else
- {
- System.out.println("Fetching " + number + " more "
- + type + (number > 1 ? "s" : "") + ", marthter!");
- Integer currentTotal = parts.get(type);
- parts.put(type, currentTotal + number);
- }
- System.out.println("We now have " + parts.toString());
- }
- public void attach(String item, String target)
- {
- System.out.println("Attaching the " + item + " to the " +
- target + ", marthter!");
- }
- public void admire()
- {
- System.out.println("It'th quite the creathion, marthter");
- }
- private Map<String, Integer> parts = new HashMap<String, Integer>();
- }
假設(shè) Igor 已在 ServantLoader 中注冊,可以很方便地將 can() 調(diào)用改得更實(shí)用,并重用前面的 Servant 代碼,如清單 9 所示:
清單 9. Igor 做了什么
- import java.io.*;
- import java.util.*;
- public class Servant
- {
- public static void main(String[] args)
- throws IOException
- {
- ServiceLoader<IPersonalServant> servantLoader =
- ServiceLoader.load(IPersonalServant.class);
- IPersonalServant i = null;
- for (IPersonalServant ii : servantLoader)
- if (ii.can("fetch body parts"))
- i = ii;
- if (i == null)
- throw new IllegalArgumentException("No suitable servant found");
- for (String arg : args)
- {
- i.process(new File(arg));
- }
- }
- }
真正 DSL 實(shí)現(xiàn)顯然不會僅僅打印到標(biāo)準(zhǔn)輸出流。我把追蹤哪些部分、跟隨哪些部分的細(xì)節(jié)留待給您(當(dāng)然,還有忠誠的 Igor)。
#p#
4. Timer
java.util.Timer 和 TimerTask 類提供了方便、相對簡單的方法可在定期或一次性延遲的基礎(chǔ)上執(zhí)行任務(wù):
清單 10. 稍后執(zhí)行
- import java.util.*;
- public class Later
- {
- public static void main(String[] args)
- {
- Timer t = new Timer("TimerThread");
- t.schedule(new TimerTask() {
- public void run() {
- System.out.println("This is later");
- System.exit(0);
- }
- }, 1 * 1000);
- System.out.println("Exiting main()");
- }
- }
Timer 有許多 schedule() 重載,它們提示某一任務(wù)是一次性還是重復(fù)的,并且有一個(gè)啟動(dòng)的 TimerTask 實(shí)例。TimerTask 實(shí)際上是一個(gè) Runnable(事實(shí)上,它實(shí)現(xiàn)了它),但還有另外兩個(gè)方法:cancel() 用來取消任務(wù),scheduledExecutionTime() 用來返回任務(wù)何時(shí)啟動(dòng)的近似值。
請注意 Timer 卻創(chuàng)建了一個(gè)非守護(hù)線程在后臺啟動(dòng)任務(wù),因此在清單 10 中我需要調(diào)用 System.exit() 來取消任務(wù)。在長時(shí)間運(yùn)行的程序中,最好創(chuàng)建一個(gè) Timer 守護(hù)線程(使用帶有指示守護(hù)線程狀態(tài)的參數(shù)的構(gòu)造函數(shù)),從而它不會讓 VM 活動(dòng)。
這個(gè)類沒什么神奇的,但它確實(shí)能幫助我們對后臺啟動(dòng)的程序的目的了解得更清楚。它還能節(jié)省一些 Thread 代碼,并作為輕量級 ScheduledExecutorService(對于還沒準(zhǔn)備好了解整個(gè) java.util.concurrent 包的人來說)。
#p#
5. JavaSound
盡管在服務(wù)器端應(yīng)用程序中不常出現(xiàn),但 sound 對管理員有著有用的 “被動(dòng)” 意義 — 它是惡作劇的好材料。盡管它很晚才出現(xiàn)在 Java 平臺中,JavaSound API 最終還是加入了核心運(yùn)行時(shí)庫,封裝在 javax.sound * 包 — 其中一個(gè)包是 MIDI 文件,另一個(gè)是音頻文件示例(如普遍的 .WAV 文件格式)。
JavaSound 的 “hello world” 是播放一個(gè)片段,如清單 11 所示:
清單 11. 再放一遍,Sam
- public static void playClip(String audioFile)
- {
- try
- {
- AudioInputStream inputStream =
- AudioSystem.getAudioInputStream(
- this.getClass().getResourceAsStream(audioFile));
- DataLine.Info info =
- new DataLine.Info( Clip.class, audioInputStream.getFormat() );
- Clip clip = (Clip) AudioSystem.getLine(info);
- clip.addLineListener(new LineListener() {
- public void update(LineEvent e) {
- if (e.getType() == LineEvent.Type.STOP) {
- synchronized(clip) {
- clip.notify();
- }
- }
- }
- });
- clip.open(audioInputStream);
- clip.setFramePosition(0);
- clip.start();
- synchronized (clip) {
- clip.wait();
- }
- clip.drain();
- clip.close();
- }
- catch (Exception ex)
- {
- ex.printStackTrace();
- }
- }
大多數(shù)還是相當(dāng)簡單(至少 JavaSound 一樣簡單)。第一步是創(chuàng)建一個(gè)文件的 AudioInputStream 來播放。為了讓此方法盡量與上下文無關(guān),我們從加載類的 ClassLoader 中抓取文件作為 InputStream。(AudioSystem 還需要一個(gè) File 或 String,如果提前知道聲音文件的具體路徑。)一旦完成, DataLine.Info 對象就提供給 AudioSystem,得到一個(gè) Clip,這是播放音頻片段最簡單的方法。(其他方法提供了對片段更多的控制 — 例如獲取一個(gè) SourceDataLine — 但對于 “播放” 來說,過于復(fù)雜)。
這里應(yīng)該和對 AudioInputStream 調(diào)用 open() 一樣簡單。(“應(yīng)該” 的意思是如果您沒遇到下節(jié)描述的錯(cuò)誤。)調(diào)用 start() 開始播放,drain() 等待播放完成,close() 釋放音頻線路。播放是在單獨(dú)的線程進(jìn)行,因此調(diào)用 stop() 將會停止播放,然后調(diào)用 start() 將會從播放暫停的地方重新開始;使用 setFramePosition(0) 重新定位到開始。
沒聲音?
JDK 5 發(fā)行版中有個(gè)討厭的小錯(cuò)誤:在有些平臺上,對于一些短的音頻片段,代碼看上去運(yùn)行正常,但就是 ... 沒聲音。顯然媒體播放器在應(yīng)該出現(xiàn)的位置之前觸發(fā)了 STOP 事件。(見 參考資料 一節(jié)中錯(cuò)誤頁的鏈接。)
這個(gè)錯(cuò)誤 “無法修復(fù)”,但解決方法相當(dāng)簡單:注冊一個(gè) LineListener 來監(jiān)聽 STOP 事件,當(dāng)觸發(fā)時(shí),調(diào)用片段對象的 notifyAll()。然后在 “調(diào)用者” 代碼中,通過調(diào)用 wait() 等待片段完成(還調(diào)用 notifyAll())。在沒出現(xiàn)錯(cuò)誤的平臺上,這些錯(cuò)誤是多余的,在 Windows® 及有些 Linux® 版本上,會讓程序員 “開心” 或 “憤怒”。
結(jié)束語
現(xiàn)在您都了解了,廚房里的工具。我知道很多人已清楚了解我此處介紹的工具,而我的職業(yè)經(jīng)驗(yàn)告訴我,很多人將從這篇介紹文章,或者說是對長期遺忘在凌亂的抽屜中的小工具的提示中受益。
我在此系列中做個(gè)簡短的中斷,讓別人能加入分享他們各自領(lǐng)域的專業(yè)經(jīng)驗(yàn)。但別擔(dān)心,我還會回來的,無論是本系列還是探索其他領(lǐng)域的新的 5 件事。在那之前,我鼓勵(lì)您一直探索 Java 平臺,去發(fā)現(xiàn)那些能讓編程更高效的寶石。
【編輯推薦】