前言
百度百科解释
可扩展标记语言 (Extensible Markup Language, XML) ,标准通用标记语言的子集,可以用来标记数据、定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言。 XML是标准通用标记语言 可扩展性良好,内容与形式分离,遵循严格的语法要求,保值性良好等优点。
XML是一种独立于软件和硬件的工具,用于存储和传输数据。XML代表可扩展标记语言,是一种与HTML非常相似的标记语言,被设计用于存储和传输数据,XML被设计为自描述的, XML是W3C推荐标准
1 笔者理解
如上所述,项目里设计之初是为了存储和传输数据,但是经过互联网的迅速迭代发展,因为xml报文的各种问题,xml在数据传输上面已经逐渐被另外一种报文协议----json所取代。
1.1 xml值得学习的原因
xml还是值得花费精力学习一下,有以下几点原因
- 尽管迅速发展的互联网导致很多新的项目报文交互用的是json,但是xml在一些老项目中仍然存在,当维护到老项目的时候,需要xml相关知识技能
- xml在报文传输上面不占优势,但是因为xml可自定义语法规范,避免使用框架的人笔误等原因配置错误,在很多开源框架的配置选择上面xml是很活跃的,常见的有mybatis,spring,zookeeper等等,基本很多流行的框架里面,都可以看到xml。
- 甚至笔者自己的公司,在做框架配置选择的时候,也是选择xml定义语法规范,使用者在配置的时候,会有提示和校验,提高开发和排查问题效率
1.2 xml语法
1.2.1 语法简介
下面我们通过一个简单的例子,介绍xml的语法
<note>
<from>张大妈</from>
<to>小明</to>
<title encoding="gbk">放学回家吃饭</title>
<body>
今天做了,红烧肉,放学别贪玩,赶紧回家吃饭.
</body>
</note>
通过上面例子可以看到,XML保存的不只是数据,还有数据之间的结构。
<标记 属性名="属性值">元素内容</标记>
在上面的例子中:
- note,title 叫做标签,标记
- encoding 叫属性
- gbk 叫属性值
- 放学回家吃饭 叫元素内容
xml语法有以下约束:
- 所有XML元素都必须有结束标签
- XML标签对大小写敏感
- XML必须正确的嵌套
- 同级标签以缩进对齐
- 元素名称可以包含字母、数字或其他的字符
- 元素名称不能以数字或者标点符号开始
- 元素名称中不能含空格
- 属性值用双引号包裹
- 一个元素可以有多个属性
- 属性值中不能直接包含<、“、&(不建议:‘、>)
1.2.2 转义字符
上面提到xml编辑有很多约束,其中要求在属性值中不能直接包含< >等,因为这些符号会被xml识别为标签,那么如果我们真实需求就是属性值中包含这些特殊字符,此时只能通过转义符替代特殊字符,使得xml能正确识别。转义在很多语言或者语法规则中都有涉及,比如html,json等。
当属性值包含的特殊字符太多时,我们逐个字符转义比较麻烦,此时有另外一种方式可以选择:
可以使用CDATA节,如:
<description>
<![CDATA[讲解了元素<title>以及</title>的使用]]>
</description>
CDATA 部分中的所有内容都会被解析器忽略。CDATA 部分由 “<![CDATA[" 开始,由 "]]>” 结束:
1.2.3 命名空间
菜鸟教程关于命名空间介绍
1.3 xml和json比较
1.3.1 xml
1.3.1.1 优点
- 格式统一,符合标准
- 可以自定义相关报文规范语法,为很多开源框架所使用
1.3.1.2 缺点
- 比较重,XML文件庞大,文件格式复杂,传输占带宽
- 服务器端和客户端都需要花费大量代码来解析XML,导致服务器端和客户端代码变得异常复杂且不易维护
- 客户端不同浏览器之间解析 XML 的方式不一致,需要重复编写很多代码
- 服务器端和客户端解析 XML 花费较多的资源和时间
1.3.2 json
1.3.2.1 优点
- 与xml相比,灵活性很高,数据格式比较简单,易于读写,格式都是压缩的,占用带宽小
- 易于解析,客户端JavaScript可以简单的通过eval()进行JSON数据的读取
- 因为JSON格式能直接为服务器端代码使用,大大简化了服务器端和客户端的代码开发量,且完成任务不变,并且易于维护
- 主要用于系统间报文交互
1.3.2.2 缺点
- json 比xml 灵活,易于使用,这是优点,但是换个角度也会是缺点,因为没有想过规范约束,只要满足json的语法规则都可以执行通过,如果需要在json上面做一些配置限制,就没有xml方便(现在市面上也有一些插件,尝试实现json的配置约束管理,但是还不流行)
- 不利于用于配置文件管理
2 使用
下面我们介绍使用开源类库编辑,修改,查询xml。
2.1 dom解析
2.1.1 dom解析简介
基于DOM解析的xml分析器是将其转换为一个对象模型的集合,在内存中形成一个dom树,用树这种数据结构对信息进行储存。通过DOM接口,应用程序可以在任何时候访问xml文档中的任何一部分数据,因此这种利用DOM接口访问的方式也被称为随机访问。
2.1.2 dom解析优点
dom树在内存中,速度快
2.1.3 dom解析缺点
在解析大文档的时候,消耗大量内存
2.1.4 dom解析XML文件步骤
- 创建解析器工厂对象
- 解析器工厂对象创建解析器对象
- 解析器对象指定XML文件创建Document对象
- 以Document对象为起点操作DOM树
2.1.4.1 添加pom
<dependency>
<groupId>org.w3c</groupId>
<artifactId>dom</artifactId>
<version>2.3.0-jaxb-1.0.6</version>
</dependency>
2.1.4.2 样例demoxml
下面测试以这个xml报文为例
<?xml version="1.0" encoding="utf-8" standalone="no"?><phoneInfo>
<brand name="华为手机">
<type name="华为荣耀"/>
<type name="HW123"/>
<type name="RY321"/>
</brand>
<brand name="小米手机">
<type name="小米10"/>
<type name="红米"/>
<type name="Rednote"/>
</brand>
<brand name="苹果手机">
<type name="iphone7" />
<type name="iphone8" />
<type name="iphone9" />
</brand>
</phoneInfo>
2.1.4.3 查询,保存,增删改节点
package com.wanlong.xml;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
/**
* @author wanlong
* @version 1.0
* @description:
* @date 2023/4/17 10:07
*/
public class DomTest {
static Document document = null;
@BeforeClass
public static void initdocument() throws Exception {
//1.创建解析器工厂对象
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//2.解析器工厂对象创建解析器对象
DocumentBuilder db = dbf.newDocumentBuilder();
//3.解析器对象指定XML文件创建Document对象,这里可以写绝对路径
document = db.parse("test.xml");
//4. 以Document对象为起点操作DOM树
}
//查询
@Test
public void print() {
NodeList brands = document.getElementsByTagName("brand");
//遍历brands集合
for (int i = 0; i < brands.getLength(); i++) {
//获取brands集合元素,返回值Node节点类型
Node node = brands.item(i);
//向下转型:将Node节点类型转换成真正的类型Element元素节点
Element brand = (Element) node;
//getAttribute("属性名"):通过属性名返回属性值
String nameValue = brand.getAttribute("name");
System.out.println(nameValue);
//getChildNodes():获取brand元素节点的子节点,这个子节点可能不止一个,返回的是NodeList集合
NodeList types = brand.getChildNodes();
//遍历子节点types集合
for (int j = 0; j < types.getLength(); j++) {
//获取子节点集合元素
Node typeNode = types.item(j);
//要做判断,确保获取的子节点是元素节点
//ELEMENT_NODE:元素节点静态常量,表示1
if (typeNode.getNodeType() == Element.ELEMENT_NODE) {
Element type = (Element) typeNode;
String typeValue = type.getAttribute("name");
System.out.println("\t" + typeValue);
}
}
}
}
//保存xml
@Test
public void save() {
String path="test2.xml";
//第一步:创建引用
//创建TransformerFactory对象引用
TransformerFactory transformerFactory = TransformerFactory.newInstance();
try {
//创建Transformer对象引用
Transformer transformer = transformerFactory.newTransformer();
//在转换前先设置编码类型格式
/*
* setOutputProperty(String name, String value):设置编码类型,包括属性、属性值
* OutputKeys:提供可用于设置 Transformer的输出属性
* OutputKeys.ENCODING:静态常量,等于encoding
*/
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");//encoding="UTF-8"
//第二步:把DOM转换为XML文件:transform(Source xmlSource, Result outputTarget)方法
//DOMSource类是Source接口的实现类,创建指定DOM树
DOMSource domSource = new DOMSource(document);
//StreamResult类是Result接口的实现类,StreamResult(OutputStream)方法:需要传递的参数是字节流
StreamResult result = new StreamResult(new FileOutputStream(path));
//传递的参数是Source类型和Result类型
transformer.transform(domSource, result);
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
//添加节点
@Test
public void addNode() {
//创建brand节点,createElement():创建元素节点
Element brandElement = document.createElement("brand");
//setAttribute(String name,String value):设置属性名、属性值
brandElement.setAttribute("name", "苹果手机");
//创建type节点
Element typeElement = document.createElement("type");
typeElement.setAttribute("name", "iphone14");
//appendChild(Node newChild):添加父子关系
brandElement.appendChild(typeElement);
//获取第一个元素节点:先获取元素节点集合,再获取一个元素
Element phoneInfoElement = (Element) document.getElementsByTagName("phoneInfo").item(0);
//添加父子关系
phoneInfoElement.appendChild(brandElement);
//添加完毕之后,此时的数据还在内存中,需要进一步保存到XML文件中
save();
}
@Test
public void updateNode() {
//获取brand元素节点集合
NodeList brands = document.getElementsByTagName("brand");
//遍历集合
for (int i = 0; i < brands.getLength(); i++) {
//获取brandElement元素节点
Element brandElement = (Element) brands.item(i);
//通过属性名获得brand元素节点属性值
String brandName = brandElement.getAttribute("name");
//做个判断,如果属性值是苹果手机,重新设置属性值
if (brandName.equals("苹果手机")) {
brandElement.setAttribute("name", "IPhone");
}
}
//修改好后,需要保存到指定的XML文件中
save();
}
//删除
//删除XML文件内容的方法
@Test
public void deleteXml() {
NodeList brandList = document.getElementsByTagName("brand");
//遍历brandList集合
for (int i = 0; i < brandList.getLength(); i++) {
Element brandElement = (Element) brandList.item(i);
String brandName = brandElement.getAttribute("name");
//如果属性名是IPhone,则删除这个brand节点
if (brandName.equals("苹果手机")) {
//通过父元素节点移除子节点
brandElement.getParentNode().removeChild(brandElement);
}
}
//删除后需要保存
save();
}
}
2.2 sax解析
2.2.1 sax解析简介
SAX解析不像DOM那样建立一个完整的文档树,而是在读取文档时激活一系列事件,这些事件被推给事件处理器,然后由事件处理器提供对文档内容的访问。常见的事件处理器有三种基本类型:
- 用于访问XML DTD内容的DTDHandler;
- 用于低级访问解析错误的ErrorHandler;
- 用于访问文档内容的ContentHandler,这也是最普遍使用的事件处理器。解析器读取输入文档并在处理文档时将每个事件推给文档处理器(MyContentHandler)。
2.2.2 sax优点
- 与DOM相比,SAX解析器能提供更好的性能优势,它提供对XML文档内容的有效低级访问。SAX模型最大的优点是内存消耗小,因为整个文档无需一次加载到内存中,这使SAX解析器可以解析大于系统内存的文档。
- 无需像在DOM中那样为所有节点创建对象。最后,SAX“推”模型可用于广播环境,能够同时注册多个ContentHandler,并行接收事件,而不是在一个管道中一个接一个地进行处理。
- 只需要单遍读取内容的应用程序可以从SAX解析中大大受益。很多B2B和EAI应用程序将XML用做封装格式,接收端用这种格式简单地接收所有数据。这就是SAX明显优于DOM的地方:因高效而获得高吞吐率。在SAX 2.0 中有一个内置的过滤机制,可以很轻松地输出一个文档子集或进行简单的文档转换。
2.2.3 sax缺点
- 必须实现多个事件处理程序以便能够处理所有到来的事件,同时你还必须在应用程序代码中维护这个事件状态,因为SAX解析器不能交流元信息,如DOM的父/子支持,所以你必须跟踪解析器处在文档层次的哪个位置。
- 文档越复杂,应用逻辑就越复杂。虽然没有必要一次将整个文档加载到内存中,但SAX解析器仍然需要解析整个文档,这点和DOM一样。
- 也许SAX面临的最大问题是它没有内置如XPath所提供的那些导航支持。再加上它的单遍解析,使它不能支持随机访问。这一限制也表现在命名空间上: 对有继承名字空间的元素不做注解。这些限制使SAX很少被用于操作或修改文档。
2.2.4 sax解析文件步骤
- 得到SAX解析工厂(SAXParserFactory)
- 由解析工厂生产一个SAX解析器(SAXParser)
- 由xmlParser获取一个xmlReader
- 传入输入流和handler给xmlReader,调用parse()解析
2.2.4.1 样例xml
<?xml version="1.0" encoding="UTF-8"?>
<Book>
<book id="testId">
<name>斗破苍穹</name>
<author>天蚕土豆</author>
</book>
<book id="testId2">
<name>神墓</name>
<author>辰东</author>
</book>
</Book>
2.2.4.2 sax解析实现查询
package com.wanlong.xml;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.util.ArrayList;
import java.util.List;
/**
* @author wanlong
* @version 1.0
* @description:
* @date 2023/4/17 13:32
*/
public class SaxTest {
static XMLReader xmlReader=null;
@BeforeClass
public static void init() throws Exception{
//1.得到SAX解析工厂(SAXParserFactory)
SAXParserFactory factory = SAXParserFactory.newInstance();
//2. 由解析工厂生产一个SAX解析器(SAXParser)
SAXParser saxParser = factory.newSAXParser();
// 获取xmlReader
xmlReader = saxParser.getXMLReader();
}
@Test
public void test() throws Exception{
// 注册自定义解析器
MyHandler myHander = new MyHandler();
//传入输入流和handler给解析器,调用parse()解析
xmlReader.setContentHandler(myHander);
// 解析xml ,这里注意,如果用junit测试,相对路径的话,需要将文件放到测试目录根目录
xmlReader.parse(this.getClass().getClassLoader().getResource("SaxTest.xml").getFile());
// 获取解析结果
List<Book> bookList =myHander.getBookList();
System.out.println(bookList);
}
// 这个事件解析器要完成的职责就是如果读取到开始节点是Book,则创建一个list,然后如果是book节点,则创建一个Book实体,
// 并且将id,name,author赋值给这个book实体,characters可以区分当前文本内容是name还是author是通过currentName去处理的
class MyHandler extends DefaultHandler {
private List<Book> bookList;
private Book book;
private String currentName;
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// characters处理节点text内容,类似 道斩乾坤 这种
if (currentName.equals("name")) {
String s = new String(ch, start, length);
book.setName(s);
} else if (currentName.equals("author")){
String s = new String(ch, start, length);
book.setAuthor(s);
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
currentName = qName;
// qName是element名字,类似Book
if (qName.equals("Book")) {
bookList = new ArrayList<Book>();
}
if (qName.equals("book")) {
book = new Book();
// attributes 是element的属性,类似id这种
String id = attributes.getValue("id");
book.setId(Long.valueOf(id));
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
currentName = "";
// endElement 表示这个节点解析结束
if (qName.equals("book")) {
bookList.add(book);
}
}
public List<Book> getBookList() {
return bookList;
}
public void setBookList(List<Book> bookList) {
this.bookList = bookList;
}
}
class Book {
private Long id;
private String name;
private String author;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
'}';
}
}
}
2.3 dom4j 解析(推荐)
2.3.1 dom4j解析简介
1.Dom4j是一个简单、灵活的开放源代码的库。Dom4j是由早期开发JDOM的人分离出来而后独立开
发的。与JDOM不同的是,dom4j使用接口和抽象基类,虽然Dom4j的API相对要复杂一些,但它提
供了比JDOM更好的灵活性。
2.Dom4j是一个非常优秀的Java XML API,具有性能优异、功能强大和极易使用的特点。现在很
多软件采用的Dom4j,例如Hibernate,包括sun公司自己的JAXM也用了Dom4j。
3.使用Dom4j开发,需下载dom4j相应的jar文件。
2.3.2 dom4j优点
- 性能优异
- 功能强大
- 极端易用
2.3.3 dom4j缺点
- 因为使用的是第三方包,代码可移植性差,需要目标项目也使用dom4j
- 和dom一样,需要读取完整文档在内存中形成dom树,对内存有要求
2.3.4 dom4j解析文件步骤
2.3.4.1 添加依赖
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
2.3.4.2 样例demo
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<phoneInfo>
<brand name="华为手机">
<type name="华为荣耀"/>
<type name="HW123"/>
<type name="RY321"/>
</brand>
<brand name="小米手机">
<type name="小米10"/>
<type name="红米"/>
<type name="Rednote"/>
</brand>
<brand name="苹果手机">
<type name="iphone7" />
<type name="iphone8" />
<type name="iphone9" />
</brand>
</phoneInfo>
2.3.4.3 api使用
2.3.4.3.1 创建document三种方式
public static void createDocument() throws Exception {
//1.读取XML文件,获得document对象
SAXReader reader = new SAXReader();
document = reader.read(new File("input.xml"));
//2.解析XML形式的文本,得到document对象.
String text = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?><phoneInfo><brand name=\"华为手机\"><type name=\"华为荣耀\" /><type name=\"HW123\" /><type name=\"RY321\" /></brand></phoneInfo>";
document = DocumentHelper.parseText(text);
//3.主动创建空document对象.
document = DocumentHelper.createDocument();
//创建根节点
Element root = document.addElement("members");
}
2.3.4.3.2 保存文件
public void saveXml(Document document) throws Exception {
OutputFormat format = OutputFormat.createPrettyPrint();
// 指定XML编码
format.setEncoding("utf-8");
XMLWriter writer = new XMLWriter(new FileWriter("output.xml"), format);
writer.write(document);
writer.close();
}
2.3.4.3.3 元素增删改查
public void processElement() {
//获取文档的元素.
Element root = document.getRootElement();
//获取某个元素的指定名称的第一个子节点.
Element element = root.element("brand");
//获取某个元素的指定名称的所有子元素的集合
List list = root.elements("brand");
//添加一个指定名称的子元素
Element childEle = root.addElement("brand");
//删除某个元素指定的子元素
root.remove(childEle);
//属性Attribute操作,获取某个元素的指定名称的属性对象
Attribute attr = element.attribute("name");
//获取某个元素的指定名称的属性值
String name = element.attributeValue("name");
//给元素添加属性或更新其值
element.addAttribute("id", "123");
//删除某个元素的指定属性
element.remove(attr);
//文本Text的操作
// 获取某个元素的文本内容
String text = element.getText();
//给某个元素添加或更新文本内容
element.setText("测试内容");
saveXml(document);
}
3 DTD和XSD
前面提到过,xml的一个优点是可以自定义约束规则,让使用者基于约束配置xml ,减少开发测试过程因为xml 编辑问题导致的文件解析错误。这就是通过DTD实现的,这个在很多框架中都有用到,比如Spring
关于XSD更多介绍
参考文献:
xml和json比较
dom操作xml
SAX解析xml
DTD和XSD区别