使用 Java 解析 XML 文件
當你編寫一個應用時,你通常都會希望用戶能夠定製化他們和應用交互的方式,以及應用與系統進行交互的方式。這種方式通常被稱為 「 偏好 」 或者 「 設置 」,它們被保存在一個 「偏好文件」 或者 「配置文件」 中,有時也直接簡稱為 「 配置 」。配置文件可以有很多種格式,包括 INI、JSON、YAML 和 XML。每一種編程語言解析這些格式的方式都不同。本文主要討論,當你在使用 Java 編程語言 來編寫軟體時,實現持久化配置的方式。
選擇一個格式
編寫配置文件是一件相當複雜的事情。我曾經試過把配置項使用逗號分隔保存在一個文本文件里,也試過把配置項保存在非常詳細的 YAML 和 XML 中。對於配置文件來說,最重要是要有一致性和規律性,它們使你可以簡單快速地編寫代碼,從配置文件中解析出數據;同時,當用戶決定要做出修改時,很方便地保存和更新配置。
目前有 幾種流行的配置文件格式。對於大多數常見的配置文件格式,Java 都有對應的 庫 。在本文中,我將使用 XML 格式。對於一些項目,你可能會選擇使用 XML,因為它的一個突出特點是能夠為包含的數據提供大量相關的元數據,而在另外一些項目中,你可能會因為 XML 的冗長而不選擇它。在 Java 中使用 XML 是非常容易的,因為它默認包含了許多健壯的 XML 庫。
XML 基礎
討論 XML 可是一個大話題。我有一本關於 XML 的書,它有超過 700 頁的內容。幸運的是,使用 XML 並不需要非常了解它的諸多特性。就像 HTML 一樣,XML 是一個帶有開始和結束標記的分層標記語言,每一個標記(標籤)內可以包含零個或更多數據。下面是一個 XML 的簡單示例片段:
<xml>
<node>
<element>Penguin</element>
</node>
</xml>
在這個 自我描述的 例子中,XML 解析器使用了以下幾個概念:
- 文檔 :
<xml>
標籤標誌著一個 文檔 的開始,</xml>
標籤標誌著這個文檔的結束。 - 節點 :
<node>
標籤代表了一個 節點。 - 元素 :
<element>Penguin</element>
中,從開頭的<
到最後的>
表示了一個 元素。 - 內容 : 在
<element>
元素里,字元串Penguin
就是 內容。
不管你信不信,只要了解了以上幾個概念,你就可以開始編寫、解析 XML 文件了。
創建一個示例配置文件
要學習如何解析 XML 文件,只需要一個極簡的示例文件就夠了。假設現在有一個配置文件,裡面保存的是關於一個圖形界面窗口的屬性:
<xml>
<window>
<theme>Dark</theme>
<fullscreen>0</fullscreen>
<icons>Tango</icons>
</window>
</xml>
創建一個名為 ~/.config/DemoXMLParser
的目錄:
$ mkdir ~/.config/DemoXMLParser
在 Linux 中,~/.config
目錄是存放配置文件的默認位置,這是在 自由桌面工作組 的規範中定義的。如果你正在使用一個不遵守 自由桌面工作組 標準的操作系統,你也仍然可以使用這個目錄,只不過你需要自己創建這些目錄了。
複製 XML 的示例配置文件,粘貼並保存為 ~/.config/DemoXMLParser/myconfig.xml
文件。
使用 Java 解析 XML
如果你是 Java 的初學者,你可以先閱讀我寫的 面向 Java 入門開發者的 7 個小技巧。一旦你對 Java 比較熟悉了,打開你最喜愛的集成開發工具(IDE),創建一個新工程。我會把我的新工程命名為 myConfigParser
。
剛開始先不要太關注依賴導入和異常捕獲這些,你可以先嘗試用 javax
和 java.io
包里的標準 Java 擴展來實例化一個解析器。如果你使用了 IDE,它會提示你導入合適的依賴。如果沒有,你也可以在文章稍後的部分找到完整的代碼,裡面就有完整的依賴列表。
Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser");
File configFile = new File(configPath.toString(), "myconfig.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
builder = factory.newDocumentBuilder();
Document doc = null;
doc = builder.parse(configFile);
doc.getDocumentElement().normalize();
這段示例代碼使用了 java.nio.Paths
類來找到用戶的主目錄,然後在拼接上默認配置文件的路徑。接著,它用 java.io.File
類來把配置文件定義為一個 File
對象。
緊接著,它使用了 javax.xml.parsers.DocumentBuilder
和 javax.xml.parsers.DocumentBuilderFactory
這兩個類來創建一個內部的文檔構造器,這樣 Java 程序就可以導入並解析 XML 數據了。
最後,Java 創建一個叫 doc
的文檔對象,並且把 configFile
文件載入到這個對象里。通過使用 org.w3c.dom
包,它讀取並規範化了 XML 數據。
基本上就是這樣啦。理論上來講,你已經完成了數據解析的工作。可是,如果你不能夠訪問數據的話,數據解析也沒有多少用處嘛。所以,就讓我們再來寫一些查詢,從你的配置中讀取重要的屬性值吧。
使用 Java 訪問 XML 的值
從你已經讀取的 XML 文檔中獲取數據,其實就是要先找到一個特定的節點,然後遍歷它包含的所有元素。通常我們會使用多個循環語句來遍歷節點中的元素,但是為了保持代碼可讀性,我會儘可能少地使用循環語句:
NodeList nodes = doc.getElementsByTagName("window");
for (int i = 0; i < nodes.getLength(); i++) {
Node mynode = nodes.item(i);
System.out.println("Property = " + mynode.getNodeName());
if (mynode.getNodeType() == Node.ELEMENT_NODE) {
Element myelement = (Element) mynode;
System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent());
System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent());
System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent());
}
}
這段示例代碼使用了 org.w3c.dom.NodeList
類,創建了一個名為 nodes
的 NodeList
對象。這個對象包含了所有名字匹配字元串 window
的子節點,實際上這樣的節點只有一個,因為本文的示例配置文件中只配置了一個。
緊接著,它使用了一個 for
循環來遍歷 nodes
列表。具體過程是:根據節點出現的順序逐個取出,然後交給一個 if-then
子句處理。這個 if-then
子句創建了一個名為 myelement
的 Element
對象,其中包含了當前節點下的所有元素。你可以使用例如 getChildNodes
和 getElementById
方法來查詢這些元素,項目中還 記錄了 其他查詢方法。
在這個示例中,每個元素就是配置的鍵。而配置的值儲存在元素的內容中,你可以使用 .getTextContent
方法來提取出配置的值。
在你的 IDE 中運行代碼(或者運行編譯後的二進位文件):
$ java ./DemoXMLParser.java
Property = window
Theme = Dark
Fullscreen = 0
Icon set = Tango
下面是完整的代碼示例:
package myConfigParser;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ConfigParser {
public static void main(String[] args) {
Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser");
File configFile = new File(configPath.toString(), "myconfig.xml");
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
e.printStackTrace();
}
Document doc = null;
try {
doc = builder.parse(configFile);
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
doc.getDocumentElement().normalize();
NodeList nodes = doc.getElementsByTagName("window");
for (int i = 0; i < nodes.getLength(); i++) {
Node mynode = nodes.item(i);
System.out.println("Property = " + mynode.getNodeName());
if (mynode.getNodeType() == Node.ELEMENT_NODE) {
Element myelement = (Element) mynode;
System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent());
System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent());
System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent());
} // close if
} // close for
} // close method
} //close class
使用 Java 更新 XML
用戶時不時地會改變某個偏好項,這時候 org.w3c.dom
庫就可以幫助你更新某個 XML 元素的內容。你只需要選擇這個 XML 元素,就像你讀取它時那樣。不過,此時你不再使用 .getTextContent
方法,而是使用 .setTextContent
方法。
updatePref = myelement.getElementsByTagName("fullscreen").item(0);
updatePref.setTextContent("1");
System.out.println("Updated fullscreen to " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent());
這麼做會改變應用程序內存中的 XML 文檔,但是還沒有把數據寫回到磁碟上。配合使用 javax
和 w3c
庫,你就可以把讀取到的 XML 內容寫回到配置文件中。
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer xtransform;
xtransform = transformerFactory.newTransformer();
DOMSource mydom = new DOMSource(doc);
StreamResult streamResult = new StreamResult(configFile);
xtransform.transform(mydom, streamResult);
這麼做會沒有警告地寫入轉換後的數據,並覆蓋掉之前的配置。
下面是完整的代碼,包括更新 XML 的操作:
package myConfigParser;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ConfigParser {
public static void main(String[] args) {
Path configPath = Paths.get(System.getProperty("user.home"), ".config", "DemoXMLParser");
File configFile = new File(configPath.toString(), "myconfig.xml");
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
DocumentBuilder builder = null;
try {
builder = factory.newDocumentBuilder();
} catch (ParserConfigurationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Document doc = null;
try {
doc = builder.parse(configFile);
} catch (SAXException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
doc.getDocumentElement().normalize();
Node updatePref = null;
// NodeList nodes = doc.getChildNodes();
NodeList nodes = doc.getElementsByTagName("window");
for (int i = 0; i < nodes.getLength(); i++) {
Node mynode = nodes.item(i);
System.out.println("Property = " + mynode.getNodeName());
if (mynode.getNodeType() == Node.ELEMENT_NODE) {
Element myelement = (Element) mynode;
System.out.println("Theme = " + myelement.getElementsByTagName("theme").item(0).getTextContent());
System.out.println("Fullscreen = " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent());
System.out.println("Icon set = " + myelement.getElementsByTagName("icons").item(0).getTextContent());
updatePref = myelement.getElementsByTagName("fullscreen").item(0);
updatePref.setTextContent("2");
System.out.println("Updated fullscreen to " + myelement.getElementsByTagName("fullscreen").item(0).getTextContent());
} // close if
}// close for
// write DOM back to the file
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer xtransform;
DOMSource mydom = new DOMSource(doc);
StreamResult streamResult = new StreamResult(configFile);
try {
xtransform = transformerFactory.newTransformer();
xtransform.transform(mydom, streamResult);
} catch (TransformerException e) {
e.printStackTrace();
}
} // close method
} //close class
如何保證配置不出問題
編寫配置文件看上去是一個還挺簡單的任務。一開始,你可能會用一個簡單的文本格式,因為你的應用程序只要寥寥幾個配置項而已。但是,隨著你引入了更多的配置項,讀取或者寫入錯誤的數據可能會給你的應用程序帶來意料之外的錯誤。一種幫助你保持配置過程安全、不出錯的方法,就是使用類似 XML 的規範格式,然後依靠你用的編程語言的內置功能來處理這些複雜的事情。
這也正是我喜歡使用 Java 和 XML 的原因。每當我試圖讀取錯誤的配置值時,Java 就會提醒我。通常,這是由於我在代碼中試圖獲取的節點,並不存在於我期望的 XML 路徑中。XML 這種高度結構化的格式幫助了代碼保持可靠性,這對用戶和開發者來說都是有好處的。
via: https://opensource.com/article/21/7/parsing-config-files-java
作者:Seth Kenlon 選題:lujun9972 譯者:lkxed 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive