感觉像XML永远存在。 事实上,XML迎来了它的十周年之际,2008年随着Java™语言只用了几年早于XML,人们可以说,对于Java开发人员,XML 已经撒手人寰。
从一开始,Java语言的发明者Sun Microsystems就一直是XML的大力支持者。 毕竟,XML对平台独立性的承诺与Java语言的“编写一次,随处运行”消息非常吻合。 鉴于这两种技术的共同敏感性,您认为Java语言和XML会比它们相处得更好。 实际上,用Java语言解析和生成XML感觉很奇怪,而且令人费解。
令人高兴的是,Groovy引入了新的更好的方法来创建和处理XML。 在一些示例(都可以下载 )的帮助下,本文向您展示了Groovy如何使构建和解析XML变得非常简单。
比较Java和Groovy XML解析
在“ Reaching Each ”的结尾,我介绍了清单1中所示的简单XML文档。(这次我添加了type
属性,使事情变得更加有趣。)
清单1.列出我知道的语言的XML文档
<langs type="current">
<language>Java</language>
<language>Groovy</language>
<language>JavaScript</language>
</langs>
如清单2所示,在Java语言中解析这个琐碎的XML文档绝对是不平凡的。它需要30行代码来解析一个五行XML文件。
清单2.用Java解析XML文件
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.IOException;
public class ParseXml {
public static void main(String[] args) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("src/languages.xml");
//print the "type" attribute
Element langs = doc.getDocumentElement();
System.out.println("type = " + langs.getAttribute("type"));
//print the "language" elements
NodeList list = langs.getElementsByTagName("language");
for(int i = 0 ; i < list.getLength();i++) {
Element language = (Element) list.item(i);
System.out.println(language.getTextContent());
}
}catch(ParserConfigurationException pce) {
pce.printStackTrace();
}catch(SAXException se) {
se.printStackTrace();
}catch(IOException ioe) {
ioe.printStackTrace();
}
}
}
将清单2中的Java代码与清单3中的相应Groovy代码进行比较:
清单3.在Groovy中解析XML
def langs = new XmlParser().parse("languages.xml")
println "type = ${langs.attribute("type")}"
langs.language.each{
println it.text()
}
//output:
type = current
Java
Groovy
JavaScript
关于Groovy代码的最好之处不是在于它比等效的Java代码短得多,尽管使用五行Groovy来解析五行XML是一个明显的胜利。 我最喜欢Groovy代码的地方是它更具表现力。 当我编写langs.language.each
,感觉就像我在直接使用XML。 在Java版本中,您再也看不到XML。
字符串变量和XML
当将XML存储在String
变量而不是文件中时,在Groovy中使用XML的好处变得更加明显。 Groovy的三引号(在其他语言中通常称为HereDoc
)使在内部存储XML变得毫不费力,如清单4所示。与清单3的Groovy示例相比,唯一的变化是翻转了parse()
的XmlParser
方法调用(该句柄File
, InputStreams
, Reader
和URI
)到parseText()
。
清单4.在Groovy内部存储XML
def xml = """
<langs type="current">
<language>Java</language>
<language>Groovy</language>
<language>JavaScript</language>
</langs>
"""
def langs = new XmlParser().parseText(xml)
println "type = ${langs.attribute("type")}"
langs.language.each{
println it.text()
}
注意,三重引号可以轻松处理多行XML文档。 xml
变量确实是一个普通的java.lang.String
您可以添加println xml.class
来自己验证。 三重引号还可以处理type="current"
的内部引号,而不必像在Java代码中那样强制您使用反斜杠字符手动对其进行转义。
将清单4中的Groovy代码的简单优雅与清单5中的相应Java代码进行对比:
清单5.用Java代码在内部存储XML
import org.xml.sax.SAXException;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.*;
public class ParseXmlFromString {
public static void main(String[] args) {
String xml = "<langs type=\"current\">\n" +
" <language>Java</language>\n" +
" <language>Groovy</language>\n" +
" <language>JavaScript</language>\n" +
"</langs>";
byte[] xmlBytes = xml.getBytes();
InputStream is = new ByteArrayInputStream(xmlBytes);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(is);
//print the "type" attribute
Element langs = doc.getDocumentElement();
System.out.println("type = " + langs.getAttribute("type"));
//print the "language" elements
NodeList list = langs.getElementsByTagName("language");
for(int i = 0 ; i < list.getLength();i++) {
Element language = (Element) list.item(i);
System.out.println(language.getTextContent());
}
}catch(ParserConfigurationException pce) {
pce.printStackTrace();
}catch(SAXException se) {
se.printStackTrace();
}catch(IOException ioe) {
ioe.printStackTrace();
}
}
}
请注意,内部引号和换行符的转义字符污染了xml
变量。 但是,更麻烦的是必须先将String
转换为byte
数组,然后再转换为ByteArrayInputStream
,然后才能对其进行解析。 奇怪的是, DocumentBuilder
并未提供将简单的String
解析为XML的直接方法。
使用MarkupBuilder
创建XML
Groovy在Java语言上最大的收获是当您想用代码创建XML文档时。 清单6显示了创建五行XML代码段所需的50行Java代码:
清单6.用Java代码创建XML
import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
public class CreateXml {
public static void main(String[] args) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
Element langs = doc.createElement("langs");
langs.setAttribute("type", "current");
doc.appendChild(langs);
Element language1 = doc.createElement("language");
Text text1 = doc.createTextNode("Java");
language1.appendChild(text1);
langs.appendChild(language1);
Element language2 = doc.createElement("language");
Text text2 = doc.createTextNode("Groovy");
language2.appendChild(text2);
langs.appendChild(language2);
Element language3 = doc.createElement("language");
Text text3 = doc.createTextNode("JavaScript");
language3.appendChild(text3);
langs.appendChild(language3);
// Output the XML
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter sw = new StringWriter();
StreamResult sr = new StreamResult(sw);
DOMSource source = new DOMSource(doc);
transformer.transform(source, sr);
String xmlString = sw.toString();
System.out.println(xmlString);
}catch(ParserConfigurationException pce) {
pce.printStackTrace();
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
}
我知道有些人在哭,“犯规!” 马上。 大量的第三方库可以使此代码更简单-JDOM和dom4j是两个流行的库。 但是,没有一个Java库能够像使用Groovy MarkupBuilder
那样简单,如清单7所示:
清单7.用Groovy创建XML
def xml = new groovy.xml.MarkupBuilder()
xml.langs(type:"current"){
language("Java")
language("Groovy")
language("JavaScript")
}
注意,我们回到了代码与XML接近1:1的比例。 更重要的是,我可以再次看到XML。 哦,可以肯定,尖括号被大括号的闭包替换了,并且属性使用冒号(Groovy的HashMap
表示法)而不是等号,但是基本结构可以用Groovy或XML识别。 它几乎就像用于构建XML的DSL,您不觉得吗?
Groovy能够完成此Builder
魔术,因为它是一种动态语言。 另一方面,Java语言是静态的:Java编译器确保所有方法都存在,然后才能调用它们。 (如果尝试调用不存在的方法,则Java代码甚至不会编译,更不用说运行了。)但是Groovy的Builder
证明了一种语言的错误是另一种语言的功能。 如果您查看MarkupBuilder
的API文档,将找不到langs()
方法, language()
方法或任何其他元素名称。 幸运的是,Groovy可以捕获对不存在的方法的这些调用,并用它们进行一些富有成效的工作。 对于MarkupBuilder
,它采用幻像方法调用并生成格式正确的XML。
清单8扩展了我刚刚向您展示的简单MarkupBuilder
示例。 如果要捕获String
变量中的XML输出, MarkupBuilder
StringWriter
传递到MarkupBuilder
的构造函数中。 如果要向langs
添加更多属性,只需将更多属性以逗号分隔即可。 请注意, language
元素的主体是一个值,前面没有名称。 您可以在同一逗号分隔的列表中添加属性和主体。
清单8.扩展的MarkupBuilder
示例
def sw = new StringWriter()
def xml = new groovy.xml.MarkupBuilder(sw)
xml.langs(type:"current", count:3, mainstream:true){
language(flavor:"static", version:"1.5", "Java")
language(flavor:"dynamic", version:"1.6.0", "Groovy")
language(flavor:"dynamic", version:"1.9", "JavaScript")
}
println sw
//output:
<langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
通过这些MarkupBuilder
技巧,您可以朝着有趣的方向发展。 例如,您可以快速构建格式正确HTML文档并将其写出到文件中。 清单9显示了代码:
清单9.使用MarkupBuilder
构建HTML
def sw = new StringWriter()
def html = new groovy.xml.MarkupBuilder(sw)
html.html{
head{
title("Links")
}
body{
h1("Here are my HTML bookmarks")
table(border:1){
tr{
th("what")
th("where")
}
tr{
td("Groovy Articles")
td{
a(href:"http://ibm.com/developerworks", "DeveloperWorks")
}
}
}
}
}
def f = new File("index.html")
f.write(sw.toString())
//output:
<html>
<head>
<title>Links</title>
</head>
<body>
<h1>Here are my HTML bookmarks</h1>
<table border='1'>
<tr>
<th>what</th>
<th>where</th>
</tr>
<tr>
<td>Groovy Articles</td>
<td>
<a href='http://ibm.com/developerworks'>DeveloperWorks</a>
</td>
</tr>
</table>
</body>
</html>
图1显示了清单9中构建HTML的浏览器视图:
图1.呈现HTML
使用StreamingMarkupBuilder
创建XML
MarkupBuilder
非常适合同步构建简单的XML文档。 对于更高级的XML创建,Groovy提供了StreamingMarkupBuilder
。 使用它,您可以使用mkp
帮助器对象添加各种XML附加功能,例如处理指令,名称空间和未转义的文本(对于CDATA
块而言是完美的)。 清单10为您快速浏览了有趣的StreamingMarkupBuilder
功能:
清单10.使用StreamingMarkupBuilder
创建XML
def comment = "<![CDATA[<!-- address is new to this release -->]]>"
def builder = new groovy.xml.StreamingMarkupBuilder()
builder.encoding = "UTF-8"
def person = {
mkp.xmlDeclaration()
mkp.pi("xml-stylesheet": "type='text/xsl' href='myfile.xslt'" )
mkp.declareNamespace('':'http://myDefaultNamespace')
mkp.declareNamespace('location':'http://someOtherNamespace')
person(id:100){
firstname("Jane")
lastname("Doe")
mkp.yieldUnescaped(comment)
location.address("123 Main")
}
}
def writer = new FileWriter("person.xml")
writer << builder.bind(person)
//output:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type='text/xsl' href='myfile.xslt'?>
<person id='100'
xmlns='http://myDefaultNamespace'
xmlns:location='http://someOtherNamespace'>
<firstname>Jane</firstname>
<lastname>Doe</lastname>
<![CDATA[<!-- address is new to this release -->]]>
<location:address>123 Main</location:address>
</person>
注意,在调用bind()
方法之前, StreamingMarkupBuilder
不会生成最终的XML,并传入包含标记和所有指令的闭包。 这使您可以异步构建XML文档的各个部分,然后一次全部输出。
了解XmlParser
Groovy为您提供了两种生成XML的方法MarkupBuilder
和StreamingMarkupBuilder
每种都有不同的功能。 解析XML同样如此。 您可以使用XmlParser
或XmlSlurper
。
XmlParser
提供了以程序员为中心的XML文档视图。 如果您习惯于用List
和Map
分别考虑文档(分别适用于Element
和Attribute
),那么您应该习惯XmlParser
。 清单11 XmlParser
解构了XmlParser
:
清单11.仔细研究XmlParser
def xml = """
<langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
"""
def langs = new XmlParser().parseText(xml)
println langs.getClass()
// class groovy.util.Node
println langs
/*
langs[attributes={type=current, count=3, mainstream=true};
value=[language[attributes={flavor=static, version=1.5};
value=[Java]],
language[attributes={flavor=dynamic, version=1.6.0};
value=[Groovy]],
language[attributes={flavor=dynamic, version=1.9};
value=[JavaScript]]
]
]
*/
请注意, XmlParser.parseText()
方法返回一个groovy.util.Node
,在本例中为XML文档的根Node
。 当您调用println langs
,它将调用Node.toString()
方法,并返回调试输出。 要获取真实数据,您需要调用Node.attribute()
或Node.text()
。
使用XmlParser
获取属性
如前所述,可以通过调用Node.attribute("key")
获得单个属性。 如果调用Node.attributes()
,它将返回一个包含所有Node
属性的HashMap
。 使用您在“ 实现每个目标 ”中了解到的each
闭包,遍历每个属性都没什么大不了的。 有关此示例,请参见清单12。
清单12. XmlParser
将属性视为HashMap
def langs = new XmlParser().parseText(xml)
println langs.attribute("count")
// 3
langs.attributes().each{k,v->
println "-" * 15
println k
println v
}
//output:
---------------
type
current
---------------
count
3
---------------
mainstream
true
与使用属性一样, XmlParser
甚至为处理元素提供了更XmlParser
支持。
使用XmlParser
获取元素
XmlParser
提供了一种查询元素的直观方法,称为GPath
。 (它类似于XPath
,仅在Groovy中实现。)例如,清单13演示了我之前使用的langs.language
构造返回了一个包含查询结果的groovy.util.NodeList
。 NodeList
扩展了java.util.ArrayList
,因此它基本上是一个被授予GPath
超能力的List
。
清单13.使用GPath
和XmlParser
查询
def langs = new XmlParser().parseText(xml)
// shortcut query syntax
// on an anonymous NodeList
langs.language.each{
println it.text()
}
// separating the query
// and the each closure
// into distinct parts
def list = langs.language
list.each{
println it.text()
}
println list.getClass()
// groovy.util.NodeList
GPath
当然是MarkupBuilder
的补充。 它使用相同的技巧来调用不存在的幻像方法,只是这次仅用于查询现有XML,而不是即时生成它。
知道GPath
查询的结果是一个List
,您可以使代码更加简洁。 Groovy提供了一个散点运算符。 在单行代码中,它实质上遍历列表并在每个项目上执行方法调用。 结果以List
形式返回。 例如,如果您关心的Node.text()
在查询结果中的每个项目上调用Node.text()
方法,清单14展示了如何在一行代码中进行操作:
清单14.将spread-dot运算符与GPath
// the long way of gathering the results
def results = []
langs.language.each{
results << it.text()
}
// the short way using the spread-dot operator
def values = langs.language*.text()
// [Java, Groovy, JavaScript]
// quickly gathering up all of the version attributes
def versions = langs.language*.attribute("version")
// [1.5, 1.6.0, 1.9]
与XmlParser
功能强大, XmlSlurper
将其提升到一个新的水平。
使用XmlSlurper
解析XML
在清单2中 ,我曾说过Groovy使我感觉就像直接在使用XML。 XmlParser
功能非常强大,但是它仍然使您可以通过编程方式处理XML。 您将Node.attribute()
Node
的List
和Attribute
的HashMap
,并且仍然被迫调用诸如Node.attribute()
和Node.text()
来获取核心数据。 XmlSlurper
消除了方法调用的最后痕迹,给您一种直接处理XML的令人愉悦的幻想。
从技术上讲, XmlParser
返回Node
和NodeList
,而XmlSlurper
返回groovy.util.slurpersupport.GPathResult
。 但是现在您知道了,我希望您忘记我曾经提到过XmlSlurper
的实现细节。 如果您不偷看窗帘,就可以尽情享受魔术表演的乐趣。
清单15 XmlParser
显示了XmlParser
和XmlSlurper
:
清单15. XmlParser
和XmlSlurper
def xml = """
<langs type='current' count='3' mainstream='true'>
<language flavor='static' version='1.5'>Java</language>
<language flavor='dynamic' version='1.6.0'>Groovy</language>
<language flavor='dynamic' version='1.9'>JavaScript</language>
</langs>
"""
def langs = new XmlParser().parseText(xml)
println langs.attribute("count")
langs.language.each{
println it.text()
}
langs = new XmlSlurper().parseText(xml)
println langs.@count
langs.language.each{
println it
}
请注意, XmlSlurper
会删除任何方法调用的概念。 而不是调用langs.attribute("count")
,而是调用langs.@count
。 从XPath
借来了at符号( @
),但结果是产生了一种幻想,即您直接使用该属性而不是调用attribute()
方法。 it.text()
调用it.text()
,而只需调用it
。 假设您要直接使用元素的内容。
现实世界中的XmlSlurper
超越langs
和language
,这里是XmlSlurper
的实际示例。 雅虎! 通过邮政编码作为RSS feed提供当前的天气状况。 RSS当然是XML的一种特殊方言。 在Web浏览器中键入http://weather.yahooapis.com/forecastrss?p=80020
。 随意将自己的邮政编码替换为科罗拉多州布鲁姆菲尔德。 清单16显示了生成的RSS feed的简化版本:
清单16. Yahoo! RSS feed显示当前天气状况
<rss version="2.0"
xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0"
xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#">
<channel>
<title>Yahoo! Weather - Broomfield, CO</title>
<yweather:location city="Broomfield" region="CO" country="US"/>
<yweather:astronomy sunrise="6:36 am" sunset="5:50 pm"/>
<item>
<title>Conditions for Broomfield, CO at 7:47 am MST</title>
<pubDate>Fri, 27 Feb 2009 7:47 am MST</pubDate>
<yweather:condition text="Partly Cloudy"
code="30" temp="25"
date="Fri, 27 Feb 2009 7:47 am MST" />
</item>
</channel>
</rss>
您需要做的第一件事是以编程方式使用此RSS。 创建一个名为weather.groovy的文件,并添加清单17中所示的代码:
清单17.以编程方式获取RSS
def baseUrl = "http://weather.yahooapis.com/forecastrss"
if(args){
def zip = args[0]
def url = baseUrl + "?p=" + zip
def xml = url.toURL().text
println xml
}else{
println "USAGE: weather zipcode"
}
在命令行中键入groovy weather 80020
以验证您可以看到原始的RSS。
该脚本最重要的部分是url.toURL().text
。 url
变量是格式良好的String
。 Groovy向所有String
都添加了toURL()
方法,该方法将它们转换为java.net.URL
。 依次,所有URL
都有一个由Groovy添加的getText()
方法,该方法执行HTTP GET
请求并将结果作为String
返回。
现在,您已经将RSS存储在xml
变量中,将XmlSlurper
混入一点以挑选出有趣的位,如清单18所示:
清单18.使用XmlSlurper
解析RSS
def baseUrl = "http://weather.yahooapis.com/forecastrss"
if(args){
def zip = args[0]
def url = baseUrl + "?p=" + zip
def xml = url.toURL().text
def rss = new XmlSlurper().parseText(xml)
println rss.channel.title
println "Sunrise: ${rss.channel.astronomy.@sunrise}"
println "Sunset: ${rss.channel.astronomy.@sunset}"
println "Currently:"
println "\t" + rss.channel.item.condition.@date
println "\t" + rss.channel.item.condition.@temp
println "\t" + rss.channel.item.condition.@text
}else{
println "USAGE: weather zipcode"
}
//output:
Yahoo! Weather - Broomfield, CO
Sunrise: 6:36 am
Sunset: 5:50 pm
Currently:
Fri, 27 Feb 2009 7:47 am MST
25
Partly Cloudy
看看XmlSlurper
使XML处理变得多么自然吗? 您可以直接引用<title>
元素rss.channel.title
来打印它。 您可以使用简单的rss.channel.item.condition.@temp
剥离temp
属性。 这感觉不像编程。 感觉就像直接使用XML。
您是否注意到XmlSlurper
甚至忽略了名称空间? 您可以在构造函数中打开名称空间意识,但是我很少这样做。 开箱即用, XmlSlurper
像通过黄油XmlSlurper
的热刀一样切成XML切片。
结论
要成为当今这样一个成功的软件开发人员,您需要一套使XML处理变得轻松的工具。 Groovy的MarkupBuilder
和StreamingMarkupBuilder
使动态创建XML变得轻而易举。 XmlParser
很好地完成了为您提供Element
的List
和Attribute
的HashMap
的工作,而XmlSlurper
使代码完全消失,给您一种直接使用XML的令人愉悦的幻想。
没有Groovy的动态功能,XML处理的大部分功能将无法实现。 在下一篇文章中,我将与您详细探讨Groovy的动态本质。 您将学习到Groovy List.each()
编程的工作原理,从添加到标准JDK类(如String.toURL()
和List.each()
)的炫酷方法,到您将自己添加的自定义方法。 在此之前,我希望您能找到Groovy的许多实际用途。
翻译自: https://www.ibm.com/developerworks/java/library/j-pg05199/index.html