XML对元素名称的唯一性没有全局约束,一份文档完全可以同时出现两个叫<table>的元素:一个描述网页里的表格,另一个记录家具库存里的餐桌。解析器面对这种情况只能靠上下文去猜,应用程序更是无从下手。XML命名空间正是W3C为解决这类冲突而设计的一套机制,它用URI把元素和属性划分进彼此隔离的名称分组,让同名元素在不同分组中各自保持独立含义。理解这套机制,是读懂XSD、XSLT、SOAP这些主流XML技术的前提。

什么是XML命名空间:从一次真实的名称冲突说起
设想一个电商系统需要把家具库存数据和页面展示描述合并在同一份XML文档里。库存模块定义了<table>元素表示餐桌,包含<名称>和<价格>子元素;页面模块同样定义了<table>元素表示表格布局,包含<tr>、<td>子元素。两份数据拼在一起后,问题立刻暴露:
<数据汇总>
<库存信息>
<table>
<名称>实木餐桌</名称>
<价格>1299</价格>
</table>
</库存信息>
<页面描述>
<table>
<tr><td>商品名称</td><td>价格</td></tr>
</table>
</页面描述>
</数据汇总>从人的视角看,凭借子元素结构还能勉强分辨两个<table>的含义,但机器做不到。DTD校验会直接报错,因为同名元素只允许一套内容模型;XPath表达式/数据汇总/table的语义变得含混不清;把这份文档交给第三方系统处理时,对方更无从知晓每个<table>究竟该按哪套规则解析。名称冲突不是理论问题,而是XML走向数据交换标准时必须迈过的坎。
命名空间的解决思路很直接:给每套元素定义贴上一个全局唯一的身份标识,这个标识就是URI。一个元素经过命名空间限定后,它的完整名称由两部分组成,即命名空间URI加上本地名称。http://www.ipipp.com/furniture下的table和http://www.w3.org/1999/xhtml下的table,在解析器眼里是两个完全不同的元素,冲突自然消解。W3C在命名空间推荐标准中定义了这套机制,它不改变XML的基本语法,只是在元素和属性名称之上叠加了一层分组逻辑。
xmlns声明、前缀绑定与默认命名空间的使用方法
命名空间通过xmlns或xmlns:前缀形式的属性声明在元素上。带前缀的写法把一个短前缀绑定到某个URI,该元素及其后代中所有以这个前缀开头的元素名称,都会被划入对应的命名空间:
<数据汇总 xmlns:furn="http://www.ipipp.com/furniture"
xmlns:html="http://www.w3.org/1999/xhtml">
<furn:table>
<furn:名称>实木餐桌</furn:名称>
<furn:价格>1299</furn:价格>
</furn:table>
<html:table>
<html:tr><html:td>商品名称</html:td><html:td>价格</html:td></html:tr>
</html:table>
</数据汇总>这里furn:table的完整名称是命名空间URI加本地名table,html:table则属于另一个命名空间,两者互不干扰。前缀可以自由命名,只要在文档内不把同一个前缀绑定到不同URI即可。声明可以写在任何元素上,作用范围是该元素及其所有后代节点,这也是前缀只在需要的局部生效的原因。
如果一个范围内绝大多数元素都属于同一命名空间,逐个写前缀会非常啰嗦,这时可以声明默认命名空间。不带前缀的xmlns属性会把该元素及后代中所有无前缀元素划入指定命名空间:
<商品目录 xmlns="http://www.ipipp.com/furniture">
<table>
<名称>实木餐桌</名称>
<价格>1299</价格>
</table>
<页面片段 xmlns="http://www.w3.org/1999/xhtml">
<table>
<tr><td>商品名称</td></tr>
</table>
</页面片段>
</商品目录>根元素把家具命名空间声明为默认,所以外层的<table>、<名称>无需前缀;<页面片段>内部重新声明了默认命名空间,其中的<table>就换到了网页表格的分组。默认命名空间可以随时被后代元素覆盖,也可以显式置空来回到无命名空间状态,写法是xmlns=""。需要特别记住一点:默认命名空间只作用于元素,对不带前缀的属性没有任何影响,这一点后面还会展开。
URI只需唯一不必可访问:命名空间在Schema与XPath中的实际应用
初学者常有的疑问是:命名空间URI指向的网页打不开,是不是写错了?答案是否定的。URI在命名空间机制中只充当身份标识,解析器从不尝试访问它,唯一性才是唯一的要求。用域名部分做URI是行业惯例,因为域名注册机制天然保证了不同组织不会撞车,但urn:ipipp:furniture这样的URN格式同样合法。判断两个命名空间是否相同,比较的只是字符串本身。
实际开发中一批固定URI已经成了事实标准,见到它们就能大致判断文档的类型:
| 常见前缀 | 命名空间URI | 典型用途 |
|---|---|---|
| xml | http://www.w3.org/XML/1998/namespace | XML内置,如xml:lang |
| xs | http://www.w3.org/2001/XMLSchema | Schema定义语言本身 |
| xsi | http://www.w3.org/2001/XMLSchema-instance | 实例文档引用Schema |
| xsl | http://www.w3.org/1999/XSL/Transform | XSLT样式表 |
Schema校验是命名空间应用最密集的场景。XSD文档自身用xs前缀引用Schema定义语言,同时通过targetNamespace声明自己定义的元素属于哪个命名空间,elementFormDefault设为qualified则要求实例文档中被定义的元素必须带命名空间限定:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.ipipp.com/furniture"
elementFormDefault="qualified">
<xs:element name="table">
<xs:complexType>
<xs:sequence>
<xs:element name="名称" type="xs:string"/>
<xs:element name="价格" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>实例文档要接受这份Schema校验,根元素就必须声明对应的命名空间,否则校验器会认为文档与Schema毫无关系。同理,用XPath查询带命名空间的文档时,表达式中也必须使用前缀,并且前缀到URI的映射要提前注册到查询环境里,否则查询结果会意外为空:
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
// 把前缀furn映射到家具命名空间的URI
xpath.setNamespaceContext(new NamespaceContext() {
@Override
public String getNamespaceURI(String prefix) {
if ("furn".equals(prefix)) {
return "http://www.ipipp.com/furniture";
}
return null;
}
@Override
public String getPrefix(String uri) {
return null;
}
@Override
public Iterator getPrefixes(String uri) {
return Collections.emptyIterator();
}
});
// 表达式中的furn前缀依赖上面的映射才能生效
NodeList result = (NodeList) xpath.evaluate(
"/furn:商品目录/furn:table/furn:价格", document, XPathConstants.NODESET);这是无数开发者踩过的坑:XPath表达式/商品目录/table/价格在无命名空间文档上工作正常,一旦文档声明了默认命名空间就查不到任何节点,因为无前缀的路径步骤匹配的是无命名空间元素。解决办法就是注册映射并改用带前缀的表达式。Java、C#、Python的各类XML库都提供了类似的命名空间上下文注册接口,思路完全一致。
使用命名空间时的常见误区与注意事项
第一个高频误区是以为默认命名空间同样覆盖属性。看下面的例子:
<商品目录 xmlns="http://www.ipipp.com/furniture"
xmlns:ext="http://www.ipipp.com/extension">
<table 版本="2.0" ext:编号="A-100">
<名称>实木餐桌</名称>
</table>
</商品目录>元素<table>和<名称>属于默认命名空间,属性ext:编号属于extension命名空间,而属性版本不属于任何命名空间。规范明确规定:无前缀的属性永远没有命名空间,即使所在元素处于默认命名空间中。如果希望属性被命名空间限定,就必须显式使用前缀。理解这条规则,可以避免在属性匹配、Schema属性声明上反复出错。
第二个误区是把前缀当成命名空间本身。前缀只是文档内的本地别名,真正起标识作用的是URI。下面两行声明指向同一个URI,因此两个table元素属于同一个命名空间,对解析器而言完全等价:
<!-- 文档A使用英文前缀 --> <furn:table xmlns:furn="http://www.ipipp.com/furniture"/> <!-- 文档B使用中文前缀,与文档A的元素属于同一命名空间 --> <家具:table xmlns:家具="http://www.ipipp.com/furniture"/>
比较元素时,工具比较的是展开后的完整名称,即URI加本地名。这意味着跨系统交换文档时,即使双方约定的前缀不同,只要URI一致就能正确互认。反过来,如果两个文档前缀相同但URI不同,对应的元素反而毫无关系。
还有几条细节值得留意。xml前缀绑定到固定的内置命名空间,xmlns前缀被保留用于声明本身,两者都不允许绑定到其他URI。命名空间声明会随元素树继承,后代元素自动处于祖先声明的命名空间中,除非重新声明。此外,虽然URI不必可访问,但建议在对应的域名下放一份说明文档,描述这个命名空间定义了哪些元素,方便协作方理解,这是许多标准组织的通行做法。
掌握命名空间后回头看,XML生态里那些看似复杂的文档头,XSD里的targetNamespace、SOAP信封上的soapenv前缀、Spring配置文件里的多个xmlns声明,其实都在做同一件事:用URI为名称划界。想清楚这一点,阅读和编写任何基于XML的配置与数据格式都会顺畅许多。