To read and update - create and manipulate - an XML document, you will need an XML parser.
Microsoft's XML parser is a COM component that comes with Internet Explorer 5 and higher. Once you have installed Internet Explorer, the parser is available to scripts.
Microsoft's XML parser supports all the necessary functions to traverse the node tree, access the nodes and their attribute values, insert and delete nodes, and convert the node tree back to XML.
The following table lists the most commonly used node types supported by Microsoft's XML parser:
Node Type | Example |
---|---|
Processing instruction | <?xml version="1.0"?> |
Element | <drink type="beer">Carlsberg</drink> |
Attribute | type="beer" |
Text | Carlsberg |
MSXML Parser 2.5 is the XML parser that is shipped with Windows 2000 and IE 5.5.
MSXML Parser 3.0 is the XML parser that is shipped with IE 6.0 and Windows XP.
The MSXML 3.0 parser features:
To create an instance of Microsoft's XML parser with JavaScript, use the following code:
var xmlDoc=new ActiveXObject("Microsoft.XMLDOM") |
To create an instance of Microsoft's XML parser with VBScript, use the following code:
set xmlDoc=CreateObject("Microsoft.XMLDOM") |
To create an instance of Microsoft's XML parser in an ASP page (using VBScript), use the following code:
set xmlDoc=Server.CreateObject("Microsoft.XMLDOM") |
The following code loads an existing XML document ("note.xml") into Microsoft's XML parser:
<script type="text/javascript"> var xmlDoc=new ActiveXObject("Microsoft.XMLDOM") xmlDoc.async="false" xmlDoc.load("note.xml") ... ... ... </script> |
The first line of the script above creates an instance of the Microsoft XML parser. The third line tells the parser to load an XML document called "note.xml". The second line turns off asynchronized loading, to make sure that the parser will not continue execution of the script before the document is fully loaded.
Plain XML documents are displayed in a tree-like structure in Mozilla (just like IE).
Mozilla also supports parsing of XML data using JavaScript. The parsed data can be displayed as HTML.
To create an instance of the XML parser with JavaScript in Mozilla browsers, use the following code:
var xmlDoc=document.implementation.createDocument("ns","root",null) |
The first parameter, ns, defines the namespace used for the XML document. The second parameter, root, is the XML root element in the XML file. The third parameter, null, is always null because it is not implemented yet.
The following code loads an existing XML document ("note.xml") into Mozillas' XML parser:
<script type="text/javascript"> var xmlDoc=document.implementation.createDocument("","",null); xmlDoc.load("note.xml"); ... ... ... </script> |
The first line of the script above creates an instance of the XML parser. The second line tells the parser to load an XML document called "note.xml".
The following example is a cross browser example that loads an existing XML document ("note.xml") into the XML parser:
<html> <head> <script type="text/javascript"> var xmlDoc function loadXML() { //load xml file // code for IE if (window.ActiveXObject) { xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async=false; xmlDoc.load("note.xml"); getmessage() } // code for Mozilla, etc. else if (document.implementation && document.implementation.createDocument) { xmlDoc= document.implementation.createDocument("","",null); xmlDoc.load("note.xml"); xmlDoc.onload=getmessage } else { alert('Your browser cannot handle this script'); } } function getmessage() { document.getElementById("to").innerHTML= xmlDoc.getElementsByTagName("to")[0].firstChild.nodeValue document.getElementById("from").innerHTML= xmlDoc.getElementsByTagName("from")[0].firstChild.nodeValue document.getElementById("message").innerHTML= xmlDoc.getElementsByTagName("body")[0].firstChild.nodeValue } </script> </head> <body onload="loadXML()" bgcolor="yellow"> <h1>W3Schools Internal Note</h1> <p><b>To:</b> <span id="to"></span><br /> <b>From:</b> <span id="from"></span> <hr /> <b>Message:</b> <span id="message"></span> </p> </body> </html> |
Try it yourself
Internet Explorer supports two ways of loading XML into a document object: the load() method and the loadXML() method. The load() method loads an XML file and the loadXML() method loads a text string that contains XML code.
The following code loads a text string into Microsoft's XML parser:
<script type="text/javascript"> var txt="<note>" txt=txt+"<to>Tove</to><from>Jani</from>" txt=txt+"<heading>Reminder</heading>" txt=txt+"<body>Don't forget me this weekend!</body>" txt=txt+"</note>" var xmlDoc=new ActiveXObject("Microsoft.XMLDOM") xmlDoc.async="false" xmlDoc.loadXML(txt) ... ... ... </script> |
If you have Internet Explorer, you can try it yourself.
?模糊測試(Fuzz testing )是一項對代碼質量有著深遠影響的簡單技術。在本文中,Elliotte Rusty Harold 故意將隨機的壞數據插入應用程序,以觀察發生的結果。他也解釋了如何使用如校驗和、XML 數據存儲及代碼驗證等防護性編碼技術,來加固您的程序以抵制隨機數據。他以一個練習進行總結,在練習中他以一個代碼破壞者的角度進行思考 —— 這是一種用于防護代碼的至關重要的技術。???? 多年來,我驚嘆于有如此大量能夠使 Microsoft Word 崩潰的壞文件。少數字節錯位,會使整個應用程序毀于一旦。在舊式的、無內存保護的操作系統中,整個計算機通常就這樣宕掉了。Word 為什么不能意識到它接收到了壞的數據,并發出一條錯誤信息呢?為什么它會僅僅因為少數字節被損壞就破壞自己的棧、堆呢?當然,Word 并不是惟一一個面對畸形文件時表現得如此糟糕的程序。
??? 本文介紹了一種試圖避免這種災難的技術。在模糊測試中,用隨機壞數據(也稱做 fuzz)攻擊一個程序,然后等著觀察哪里遭到了破壞。模糊測試的技巧在于,它是不符合邏輯的:自動模糊測試不去猜測哪個數據會導致破壞(就像人工測試員那樣),而是將盡可能多的雜亂數據投入程序中。由這個測試驗證過的失敗模式通常對程序員來說是個徹底的震憾,因為任何按邏輯思考的人都不會想到這種失敗。
??? 模糊測試是一項簡單的技術,但它卻能揭示出程序中的重要 bug。它能夠驗證出現實世界中的錯誤模式并在您的軟件發貨前對潛在的應當被堵塞的攻擊渠道進行提示。
模糊測試如何運行
模糊測試的實現是一個非常簡單的過程:
??? 可以用任意多種方式改變該隨機數據。例如,可以將整個文件打亂,而不是僅替換其中的一部分,也可以將該文件限制為 ASCII 文本或非零字節。不管用什么方式進行分割,關鍵是將大量隨機數據放入應用程序并觀察出故障的是什么。
![]() |
|
??? 可以手動進行初始化測試,但要想達到最佳的效果則確實需要采用自動化模糊測試。在這種情況下,當面臨破壞輸入時首先需要為應用程序定義適當的錯誤行為。(如果當輸入數據被破壞時,您發現程序正常運行,且未定義發生的事件,那么這就是第一個 bug。)隨后將隨機數據傳遞到程序中直到找到了一個文件,該文件不會觸發適當的錯誤對話框、消息、異常,等等。存儲并記錄該文件,這樣就能在稍后重現該問題。如此重復。
??? 盡管模糊測試通常需要一些手動編碼,但還有一些工具能提供幫助。例如,清單 1 顯示了一個簡單的 Java? 類,該類隨機更改文件的特定長度。我常愿意在開始的幾個字節后面啟動模糊測試,因為程序似乎更可能注意到早期的錯誤而不是后面的錯誤。(您的目的是想找到程序未檢測到的錯誤,而不是尋找已經檢測到的。)
import java.io.*; import java.security.SecureRandom; import java.util.Random; public class Fuzzer { private Random random = new SecureRandom(); private int count = 1; public File fuzz(File in, int start, int length) throws IOException { byte[] data = new byte[(int) in.length()]; DataInputStream din = new DataInputStream(new FileInputStream(in)); din.readFully(data); fuzz(data, start, length); String name = "fuzz_" + count + "_" + in.getName(); File fout = new File(name); FileOutputStream out = new FileOutputStream(fout); out.write(data); out.close(); din.close(); count++; return fout; } // Modifies byte array in place public void fuzz(byte[] in, int start, int length) { byte[] fuzz = new byte[length]; random.nextBytes(fuzz); System.arraycopy(fuzz, 0, in, start, fuzz.length); } } |
![]() |
|
??? 模糊測試文件很簡單。將其傳至應用程序通常不那么困難。如 AppleScript 或 Perl 腳本語言通常是編寫模糊測試的最佳選擇。對于 GUI 程序,最困難的部分是辨認出應用程序是否檢測出正確的故障模式。有時,最簡單的方法是讓一個人坐在程序前將每一個測試通過或失敗的結果都標記下來。一定要將所有生成的隨機測試用例單獨地命名并保存下來,這樣就能夠重現這個過程中檢測到的任何故障。
防護性編碼
??? 可靠的編碼遵循了這樣的基本原則:絕不會讓程序中插入未經過一致性及合理性驗證的外部數據。
??? 如果從文件中讀入一個數字并期望其為正數,那么,在使用其進行進一步處理前對其先驗證一下。如果期望字符串只包含 ASCII 字母,請確定它確實是這樣。如果認為文件包含一個四字節的整數倍的數據,請驗證一下。一定不要假設任何外部提供的數據中的字符都會如您所料。
??? 最常見的錯誤是做出這樣的假設:因為程序將該數據寫出,該程序就能不用驗證再一次將該數據讀回去。這是很危險的!因為該數據很可能已經被另一個程序在磁盤上復寫過了。它也可能已經被一個故障磁盤或壞的網絡傳輸所破壞了或已經被另一個帶 bug 的程序更改過了。它甚至可能已經被故意更改過以破壞程序的安全性。所以不要假設任何事,要進行驗證。
??? 當然,錯誤處理及驗證十分令人生厭,也很不方便,并被全世界程序員們所輕視。計算機的誕生已進入了六十個年頭,我們仍舊沒有檢查基本的東西,如成功打開一個文件及內存分配是否成功。讓程序員們在閱讀一個文件時測試每一個字節和每一個不變量似乎是無望的 —— 但不這樣做就會使程序易被模糊攻擊。幸運的是,可以尋求幫助。恰當使用現代工具和技術能夠顯著減輕加固應用程序的痛苦,特別是如下三種技術更為突出:
用校驗和進行的模糊試驗
??? 能夠保護程序抵御模糊攻擊的最簡單的方法是將一個檢驗和添加到數據中。例如,可以將文件中所有的字節都累加起來,然后取其除以 256 的余數。將得到的值存儲到文件尾部的一個額外字節中。然后,在輸入數據前,先驗證檢驗和是否匹配。這項簡單模式將未被發現的意外故障的風險降低到約 1/256 。
??? 健壯的校驗和算法如 MD5 和 SHA 并不僅僅取其除以 256 的余數,它完成的要多得多。在 Java 語言中,java.security.DigestInputStream 和 java.security.DigestOutputStream 類為將一個校驗和附屬到數據中提供了便捷的方式。使用這些校驗和算法中的一種可以將程序遭受意外破壞的機率降低到少于十億分之一(盡管故意攻擊仍有可能)。
XML 存儲及驗證
?? 將數據以 XML 形式存儲是一種避免數據損壞的好方法。XML 最初即著力于 Web 頁面、書籍、詩歌、文章及相似文檔,它幾乎在每個領域都獲取了巨大的成功,從金融數據到矢量圖形到序列化對象等等。
![]() |
|
??? 使 XML 格式抵制模糊攻擊的關鍵特征是一個對輸入不做任何 假設的解析器。這就是真正想在一個健壯的文件格式中所獲得的。設計 XML 解析器是為了讓任何輸入(格式良好的或無格式的,有效的或無效的)都以定義好的形式處理。XML 解析器能夠處理任何 字節流。如果數據首先通過了 XML 解析器,則僅需要準備好接受解析器所能提供的東西。例如,不需要檢查數據是否包含空字符,因為 XML 解析器絕不會傳送一個空值。如果 XML 解析器在其輸入中看到一個空字符,它就會發出異常并停止處理。當然還需要處理這個異常,但編寫一個 catch 塊來處理檢測到的錯誤比起編寫代碼來檢測所有可能的錯誤來說要簡單得多。
??? 為使程序更加安全,可以用 DTD 和/或模式來驗證文檔。這不僅檢查了 XML 是否格式良好,而且至少與所預期更加接近。驗證并不會告知關于文檔所需了解的一切,但它卻使編寫大量簡單檢查變得很簡單。用 XML,很明顯能夠將所接受的文檔嚴格地限定為能夠處理的格式。
??? 盡管如此,還有多段代碼不能用 DTD 或模式進行驗證。例如,不能測試發票上商品的價格是否和數據庫中庫存商品的價格一致。當從客戶接收到一份包含價格的訂單文檔時,不論其是 XML 格式或是其他格式,在提交前通常都會檢查一下,以確保客戶并未修改價格。可以用定制代碼實現這些最后的檢查。
基于語法的格式
??? 使 XML 能夠對模糊攻擊具有如此的抵御能力的是其使用巴科斯-諾爾范式(Backus-Naur Form,BNF)語法仔細且標準地定義的格式。許多解析器都是使用如 JavaCC 或 Bison 等解析器-生成器工具直接從此語法中構建的。這種工具的實質是閱讀一個任意的輸入流并確定其是否符合此語法。
??? 如果 XML 并不適合于您的文件格式,您仍可以從基于解析器的解決方案的健壯性中獲益。您必須為文件格式自行編寫語法,隨后開發自己的解析器來閱讀它。相比使用唾手可得的 XML 解析器,開發自己的解析器需要更多的工作。然而它是一個更為健壯的解決方案,而不是不根據語法正式地進行驗證就將數據簡單地裝載到內存中。
Java 代碼驗證
??? 由模糊測試導致的許多故障都是內存分配錯誤及緩沖器溢出的結果。用一種安全的垃圾收集語言(在如 Java 或 managed C# 等虛擬機上執行的)來編寫應用程序避免了許多潛在問題。即使用 C 或 C++ 來編寫代碼,還是需要使用一個可靠的垃圾收集庫。在 2006 年,臺式機程序員或服務器程序員不應該還需要管理內存。
??? Java 運行時對其自身的代碼起到了額外保護層的作用。在將一個 .class 文件裝載到虛擬機之前,該文件要由一個字節符驗證器或一個可選的 SecurityManager 進行驗證。Java 并不假設創建 .class 文件的編譯器沒有 bug 且運轉正常。設計 Java 語言之初就是為了允許在一個安全沙箱中運行不信任的、潛在惡意的代碼。它甚至不信任其自身編譯過的代碼。畢竟,也許有人已經用十六進制編輯器手工修改了字節符,試圖觸發緩沖器溢出。我們大家都應該對我們的程序也有對輸入這樣的偏執。
以敵人的角度思考
??? 之前介紹的每項技術都在阻止意外破壞方面造詣頗深。將它們綜合起來恰當地實現,會將未被發現的非蓄意破壞發生的可能性幾乎減少到零。(當然,并不會減少到零,但其發生的可能性就如同一束偏離軌道的宇宙射線將您 CPU 運算 1+1 的結果變為 3 的可能性一樣微乎其微。)但不是所有的數據損壞都是非蓄意的。如果有人故意引入壞數據來破壞程序的安全性又該如何呢?以一個攻擊者的角度進行思考是防護代碼的下一個步驟。
???? 轉回到一個攻擊者的角度進行思考,假設要攻擊的應用程序是用 Java 編程語言編寫的、使用非本地代碼且將所有額外數據都以 XML(在接受前經過徹底驗證)形式存儲,還能成功攻擊嗎?是的,能。但用隨機改變文件字節的低級方法顯然不行。需要一種更為復雜的方法來說明程序自身的錯誤檢測機制及路徑。
??? 當測試一個抵御模糊攻擊的應用程序時,不可能做純黑盒測試,但通過一些明顯的修改,基本的想法還是可以應用的。例如,考慮校驗和,如果文件格式包含一個校驗和,在將文件傳至應用程序前僅僅修改此校驗和就可以使其同隨機數據相匹配。
??? 對于 XML,試著模糊單獨的元素內容和屬性值,而不是從文檔中挑選一部分隨機的字節進行替換。一定要用合法的 XML 字符替換數據,而不要用隨機字節,因為即使一百字節的隨機數據也一定是畸形的。也可以改變元素名稱和屬性名稱,只要細心地確保得到的文檔格式仍是正確的就可以了。如果該 XML 文檔是由一個限制非常嚴格的模式進行檢查的,還需要計算出該模式沒有 檢查什么,以決定在哪里進行有效的模糊。
??? 一個結合了對剩余數據進行代碼級驗證的真正嚴格的模式也許不會留下可操縱的空間。這就是作為一個開發人員所需要追求的。應用程序應能夠處理所發送的任何有意義的字節流,而不會因權利上( de jure ) 無效而拒絕。
結束語
??? 模糊測試能夠說明 bug 在程序中的出現。并不證明不存在這樣的 bug。而且,通過模糊測試會極大地提高您對應用程序的健壯性及抵御意外輸入的安全性的自信心。如果您用 24 小時對程序進行模糊測試而其依然無事,那么隨后同種類型的攻擊就不大可能再危及到它。(并不是不可能,提醒您,只是可能性很小。)如果模糊測試揭示出程序中的 bug,就應該進行修正,而不是當 bug 隨機出現時再對付它們。模糊測試通過明智地使用校驗和、XML、垃圾收集和/或基于語法的文件格式,更有效地從根本上加固了文件格式。
??? 模糊測試是一項用于驗證程序中真實錯誤的重要工具,也是所有意識到安全性問題且著力于程序健壯性的程序員們的工具箱中所必備的工具。
關于作者
![]() | ||
![]() | Elliotte Rusty Harold 來自新奧爾良, 現在他還定期回老家喝一碗美味的秋葵湯。不過目前,他和妻子 Beth 定居在紐約臨近布魯克林的 Prospect Heights,同住的還有他的貓咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)。他是 Polytechnic 大學計算機科學的副教授,他在該校講授 Java 和面向對象編程。他的 Web 站點 Cafe au Lait 已經成為 Internet 上最流行的獨立 Java 站點之一,它的姊妹站點 Cafe con Leche 已經成為最流行的 XML 站點之一。他的書包括 Effective XML、 Processing XML with Java、 Java Network Programming 和 The XML 1.1 Bible。他目前在從事處理 XML 的 XOM API、Jaxen XPath 引擎和 Jester 測試覆蓋率工具的開發工作。 |
XDoclet起步
XDoclet是一個代碼生成工具,它可以把你從Java開發過程中繁重的重復勞動中解脫出來。XDoclet可以讓你的應用系統開發的更加快速,而你只要付比原先更少的努力。你可以把你手頭上的冗長而又必需的代碼交給它幫你完成,你可以逃脫“deployment descriptor地獄”,你還可以使你的應用系統更加易于管理。而你所要做的,只不過是在你的注釋里,多加一些類javadoc屬性。然后,你會驚訝于XDoclet為了做到的一切。
討論XDoclet,有一點比較容易產生混淆,那就是XDoclet不但是一系統的代碼生成應用程序,而且它本身還是一個代碼生成框架。雖然每個應用系統的細節千變萬化(比如EJB代碼生成和Struts代碼生成是不一樣的,而JMX代碼生成又是另一番景象),但這些代碼生成的核心概念和用法卻是類似的。
在這一章里,我們將會看到滲透到所有XDoclet代碼生成程序當中的XDoclet框架基礎概念。但在之前,我們先從一個例子入手。
2.1 XDoclet in action
每一個程序員都會認識到,他們的程序永遠也不會完成。總會有另一些的功能需要添加,另一些的BUG需要修正,或者需要不斷的進行重構。所以,在代碼里添加注釋,提醒自己(或者其他的程序員)有哪些任務需要完成已成為一個共識。
如何來跟蹤這些任務是否完成了呢?理想情況下,你會收集整理出來一個TODO任務列表。在這方面,XDoclet提供了一個強大的TODO生成器,來幫助你完成這個任務。這是一個把XDoclet引入項目的好機會。
2.1.1 一個公共的任務
假設你正在開發一個使用了勺子的類。
public class Matrix {
// TODO – 需要處理當沒有勺子的情況
public void reload() {
// ...
Spoon spoon = getSpoon();
// ...
}
}
理想情況下,你在下一次閱讀這段代碼的時候,你會處理這個“空勺子”(null spoon)的問題。但如果你過了很久才回來看這段代碼,你還會記得在這個類里還有一些工作要做嗎?當然,你可以在你的源碼里全局搜索TODO,甚至你的集成開發環境有一個內建的TODO列表支持。但如果你想把任務所在的類和方法也標注出來的話,XDoclet可以是另一種選擇。XDoclet可以為你的項目生成一個TODO報表。
2.1.2 添加XDoclet標簽
為了把你的TODO項目轉換成另一種更加正式的格式,你需要對代碼進行一些細微的改動。如下所示:
public class Matrix {
/** @todo 需要處理當沒有勺子的情況 */
public void reload() {
// ...
}
}
這里加入了一個XDoclet需要的類javadoc標簽。XDoclet會使用這些標簽標記的信息,以及在這種情況下標簽所處的類和方法,來生成TODO報表。
2.1.3 與Ant集成
要生成TODO報表,你需要確保在你的機器上正確安裝了XDoclet。
在Ant任務里,最少要包含一個目標(例如init目標)定義<documentdoclet>任務,這是一個Ant自定義任務,例如:
<taskdef name=”documentdoclet”
classname=”xdoclet.modules.doc.DocumentDocletTask”
classname=”xdoclet.lib.path” />
這個<documentdoclet>任務是XDoclet核心代碼生成應用程序中的一個。
現在,你可以在Ant構建文件中加入一個todo目標調用這個任務來生成TODO報表,如:
<target name=”todo” depends=”init”>
<documentdoclet destdir=”todo”>
<fileset dir=”${dir.src}”>
<include name=”**/*.java” />
</fileset>
<info/>
</documentdoclet>
</target>
<info>子任務會遍歷你的源文件,查找todo標簽,并在todo子目錄下生成HTML格式的TODO報表。
2.1.4 創建一個更加職業化的TODO報表
XDoclet生成的TODO報表可以有一個更加職業化的外表。報表會列出一個概覽,顯示在哪個包哪個類里有todo項(以及todo項的個數)。Todo項可以跟在方法、類和域上,從報表上可以清楚的區別它們。類級別的todo項會標注class,方法級別的todo項會在方法簽名上標注M。構造函數和域相關的todo項也會進行相似的標注。
這個任務看起來很簡單,但考慮到你所需要做的只是在注釋上添加一些格式化的@todo標簽,相對于那種只有人才可以理解的無格式的松散的注釋,這種標簽是機器可讀的,也更容易編程處理。生成的輸出也更容易閱讀并且更加的商業化。
2.2 任務和子任務
生成todo報表,只是XDoclet可以完成的事情當中的冰山一角。當初,XDoclet因為可以自動生成EJB繁雜的接口和布署描述文件而聲名鵲起。然而,現在的XDoclet已經發展成了一個全功能的、面向屬性的代碼生成框架。J2EE代碼生成只是XDoclet的一個應用方面,它可以完成的任務已經遠遠超越了J2EE和項目文檔的生成。
2.2.1 XDoclet 任務
到現在為止,我們一直在討論使用XDoclet生成代碼,但事實上,更確切的說法應該是,我們使用XDoclet的一個特定的任務來生成代碼,比如<ejbdoclet>。每一個XDoclet任務關注于一個特定的領域,并提供這個領域的豐富的代碼生成工具。
[定義:任務(Tasks)是XDoclet里可用的代碼生成應用程序的高層概念。]
在XDoclet里,目前已有如下所示的七個核心任務。
<ejbdoclet>:面向EJB領域,生成EJB、工具類和布署描述符。
<webdoclet>:面向Web開發,生成serlvet、自定義標簽庫和web框架文件。
<hibernatedoclet>:Hibernate持續,配置文件、Mbeans
<jdodoclet>:JDO,元數據,vender configuration
<jmxdoclet>:JMX,MBean接口,mlets,配置文件。
<doclet>:使用用戶自定義模板來生成代碼。
<documentdoclet>:生成項目文件(例如todo列報表)
這其中,<ejbdoclet>最常用,并且很多項目也僅僅使用XDoclet來進行EJB代碼生成。<webdoclet>是其次一個常用的代碼生成任務。當然,在一個項目中同時使用幾個XDoclet任務是可能的(并且也是推薦的),但在這些任務之間是完全獨立的,它們彼此之間并不能進行直接的交流。
2.2.2 XDoclet子任務
XDoclet的任務是領域相關的,而在某個特定領域的XDoclet任務,又由許許多多緊密耦合在一起的子任務組成的,這些子任務每個都僅僅執行一個非常特定和簡單的代碼生成任務。
[定義:子任務(subtasks)是由任務提供的單目標的代碼生成過程]
任務提供子任務執行時的上下文,并且把這些相關的子任務組織管理了起來。任務會依賴這些子任務來生成代碼。在一個任務當中調用多個子任務來協同完成各種各樣比較大型的代碼生成任務是非常常見的。比如,在開發EJB時,你可能想要為每一個bean生成一個home接口,一個remote接口以及ejb-jar.xml布署描述符文件。這就是在<ejbdoclet>任務的上下文環境中的三個獨立的代碼生成子任務。
子任務可以隨意的組合排列,以滿足項目代碼生成的需要。某個XDoclet任務包含的子任務經常會共享功能和在源文件中使用相同的XDoclet標簽。這意味著當你開始一個任務的時候,你可以很容易的集成進一個相關的子任務,而不需要很大的改動。
子任務交互
讓我們以<ejbdoclet>任務為例,看一下相關的子任務之間是如何進行關聯的。假設你正在開發一個CMP(容器管理持久化)實體Bean。你想要使用一些<ejbdoclet>的子任務:
?<deploymentdescriptor>:生成ejb-jar.xml布署描述符文件。
?<localhomeinterface>:生成local home接口。
?<localinterface>:生成local接口。
在執行如上子任務的時候,你需要標記出你的實體Bean的CMP域。當你發布你的bean的時候,你還需要在開發商相關的布署描述符中提供某個特定的關系數據庫中的特定表和列與你的CMP實體Bean的映射關系。XDoclet可以讓你在原先已存在的CMP XDoclet屬性基礎上再加上一些關系映射屬性,然后,你就可以在任務中加入一個開發商相關的子任務(例如<jboss>或者<weblogic>)來生成布署描述符文件。XDoclet提供了幾乎所有的應用服務器的支持,你只需要一些初始化的小改動,就可以進行這些應用服務器相關的代碼生成了。
但那只是冰山一角。你還可以使用<entitycmp>子任務為為你的bean生成一個實體bean接口的實現子類。如果你使用<valueobject>子任務來為了你的bean生成值對象,<entityemp>子任務還會為你的值對象生成方法的實現代碼。
覺得不可思議了吧。可惜XDoclet沒有提供<cupofcoffee>子任務,要不然我們可以喝杯咖啡,休息一下啦。
這里不是想向你介紹<ejbdoclet>所有的子任務或者<ejbdoclet>可以完成的所有代碼生成功能,而僅僅是想向你展示一下任務的子任務之間是如何工作在一起的。一旦你開始并熟悉了一個XDoclet 子任務,熟悉另一個子任務會變得非常簡單- 那種每個子任務都是孤立的相比,使用這種可以相互協作的子任務,開發成本會顯著的降低,效果也更加的立竿見影。
2.3 使用Ant執行任務
XDoclet“嫁”給了Ant。XDoclet任務就是Ant的自定義任務,除此以外,沒有其他運行XDoclet任務的方法。所幸的是,Ant已經成為了Java構建工具事實上的標準,所以這不算什么限制。事實上,反過來,XDoclet與Ant的這種“親密”關系使得XDoclet可以參與到任何Ant構建過程當中去。
2.3.1 聲明任務
XDoclet并沒有和Ant一起發布,所以如果你想要使用XDoclet的話,就需要單獨的下載和安裝。在使用任何一個XDoclet的任務之前,你首先需要在使用Ant的<taskdef>任務來聲明它。例如:
<taskdef name=”ejbdoclet”
classname=”xdoclet.modules.ejb.EjbDocletTask”
classpathref=”xdoclet.lib.path”/>
如果你熟悉Ant的話,你就會知道這段代碼是告訴Ant加載<ejbdoclet>的任務定義。當然,你也可以以你喜歡的任何方式來命名這個自定義任務,但最好還是遵守標準的命名規律以免發生混淆。classname和classpathref屬性告訴Ant到哪里去找實現這個自定義任務的XDoclet類。如果你想使用其他的XDoclet任務,就必須要類似這樣首先聲明這個任務。
一般共通的做法是,把所有需要使用的XDoclet任務都放在Ant的一個目標里聲明,這樣在其他的目標里如果需要使用這些任務,只要depends這個任務就可以了。你可能已經在Ant的構建文件里包含了init目標,這就是放置XDoclet任務聲明的好地方(當然如果你沒有,你也可以建一個)。下面的例子就是在一個init目標里加入了<ejbdoclet>和<webdoclet>的聲明:
<target name=”init”>
<taskdef name=”documentdoclet”
classname=”xdoclet.modules.doc.DocumentDocletTask”
classpathref=”xdoclet.lib.path” />
<taskdef name=”ejbdoclet”
classname=”xdoclet.modules.ejb.EjbDocletTask”
classpathref=”xdoclet.lib.path” />
<taskdef name=”webdoclet”
classname=”xdoclet.modules.web.WebDocletTask”
classpathref=”xdoclet.lib.path” />
</target>
現在,任務聲明好了,XDoclet“整裝待發”。
2.3.2 使用任務
你可以在任何目標里使用聲明好的任務。在任務的上下文環境里,可以調動相關的子任務。讓我們看一個例子,這個例子調用了<ejbdoclet>任務。不要擔心看不懂語法的細節,現在你只需要關心一些基礎概念就可以了。
<target name=”generateEjb” depends=”init”>
<ejbdoclet destdir=”${gen.src.dir}”>
<fileset dir=”${src.dir}”>
<include name=”**/*Bean.java”/>
</fileset>
<deploymentdescriptor destdir=”${ejb.deployment.dir}”/>
<homeinterface/>
<remoteinterface/>
<localinterface/>
<localhomeinterface/>
</ejbdoclet>
</target>
把任務想像成一個子程序運行時需要的一個配置環境(記住,子任務才是真正進行代碼生成工作的)。當調用一個子任務時,子任務從任務繼承上下文環境,當然,你也可以根據需要隨意的覆蓋這些值。在上面的例子里,因為<deploymentdescriptor>子任務生成的布署描述符文件和其他生成各種接口的子任務生成的Java源文件需要放在不同的位置,所以覆蓋了destdir的屬性值。布署描述符文件需要放在一個在打包EJB JAR文件的時候可以容易包含進來的地方,而生成的Java代碼則需要放置在一個可以調用Java編譯器進行編譯的地方。需要這些子任務之間是緊密關聯的,但只要你需要,你可以有足夠的自主權控制任務的生成環境。
<fileset>屬性同樣被應用到所有的子任務。這是一個Ant的復雜類型(相對于文本和數值的簡單類型),所以以子元素的方式在任務中聲明。不要把它和子任務混為一談。當然,如果你想在某個子任務中另外指定一個不同的輸入文件集,你也可以在這個子任務中放置一個<fileset>子元素來覆蓋它。
子任務的可配置選項遠遠不止這些。我們會在下一章繼續介紹所有的任務和子任務,以及常用的配置選項。
2.4 用屬性標注你的代碼
可重用的代碼生成系統需要輸入來生成感興趣的輸出。一個解析器生成器也需要一個語言描述來解析生成解析器。一個商務對象代碼生成器需要領域模型來知道要生成哪些商務對象。XDoclet則需要Java源文件做為輸出來生成相關的類或者布署/配置文件。
然而,源文件可能并沒有提供代碼生成所需要的所有信息。考慮一個基于servlet的應用,當你想生成web.xml文件的時候,servlet源文件僅可以提供類名和適當的servlet接口方法。其他的信息比如URI pattern映射、servlet需要的初始化參數等信息并沒有涵蓋。顯而易見,如果class并沒有提供這些信息給你,你就需要自己手動在web.xml文件時填寫這些信息。
XDoclet當然也不會知道這些信息。幸運的是,解決方法很簡單。如果所需信息在源文件時沒有提供,那就提供它,做法就是在源文件里加入一些XDoclet屬性。XDoclet解析源文件,提取這些屬性,并把它們傳遞給模板,模板使用這些信息生成代碼。
2.4.1 剖析屬性
XDoclet屬性其實就是javadoc的擴展。它們在外表上和使用上都有javadoc屬性一樣,可以放置在javadoc文檔注釋里。文檔注釋以/**開始,*/結尾。下面是一個簡單的例子:
/**
* 這是一段javadoc注釋。
* 注釋可以被分解成多行,每一行都以星號開始。
*/
在注釋里的所有文本都被視為javadoc注釋,并且都能夠被XDoclet訪問到。注釋塊一般都與Java源文件中的某個實體有關,并緊跟在這個實體的前面。沒有緊跟實體的注釋塊將不會被處理。類(或者接口)可以有注釋塊,方法和域也可以有自己的注釋塊,比如:
/**
* 類注釋塊
*/
public class SomeClass {
/** 域注釋塊 */
private int id;
/**
* 構造函數注釋塊
*/
public SomeClass() {
// ...
}
/**
* 方法注釋塊
*/
public int getId() {
return id;
}
}
注釋塊分成兩部分:描述部分和標簽部分。當遇到第一個javadoc標簽時,標簽部分開始。Javadoc標簽也分成兩部分:標簽名和標簽描述。標簽描述是可選的,并且可以多行。例如:
/**
* 這是描述部分
* @tag1 標簽部分從這里開始
* @tag2
* @tag3 前面一個標簽沒有標簽描述。
* 這個標簽有多行標簽描述。
*/
XDoclet使用參數化標簽擴展了javadoc標簽。在XDoclet里,你可以在javadoc標簽的標簽描述部分加入name=”value”參數。這個微小的改動大大增強了javadoc標簽的表達能力,使得javadoc標簽可以用來描述復雜的元數據。下面的代碼顯示了使用XDoclet屬性描述實體Bean方法:
/**
* @ejb.interface-method
* @ejb.relation
* name=”blog-entries”
* role-name=”blog-has-entries”
* @ejb.value-object
* compose=”com.xdocletbook.blog.value.EntryValue”
* compose-name=”Entry”
* members=”com.xdocletbook.blog.interfaces.EntryLocal”
* members-name=”Entries”
* relation=”external”
* type=”java.util.Set”
*/
public abstract Set getEntries();
參數化標簽允許組合邏輯上相關聯的屬性。你可以加入描述這個類的元信息,使得這個類的信息足夠生成代碼。另外,程序員借由閱讀這樣的元信息,可以很快的理解這個類是如何使用的。(如果這個例子上的元信息你看不懂,不要擔心,在第4章里,我們會學到EJB相關的標簽以及它們的涵意。)
另外,請注意上面的例子中,所有的標簽名都以ejb開頭。XDoclet使用namespace.tagname的方式給標簽提供了一個命名空間。這樣做除了可以跟javadoc區別開來以外,還可以把任務相關的標簽組織起來,以免任務之間的標簽產生混淆。
2.5 代碼生成模式
XDoclet是一種基于模板的代碼生成引擎。從高層視圖上來看,輸出文件其實就是由解析執行各式各樣的模板生成出來的。如果你理解了模板以及它所執行的上下文環境,就可以確切的認識到,XDoclet可以生成什么,不可以生成什么。如果你正在評估XDoclet平臺,理解這些概念是非常重要的。要不然,你可能會錯過XDoclet的許多強大的功能,也可能會被XDoclet的一些限制感到迷惑。
XDoclet運行在在Ant構建文件環境中,它提供了Ant自定義任務和子任務來與XDoclet引擎交互。任務是子任務的容器,子任務負責執行代碼生成。子任務調用模板。模板提供了你將生成代碼的餅干模子。XDoclet解析輸入的源文件,提取出源文件中的XDoclet屬性元數據,再把這些數據提供給模板,驅動模板執行。除此之外,模板還可以提供合并點(merge points),允許用戶插入一些模板片斷(合并文件merge files)來根據需要定制代碼生成。
2.5.1 模板基礎
XDoclet使用代碼模板來生成代碼。模板(template)是你想生成文件的原型。模板里使用一些XML標簽來指導模板引擎如何根據輸入類以及它們的元數據來調整代碼的生成。
[定義:模板(template)是生成代碼或描述文件的抽象模視圖。當模板被解析的時候,指定的細節信息會被填入。]
模板一般情況下會有一個執行環境。模板可能應用在一個類環境(轉換生成transform generation),也有可能應用在一個全局環境(聚集生成aggregate generation)。轉換生成和聚集生成是XDoclet的兩種類型的任務模式,理解它們之間的區別對于理解XDoclet是非常重要的。
當你使用XDoclet生成布置描述符文件時,你使用的是聚集生成。布置描述符文件并不僅僅只與一個類相關,相反,它需要從多個類里聚集信息到一個輸入文件。在這種生成模式里,解析一次模板只會生成一個輸出文件,不管有多少個輸入文件。
在轉換生成模式里,模板遇到每一個源文件就會解析一次,根據該文件類的上下文環境生成輸出。這種生成模式會為每一個輸入文件生成一個輸出文件。
轉換生成模式的一個很好的例子是生成EJB的local和remote接口。顯然,接口是和Bean類一一相關的。從每一個類里提取信息(類以及它的方法、域、接口以及XDoclet屬性等信息)轉換出接口。除此以外,不需要其他的信息。
從實現里提取出接口似乎有點反向。如果你手寫程序的話,一般來說會先定義一個接口,然后再寫一個類來關現它。但XDoclet做不到,XDoclet不可能幫你實現一個已有接口,因為它不可能幫你生成你的業務邏輯。當然,如果業務邏輯可以從接口本身得到(比如JavaBean的get/set訪問器)或者使用XDoclet屬性聲明好,那么生成業務邏輯代碼來實現一個接口也不是不可能。但一般情況下,這樣做不太現實。相比而言,提供一個實現,并描述接口與這個實現之間的關聯就容易多了。
聚集生成和轉換生成主要區別在它們的環境信息上。即使一個代碼生成任務中生成一個Java文件,一般也不常用聚集生成,因為生成一個Java類還需要一些重要信息如類所處的包以及你想生成的類名,在這種環境下是無法提供的。如果一定要使用聚集生成的話,那就需要在另一個單獨的地方提供好配置信息了。
2.5.2 模板標簽
在還沒見到模板長啥樣子之前,我們已經比較深入的認識它了。那模板文件究竟長啥樣子呢?它有點像JSP文件。它們都包含文件和XML標簽,生成輸出文件時XML標簽會被解析,然后生成文本并顯示在XML標簽所處的位置上。除了以XDt為命名空間打頭的XML標簽會被XDoclet引擎解析以外,其余的XML標簽XDoclet會忽略不管。下面的代碼片斷顯示了XDoclet模板的“經典造型”:
public class
<XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
Extends Observabe {
static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
_instance = null;
public static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClassOf>
getInstance() {
if (_instance == null) {
_instance =
new <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/>
</XDtClass:classOf>();
}
return _instance;
}
}
研究一下這個模板,你會發現,它生成的是一個類定義。這個類里定義了一個靜態變量instance,并且使用一個靜態方法來控制這個靜態文件的訪問。借助Java語法,你可以很容易的推斷出那些XDoclet模板標簽的目錄是生成類名,雖然對于這個標簽如何工作你還并不是很了解。
即使你從沒打算過要自己寫模板,但理解模板是如何被解析運行的還是很有必要的。遲早你會調用到一個運行失敗的XDoclet任務,沒有產生你所期望的輸出,那么最快捷的找出原因的方法就是直接檢查模板文件,看看是哪里出了問題。
讓我們看一下生成靜態域定義的片斷:
static <XDtClass:classOf><XDtEjbFacade:remoteFacadeClass/></XDtClass:classOf>
_instance = null;
在XDoclet的眼里,這段模板代碼很簡單,那就是:
static <tag/> _instance = null;
XDoclet解析執行標簽,如果有輸出的話,把輸入置回到文本里去。有些標簽會執行一些運算,把輸出放回到一個流里。這樣的標簽稱之為內容標簽(content tags),因為它們產生內容。
另一種類型的標簽稱之為BODY標簽。BODY標簽在開始和結束標簽之間存在文本。而BODY標簽強大就強大在這些文本自己也可以是一斷可以由外圍標簽解析的模板片斷。比如在上面的例子里,XDtClass:classOf標簽,它們的內容就是模板片斷:
<XDtEjbFacade:remoteFacadeClass/>
classOf標簽解析這段模板,提取出全限制的內容,然后剃除前面的包面,只輸出類名。BODY標簽并不總是會解析它的內容,在做這件事之前,它們會事先檢查一些外部判斷條件(比如檢查檢查你正在生成的是一個接口還是一個類)。這里標標簽稱之為條件標簽(conditional tags)。還有一些BODY標簽提供類似迭代的功能,它的內容會被解析多次。比如一個標簽針對類里的每一個方法解析一次內容。
XDoclet標簽提供了許多高層次的代碼生成功能,但是有時候,它們可能顯得不夠靈活,或者表達能力滿足不了你的需要。這時候,相對于另外開發一套通用功能的模板引擎相比,你可以選擇擴展XDoclet模板引擎。你可以使用更具表述能力、功能更加強大的Java平臺開發你自己的一套標簽。
2.6 使用合并定制
代碼生成系統之所以使用的不多,主要原因就在于它們往往只能生成一些死板的、不夠靈活的代碼。大多數代碼生成系統不允許你改動它們生成的代碼;如果,如果這個系統不夠靈活,你所能做到的最好的擴展就是應用繼承擴展生成的代碼,或者使用一些共通的設計模式(比如Proxy和Adaptor)來滿足你的需要。無論如此,這都不是產生你想生成的代碼的好辦法。代碼生成器最好能做到所生成即所得WYGIWYG(what you generate is what you get),來取代你需要花費大量的時間來粉飾生成出來的并不滿足要求的代碼。所以,對于代碼生成器來說,支持靈活的定制,是生成能夠完全滿足要求的代碼的前提條件。
XDoclet通過合并點(merge points)支持定制——合并點是在模板文件定義里允許運行時插入定制代碼的地方。有時候,合并點甚至可以影響到全局代碼的生成,不但允許你添加一些定制內容,還可以從根本上改變將要生成出來的東西。
[定義:合并點(Merge points)是模板預先定義的允許你在代碼生成的運行時加入定制內容的擴展點]
讓我們研究一段從XDoclet源代碼里摘取出來的模板代碼。在為實體Bean生成主鍵的模板末尾,定義了這樣一個合并點:
<XDtMerge:merge file=”entitypk-custom.xdt”></XDtMerge:merge>
如果你在你的merge目錄下創建了一個名為entitypk-custom.xdt文件,那么這個模板文件的內容將會在這個合并點被包含進來。你的定制可以執行高層模板可以執行的所有強大功能,可以進行所有模板可以進行的運算(包括定義自定義標簽,定義它們自己的合并點)。
上面的這種合并點,為所有的類環境使用了同一個文件。當然,也可以為每一個類環境使用不同的合并文件。如果你不想定制全部的類文件,或者你不想為了某些改動而重寫模板的時候,這會很有用。不管動機是什么,逐類的合并點很容易識別出來:他們會在名字里包含一個XDoclet的逐類標記{0}。這里有一個生成ejb-jar.xml文件里的安全角色引用的例子:
<XDtMerge:merge file=”ejb-sec-rolerefs-{0}.xml”>
<XDtClass:forAllClassTags tagName=”ejb:security-role-ref”>
<security-role-ref>
<role-name>
<XDtClass:classTagValue
tagName=”ejb:security-roleref”
paramName=”role-name”/>
</role-name>
<role-link>
<XDtClass:classTagValue
tagName=”ejb:security-roleref”
paramName=”role-link”/>
</role-link>
</security-role-ref>
</XDtClass:forAllClassTags>
</XDtMerge:merge>
這段模板會遍歷工程里的所有Bean。對于每一個Bean,XDoclet先從Bean的文件名里提取出Bean名,然后替換{0},再根據替換后的文件名去尋找合并文件。例如,如果你有一個名為BlogFacadeBean的Bean,XDoclet會嘗試尋找一個名為ejb-src-rolerefs-BlogFacade.xml的合并文件。
如果找不到這個合并文件,則這個<merge>標簽的內容模板會被解析。這意味著合并點不僅可以提供定制內容,還可以在一個模板文件里定義一個替換點,當定制內容不存在的時候使用替換點里的內容。不是所有的XDoclet任務都提供了有替換內容的合并點,一般來說,它們更傾向于只提供一個簡單的合并點,僅僅當合并文件存在的時候解析并導入合并文件的內容。這取決于任務的開發者覺得哪種合并點更符合他的要求。
還有一點沒有介紹到的是XDoclet如何定位合并文件,每一個XDoclet任務或者子任務都會提供一個mergeDir屬性,這個屬性用于設置你存放合并文件的目錄。
<ccid_code>private HtmlInputText txtUsername = new HtmlInputText(); |
<ccid_code>/** |
<ccid_code>public javascriptSubTask() |
<ccid_code>/* |
<ccid_code><target name="javascript" depends="jxdoc_init" |
<ccid_code><XDtClass:forAllClasses> 遍歷所有含有標簽的類(在ant中指定) |
<ccid_code><?xml version="1.0" encoding="UTF-8"?> |
<ccid_code><xdoclet> |
<ccid_code>“<level> method </level>”代表該標簽出現在方法上而不是類之上。例如 |
XDoclet是生成配置文件的強有力的工具,在使用Spring框架時,手動編寫Spring配置文件極其繁瑣,而且容易遺漏,利用XDoclet即可輕松生成配置文件。
XDoclet支持的Spring方法注入包括:ref,list,name和value。遺憾的是,在某個項目中,我需要注入一個包含Bean引用的List:
public void setHandlers(List handlers) {
? ...
}
然后,XDoclet并不支持元素為引用類型的List,倘若使用下列注釋:
/**
?* @spring.property list="articleHandler,imageHandler"
?*/
public void setHandlers(List handlers) {
? ...
}
生成的配置文件如下:
<property name="handlers">
? <list>
??? <value>articleHandler</value>
??? <value>imageHandler</value>
? </list>
</property>
毫無疑問,在Spring啟動時,一個ClassCastException將被拋出,因為無法將String類型轉化為我們自定義的Handler引用類型。
幸運的是,XDoclet良好的可擴展性使我們能夠輕松擴展需要的配置,甚至不需要我們利用XDoclet提供的API編寫代碼。XDoclet提供一種XML結構的模版語言來生成配置文件,對于Spring配置文件,對應的XML配置文件在xdoclet-spring-module-1.2.3.jar/xdoclet/modules/spring/resources/spring_xml.xdt中。
解開jar包,修改spring_xml.xdt,增加如下XML片斷(紅色部分):
? <XDtMethod:forAllMethods superclasses="true">
?? <XDtMethod:ifHasMethodTag tagName="spring.property">
??? <property name="<XDtMethod:propertyName/>">
??? <XDtMethod:ifHasMethodTag tagName="spring.property" paramName="value">
????? <value><XDtMethod:methodTagValue tagName="spring.property" paramName="value"/></value>
??? </XDtMethod:ifHasMethodTag>
??? <XDtMethod:ifHasMethodTag tagName="spring.property" paramName="ref">
????? <ref bean="<XDtMethod:methodTagValue tagName="spring.property" paramName="ref"/>"/>
??? </XDtMethod:ifHasMethodTag>
??? <XDtMethod:ifHasMethodTag tagName="spring.property" paramName="list">
????? <list>
????? <XDtMethod:forAllMethodTagTokens tagName="spring.property" paramName="list">
??????? <value><XDtMethod:currentToken/></value>
????? </XDtMethod:forAllMethodTagTokens>
????? </list>
??? </XDtMethod:ifHasMethodTag>
??? <XDtMethod:ifHasMethodTag tagName="spring.property" paramName="list.ref">
????? <list>
??????? <XDtMethod:forAllMethodTagTokens tagName="spring.property" paramName="list.ref">
????????? <ref bean="<XDtMethod:currentToken/>"/>
??????? </XDtMethod:forAllMethodTagTokens>
????? </list>
??? </XDtMethod:ifHasMethodTag>
??? </property>
?? </XDtMethod:ifHasMethodTag>
? </XDtMethod:forAllMethods>
注意紅色部分的代碼,我們仿照list,增加一個list.ref來實現引用類型的List。
現在,修改注釋如下:
/**
?* @spring.property list.ref="articleHandler,imageHandler"
?*/
public void setHandlers(List handlers) {
? ...
}
備份好原有的xdoclet-spring-module-1.2.3.jar,然后將修改后的目錄打包:
jar cvf xdoclet-spring-module-1.2.3.jar .
替換原來的xdoclet-spring-module-1.2.3.jar,運行XDoclet,順利生成預期配置:
<property name="handlers">
? <list>
??? <ref bean="articleHandler"/>
??? <ref bean="imageHandler"/>
? </list>
</property>
類似的,我們還可以增加XDoclet對Map注入的支持。
發現多功能的模板驅動的代碼生成器 ![]() |
![]() |
![]() |
級別: 初級
Sing Li
, 作者, Wrox Press 2004 年 11 月 05 日 開放源代碼的 XDoclet 代碼生成引擎,是許多領先的 Java 框架不可缺少的組成部分,常常被用作面向屬性的編程和持續集成的引擎。但是 XDoclet 還有一些不太惹人注目的地方:對初級開發人員來說,它太難掌握、太難精通。在這篇文章中,流行作者 Sing Li 以 XDoclet 為對象,揭示了其內部簡單卻優雅的設計,使您能夠理解這項技術,并將它應用在實踐當中。 XDoclet 能夠很容易成為您的 Java 編程工具箱中的一個更加通用的跨技術代碼生成工具。不幸的是,開發人員經常忽視 XDoclet 的一般用途,只有將它捆綁在大型開發框架或者 IDE 中,作為其中的一個隱藏元素時,才會用到它。人們常常認為很難將 XDoclet 應用在定制解決方案上。這篇文章的目的就是要消除這個迷惑,把 XDoclet 從常見的復雜陷阱中解脫出來,并向您展示了如何能夠利用這個代碼生成引擎。 我會用一個實際的例子演示 XDoclet 的用途,該例子將接收一個 POJO(plain old Java object),并用 XDoclet 生成完整 Web 應用程序的全部文件,這些文件是把數據輸入關系數據庫所必需的。該示例使用了 XDoclet 的 自定義模板代碼生成功能,以及它對 Hibernate 對象關系映射工具、Struct Web 應用程序框架和應用程序服務器的內部支持。(請參閱 參考資料)。 XDoclet 的核心功能是根據以下組合來生成代碼的(或者生成其他配置/數據文件):
與其他基于模板的代碼生成技術(例如 Velocity;請參閱 參考資料)相比,XDoclet 具有以下獨特優勢:
接下來,我將進一步研究 XDoclet 是如何工作的,以幫助您理解這些特性。 圖 1 顯示了 XDoclet 要求的輸入和生成的輸出。 圖 1. XDoclet 黑盒子 ![]() 您可以看到,包含嵌入式 XDoclet 標簽的 Java 源代碼是系統的輸入。在 Apache Ant 的驅動下,XDoclet 處理輸入的代碼,生成的輸出文本文件可以是 Java 源代碼、HTML 頁面、XML 文件等。為了處理輸入,XDoclet 需要使用模板(保存在 .xdt 文件中)和標簽處理器(用 Java 編碼)。XDoclet 把模板和標簽處理器打包成“模塊”,不同的“模塊”處理不同的問題域。 XDoclet 對包含嵌入式 XDoclet 標簽的輸入 Java 源代碼進行解析,并為代碼建立非常詳細的結構模型。結構模型中的每個元素都代表源代碼中的一個 Java 結構。圖 2 顯示的結構模型,揭示了 XDoclet 跟蹤的代碼構造和關系。 圖 2. XDoclet 的解析的 Java 源代碼的內部結構模型 ![]() 圖 2 中的結構模型跟蹤類、接口、方法之類的代碼構造(模型元素)。該模型還跟蹤元素之間的關系,例如繼承和接口實現。以內聯注釋的形式嵌入在源代碼中的 XDoclet 標簽被解析為模型元素的屬性,并被跟蹤。
圖 3 顯示了 XDoclet 的內部結構,揭示了使其運行的功能塊。 圖 3. XDoclet 內部的功能塊 ![]() |
如圖 3 所示,Apache Ant 在運行的時候控制著 XDoclet 的配置和操作。XDoclet 解析輸入的 Java 源代碼,并在內存中生成結構模型。模板引擎通過處理一組模板和標簽處理器,生成輸出文件。模板和標簽處理器可以是內置的,也可以是定制的。在代碼生成期間,模板和標簽處理器擁有對結構模型的完全訪問。
XDoclet 實質上就是一個通用的 Javadoc 引擎(請參閱側欄, 通用的 Javadoc 引擎)。那么,是什么讓它看起來這么復雜呢?答案在于:XDoclet 幾乎從未被單獨討論過,而總是藏在其他許多復雜的技術中。圖 4 顯示了了圍繞在 XDoclet 周圍的復雜性迷霧(請參閱側欄 為什么 XDoclet 看起來比實際的要復雜得多)。
![]() |
|
在圖 4 中,您可以看到 XDoclet 與以下內容是緊密相關的:
XDoclet 本身卻是驚人地簡單,正如下面示例中的工作代碼所示的那樣。
![]() ![]() |
![]()
|
現在,您可以通過研究我向您提供的數據入口應用程序示例,來觀察 XDoclet 的實際工作。(要下載這個示例中使用的 Java 代碼、XDoclet 模板和 Ant 腳本,請單擊本文頂部或底部的 Code圖標,或者請參閱 下載部分。)我們將從檢查清單 1 所示的 Java 代碼開始,這部分代碼表示了一個客戶的地址。該地址被編碼成 JavaBean 組件,其中的 XDoclet 標簽是以黑體字形式顯示的:
|
在清單 1 中,需要注意的是,要把 XDoclet 標簽嵌入到注釋中,緊放在相關代碼元素(例如字段、方法、接口或類)的前面。在解析源代碼時,XDoclet 會為每個標簽建立一個屬性,并將該屬性附加到結構模型的代碼元素上。現在,請注意 @dw.genStruts
標簽,因為這是在本例中將用到的第一個模板。
對于本例,您需要生成新的 Java 類的代碼 —— 一個 Struts 表單 bean。Struts 會用這個 bean 保存并傳輸用戶輸入。bean 必須以 bean 屬性的形式包含所有數據字段,而且它必須是 org.apache.struts.action.ActionForm
的子類。
為了生成表單 bean 的代碼,您要根據清單 2 所示的偽代碼生成 XDoclet 模板。括號中的黑體字代表控制流邏輯和您要進行替換的文本。請注意模板是如何從已解析的 Java 源代碼文件的結構模型中提取信息的:
|
![]() |
|
在清單 2 的循環中的代碼生成了一個字段聲明和一個訪問器方法,還為輸入源代碼中每個用 @dw.genStruts
標記的訪問器方法生成了一個設置器方法。
清單 2 使用了易于理解的偽代碼來表示模板替換標簽。實際的 XDoclet 模板標簽則相當繁瑣。清單 3 顯示了 genformbean.xdt 模板(所有的 XDoclet 模板都保存在 .xdt 文件中)。我已經用黑體字強調了 XDoclet 模板標簽,以方便在偽代碼中對其進行引用。
|
![]() |
|
您可以參考 XDoclet 的“模板語言”文檔,查找 XDoclet 所有可用標簽的列表(請參閱 參考資料)。
要運行用于 AddressBean.java 源文件的模板,請使用以下 Ant 命令行:
|
這個命令可以執行定制 Ant 目標(請參閱側欄 熟悉 Ant 腳本編寫)來處理 genbeanform.xdt 模板。XDoclet 提供的 Ant 任務叫作 xdoclet.DocletTask
,它被用來運行模板文件。如果您對 Ant 的細節感興趣,請參閱示例代碼中的 build.xml 文件,以了解更多信息。
在 XDoclet 處理模板的時候,它在名為 generated的子目錄下生成一個 AddressBeanForm.java 文件。清單 4 顯示了該文件,它包含模板處理期間替換的所有文本:
|
您可以用相同的 AddressBean.java 源文件,但是用 genformjsp.xdt 模板生成數據入口表單 JSP 頁面。清單 5 顯示了 genformjsp.xdt:
|
請注意,代碼中用 <XDt:methodTagValue>
取得原始 AddressBean.java 代碼中在 XDoclet 標簽中指定的屬性值。
當您執行 genstruts
Ant 目標的時候,也會處理清單 5 顯示的 genformjsp.xdt 模板。您可以在 generated 子目錄中找到生成的 AddressBeanForm.jsp 文件,檢查文件內容,可以看到要對模板進行的替換。
![]() |
|
您可以用 XDoclet 生成任意基于文本的輸出。我向您展示的例子使用 XDoclet 生成了 Java 代碼、JSP 頁面、XML 文件、配置文件以及其他更多輸出。它從一個簡單的用 XDoclet 進行標記的 Java 源文件 AddressBean.java,建立了一個完整的數據入口 Web 應用程序。為了做到這一點,它執行了 XDoclet 的內置模板(位于 JAR 文件中,稱為模塊),從而生成:
表 1 顯示了為示例應用程序生成的所有文件(通常稱為 工件(artifact)):
生成的工件 | 說明 | 位置 |
AddressBeanForm.java | Java 源文件,包含表單 bean 類,在 Struts 的表單處理中使用 | generated目錄 |
AddressBeanForm.jsp | JSP 表單,用 Struts 標簽庫接受用戶地址輸入 | jsp目錄 |
AddressBeanAction.java | Struts 動作類,接受輸入值,用 Hibernate 把值保存到關系數據庫 | generated目錄 |
AddressBean.hbm.xml | Hibernate 映射文件,在 AddressBean Java 對象和數據庫的關系型 ADDRESS 表之間進行映射 | web/classes目錄 |
dwschema.sql | RDBMS 表的架構,用來對 AddressBean 對象的實例進行持久化 | sql目錄 |
hibernate.cfg.xml | Hibernate 運行時的配置文件 | web/classes目錄 |
web.xml | 生成的 Web 應用程序的部署描述符 | web目錄 |
struts-config.xml | Struts 框架的配置文件 | web目錄 |
在這篇文章中,您詳細了解了表 1 中所列的兩個工件中的第一個工件的生成,深入了解了生成它們的模板。工件 AdddressBeanAction.java 則用類似的方法,利用叫作 genaction.xdt 的模板生成。XDoclet 具有內置模板和標簽處理器,可以生成表 1 中的其他工件。
表 2 列出了每個生成的工件對應的 Ant 目標和 Ant 任務。您可以執行表格中的每個 Ant 目標,生成對應的工件。所有這些生成的工件,再加上原始和 AddressBean.java,共同構成了示例 Web 應用程序。您還會發現叫作 all
的默認 Ant 目標,它會為您做任何事,包括為應用程序建立 WAR(可以部署的 Web 歸檔)。在進行處理之前,一定要閱讀代碼發布包中的 README.txt 文件。
![]() |
|
Ant 目標 | Ant 任務 | 工件 |
genstruts | xdoclet.DocletTask | AddressBeanForm.java |
genstruts | xdoclet.DocletTask | AddressBeanForm.jsp |
genstruts | xdoclet.DocletTask | AddressBeanAction.java |
generateHIB | xdoclet.modules.hibernate.HibernateDocletTask | AddressBean.hbm.xml |
generateHIB | xdoclet.modules.hibernate.HibernateDocletTask | hibernate.cfg.xml |
createDDL | xdoclet.modules.hibernate.HibernateDocletTask | dwschema.sql |
generateDD | xdoclet.modules.web.WebDocletTask | web.xml |
generateDD | xdoclet.modules.web.WebDocletTask | struts-config.xml |
![]() ![]() |
![]()
|
XDoclet 是一個有用的、智能的代碼生成器,您可以用它自動進行許多日常的 Java 開發任務。不要被它表面的復雜所嚇退。隨著逐漸精通 XDoclet(以及與之相關的 Apache Ant),您會節約您寶貴的時間,并在未來的開發工作中,得到數倍的回報。
![]() ![]() |
![]()
|
名字 | 大小 | 下載方法 |
---|---|---|
j-xdoclet-code.zip | ? | ?FTP |
![]() | ||||
![]() | 關于下載方法的信息 | ![]() | ![]() | Get Adobe? Reader? |
![]() ![]() |
![]()
|
![]() ![]() |
![]()
|
![]() | ||
![]() | Sing Li 是 Professional Apache Tomcat 5、 Pro JSP, Third Edition、 Early Adopter JXTA、 Professional Jini,以及 Wrox Press 出版的許多其他圖書的作者。他是技術雜志的定期撰稿人,還是 P2P 發展的熱心傳播者。Sing 是一名咨詢顧問和資深作者,您可以通過他的電子郵件 westmakaha@yahoo.com與他聯系。 |
XDoclet 操作
圖 1 顯示了 XDoclet 要求的輸入和生成的輸出。
您可以看到,包含嵌入式 XDoclet 標簽的 Java 源代碼是系統的輸入。在 Apache Ant 的驅動下,XDoclet 處理輸入的代碼,生成的輸出文本文件可以是 Java 源代碼、HTML 頁面、XML 文件等。為了處理輸入,XDoclet 需要使用模板(保存在 .xdt 文件中)和標簽處理器(用 Java 編碼)。XDoclet 把模板和標簽處理器打包成“模塊”,不同的“模塊”處理不同的問題域。
XDoclet 生成的結構模型
XDoclet 對包含嵌入式 XDoclet 標簽的輸入 Java 源代碼進行解析,并為代碼建立非常詳細的結構模型。結構模型中的每個元素都代表源代碼中的一個 Java 結構。圖 2 顯示的結構模型,揭示了 XDoclet 跟蹤的代碼構造和關系。
圖 2. XDoclet 的解析的 Java 源代碼的內部結構模型
圖 2 中的結構模型跟蹤類、接口、方法之類的代碼構造(模型元素)。該模型還跟蹤元素之間的關系,例如繼承和接口實現。以內聯注釋的形式嵌入在源代碼中的 XDoclet 標簽被解析為模型元素的屬性,并被跟蹤
深入 XDoclet
圖 3 顯示了 XDoclet 的內部結構,揭示了使其運行的功能塊。
如圖 3 所示,Apache Ant 在運行的時候控制著 XDoclet 的配置和操作。XDoclet 解析輸入的 Java 源代碼,并在內存中生成結構模型。模板引擎通過處理一組模板和標簽處理器,生成輸出文件。模板和標簽處理器可以是內置的,也可以是定制的。在代碼生成期間,模板和標簽處理器擁有對結構模型的完全訪問。
XDoclet 虛假的復雜性
XDoclet 實質上就是一個通用的 Javadoc 引擎(請參閱側欄,通用的 Javadoc 引擎)。那么,是什么讓它看起來這么復雜呢?答案在于:XDoclet 幾乎從未被單獨討論過,而總是藏在其他許多復雜的技術中。圖 4 顯示了了圍繞在 XDoclet 周圍的復雜性迷霧(請參閱側欄為什么 XDoclet 看起來比實際的要復雜得多)。
為什么 XDoclet 看起來比實際的要復雜得多
XDoclet 處理的問題領域是復雜性的另一個來源。在發布 XDoclet 的時候,XDoclet 已經可以為 EJB 組件集成、J2EE Web 容器集成、Hibernate 持久性層、Struts 框架、Java 管理擴展(JMX)等生成代碼。這些問題領域中的每一個領域都有一大套該領域專用的行話和概念。從這些復雜的問題領域出來的問題,經常主導著 XDoclet 的討論,這也提高了 XDoclet 表面的復雜性。可能是“只見森林,不見樹木”。 |
在圖 4 中,您可以看到 XDoclet 與以下內容是緊密相關的: