XML和JSON是两种经常在网络使用的数据格式。本章我们介绍如何使用Java读写XML和JSON。
XML简介 XML是可扩展标记语言(eXtensible Markup Language)的缩写,它是一种数据表示格式,可以描述非常复杂的数据结构,常用于传输和存储数据。
XML有几个特点:一是纯文本,默认使用UTF-8编码;二是可嵌套,适合表示结构化数据。如果把XML内容存为文件,那它就是一个XML文件。此外,XML内容经常通过网络进行消息传输。
XML的结构 XML有固定的结构,首行必定是<?xml version="1.0"?>,可以加上可选编码。紧接着,如果以类似<!DOCTYPE note SYSTEM "book.dtd">声明的是文档定义类型(DTD:Document Type Definition),DTD是可选的。接下来是XML的文档内容,一个XML文档有且仅有一个根元素,根元素可以包含任意个子元素,元素可以包含属性,例如,<isbn lang="CN">1234567</isbn>包含一个属性lang="CN",且元素必须正确嵌套。如果是空元素,可以用<tag/>表示。
由于使用了<,>和引号等标识符,如果内容出现了特殊符号,需要使用&???;表示转义。例如,Java<tm>必须写成:
1 <name>Java<tm></name>
 
常见的特殊字符如下:
字符 
表示 
 
 
< 
< 
 
> 
> 
 
& 
& 
 
“ 
" 
 
‘ 
' 
 
格式正确的XML(Well Formed)是指XML的格式是正确的,可以被解析器正确读取。而合法的XML是指,不但XML格式正确,而且它的数据结构可以被DTD或者XSD验证。
DTD文档可以指定一系列规则,例如:
根元素必须是book 
book元素必须包含name,author等指定元素 
isbn元素必须包含属性lang 
… 
 
如何验证XML文件的正确性呢?最简单的方式是通过浏览器验证。可以直接把XML文件拖拽到浏览器窗口,如果格式错误,浏览器会报错。
和结构类似的HTML不同,浏览器对HTML有一定的“容错性”,缺少关闭标签也可以被解析,但XML要求严格的格式,任何没有正确嵌套的标签都会导致错误。
XML是一个技术体系,除了我们经常用到的XML文档本身外,XML还支持:
DTD和XSD:验证XML结构和数据是否有效; 
Namespace:XML节点和属性的名字空间; 
XSLT:把XML转化为另一种文本; 
XPath:一种XML节点查询语言; 
… 
 
实际上,XML的这些相关技术实现起来非常复杂,在实际应用中很少用到,通常了解一下就可以了。
使用DOM 因为XML是一种树形结构的文档,它有两种标准的解析API:
DOM:一次性读取XML,并在内存中表示为树形结构 
SAX:以流的形式读取XML,使用事件回调 
 
我们先来看看如何使用DOM读取XML。
DOM是Document Object Model的缩写,DOM模型就是把XML结构作为一个树形结构来处理,从根节点开始,每个节点都可以包含任意个子节点。
我们以下面的XML为例:
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="UTF-8" ?> <book  id ="1" >      <name > Java核心技术</name >      <author > Cay S. Horstmann</author >      <isbn  lang ="CN" > 1234567</isbn >      <tags >          <tag > Java</tag >          <tag > Network</tag >      </tags >      <pubDate /> </book > 
 
如果解析为DOM结构,它大概长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20                       ┌─────────┐                       │document │                       └─────────┘                            │                            ▼                       ┌─────────┐                       │  book   │                       └─────────┘                            │      ┌──────────┬──────────┼──────────┬──────────┐      ▼          ▼          ▼          ▼          ▼ ┌─────────┐┌─────────┐┌─────────┐┌─────────┐┌─────────┐ │  name   ││ author  ││  isbn   ││  tags   ││ pubDate │ └─────────┘└─────────┘└─────────┘└─────────┘└─────────┘                                       │                                  ┌────┴────┐                                  ▼         ▼                              ┌───────┐ ┌───────┐                              │  tag  │ │  tag  │                              └───────┘ └───────┘
 
注意到最顶层的document代表XML文档,它是真正的“根”,而<book>虽然是根元素,但它是document的一个子节点。
Java提供了DOM API来解析XML,它使用下面的对象来表示XML的内容:
Document:代表整个XML文档; 
Element:代表一个XML元素; 
Attribute:代表一个元素的某个属性。 
 
使用DOM API解析一个XML文档的代码如下:
1 2 3 4 InputStream input = Main.class.getResourceAsStream("/book.xml" ); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(input);
 
DocumentBuilder.parse()用于解析一个XML,它可以接收InputStream,File或者URL,如果解析无误,我们将获得一个Document对象,这个对象代表了整个XML文档的树形结构,需要遍历以便读取指定元素的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void  printNode (Node n, int  indent)   {     for  (int  i = 0 ; i < indent; i++) {         System.out.print(' ' );     }     switch  (n.getNodeType()) {     case  Node.DOCUMENT_NODE:          System.out.println("Document: "  + n.getNodeName());         break ;     case  Node.ELEMENT_NODE:          System.out.println("Element: "  + n.getNodeName());         break ;     case  Node.TEXT_NODE:          System.out.println("Text: "  + n.getNodeName() + " = "  + n.getNodeValue());         break ;     case  Node.ATTRIBUTE_NODE:          System.out.println("Attr: "  + n.getNodeName() + " = "  + n.getNodeValue());         break ;     default :          System.out.println("NodeType: "  + n.getNodeType() + ", NodeName: "  + n.getNodeName());     }     for  (Node child = n.getFirstChild(); child != null ; child = child.getNextSibling()) {         printNode(child, indent + 1 );     } }
 
解析结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 Document: #document  Element: book   Text: #text =         Element: name    Text: #text = Java核心技术   Text: #text =         Element: author    Text: #text = Cay S. Horstmann   Text: #text =    ...
 
对于DOM API解析出来的结构,我们从根节点Document出发,可以遍历所有子节点,获取所有元素、属性、文本数据,还可以包括注释,这些节点被统称为Node,每个Node都有自己的Type,根据Type来区分一个Node到底是元素,还是属性,还是文本,等等。
使用DOM API时,如果要读取某个元素的文本,需要访问它的Text类型的子节点,所以使用起来还是比较繁琐的。
小结 Java提供的DOM API可以将XML解析为DOM结构,以Document对象表示;
DOM可在内存中完整表示XML数据结构;
DOM解析速度慢,内存占用大。
使用SAX 使用DOM解析XML的优点是用起来省事,但它的主要缺点是内存占用太大。另一种解析XML的方式是SAX,SAX是Simple API for XML的缩写,它是一种基于流的解析方式,边读取XML边解析,并以时间回调的方式让调用者获取数据。因为是一边读一边解析,所以无论XML有多大,占用的内存都很小。
SAX解析会触发一系列事件:
startDocument:开始读取XML文档; 
startElement:读取到了一个元素,例如<book>; 
characters:读取到了字符; 
endElement:读取到了一个结束的元素,例如</book>; 
endDocument:读取XML文档结束。 
 
如果我们用SAX API解析XML,Java代码如下:
1 2 3 4 InputStream input = Main.class.getResourceAsStream("/book.xml" ); SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser = spf.newSAXParser(); saxParser.parse(input, new  MyHandler());
 
关键代码SAXParser.parse()除了需要传入一个InputStream外,还需要传入一个回调对象,这个对象要继承自DefaultHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class  MyHandler  extends  DefaultHandler   {     public  void  startDocument ()  throws  SAXException  {         print("start document" );     }     public  void  endDocument ()  throws  SAXException  {         print("end document" );     }     public  void  startElement (String uri, String localName, String qName, Attributes attributes)  throws  SAXException  {         print("start element:" , localName, qName);     }     public  void  endElement (String uri, String localName, String qName)  throws  SAXException  {         print("end element:" , localName, qName);     }     public  void  characters (char [] ch, int  start, int  length)  throws  SAXException  {         print("characters:" , new  String(ch, start, length));     }     public  void  error (SAXParseException e)  throws  SAXException  {         print("error:" , e);     }     void  print (Object... objs)   {         for  (Object obj : objs) {             System.out.print(obj);             System.out.print(" " );         }         System.out.println();     } }
 
运行SAX解析代码,可以打印出下面的结果:
1 2 3 4 5 6 7 8 9 10 11 start document start element:  book characters:       start element:  name characters: Java核心技术 end element:  name characters:       start element:  author ...
 
如果要读取<name>节点的文本,我们就必须在解析过程中根据startElement()和endElement()定位当前正在读取的节点,可以使用栈结构保存。每遇到一个startElement()入栈,每遇到一个endElement()出栈,这样,读到characters()时我们才知道读取的文本是那个节点的。可见,使用SAX API仍然比较麻烦。
使用Jackson 前面我们介绍了DOM和SAX两种解析XML的标准接口,但是,二者使用起来都不直观。
我们观察XML文档的内容,发现,它完全可以对应一个定义号的JavaBean中。如果能直接从XML文档解析成一个JavaBean,那比DOM和SAX不知道容易到哪里去了。
幸运的是,一个名叫Jackson的开源第三方库可以轻松做到XML到JavaBean的转换。我们要使用Jackson,首先添加两个Maven依赖。
com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.10.1 
org.codehaus.woodstox:woodstox-core-asl:4.4.1 
 
然后,定义好JavaBean,就可以用下面几行代码解析:
1 2 3 4 5 6 7 8 9 10 InputStream input = Main.class.getResourceAsStream("/book.xml" ); JacksonXmlModule module  = new  JacksonXmlModule(); XmlMapper mapper = new  XmlMapper(module ); Book book = mapper.readValue(input, Book.class); System.out.println(book.id); System.out.println(book.name); System.out.println(book.author); System.out.println(book.isbn); System.out.println(book.tags); System.out.println(book.pubDate);
 
注意到XmlMapper就是我们需要创建的核心对象,可以用readValue(InputStream, Class)直接读取XML并返回一个JavaBean。运行上述代码,就可以直接从Book对象中拿到数据:
1 2 3 4 5 6 1 Java核心技术 Cay S. Horstmann 1234567 [Java, Network] null
 
如果要解析的数据格式不是Jackson内置的标准格式,那么需要编写一点额外的扩展来告诉Jackson如何自定义解析,详细请看官方文档 。
使用JSON 前面我们讨论了XML这种数据格式。XML的特点是功能全面,但标签繁琐,格式复杂。在Web上使用XML现在越来越少,取而代之的是JSON这种数据结构。
JSON是JavaScript Object Notation的缩写,它去除了所有JavaScript执行代码,只保留JavaScript的对象格式。一个典型的JSON如下:
1 2 3 4 5 6 7 8 9 10 {     "id" : 1 ,     "name" : "Java核心技术" ,     "author" : {         "firstName" : "Abc" ,         "lastName" : "Xyz"      },     "isbn" : "1234567" ,     "tags" : ["Java" , "Network" ] }
 
JSON作为数据传输格式,有几个显著的优点:
JSON只允许使用UTF-8编码,不存在编码问题 
JSON只允许使用双引号作为key,特殊字符用\转义,格式简单 
浏览器内置JSON支持,如果把数据用JSON发送给浏览器,可以用JavaScript直接处理 
 
因此,JSON适合表示层次结构,因为它格式简单,仅支持以下几种数据类型:
键值对:{"key": value} 
数组:[1, 2, 3] 
字符串:"abc" 
数值(整数和浮点数):12.34 
布尔值:true或false 
空值:null 
 
浏览器直接支持使用JavaScript对JSON进行读写:
1 2 3 4 5 // JSON string to JavaScript object: jsObj = JSON.parse(jsonStr); // JavaScript object to JSON string: jsonStr = JSON.stringify(jsObj);
 
所以,开发Web应用的时候,使用JSON作为数据传输,在浏览器端非常方便。因为JSON天生适合JavaScript处理,所以,绝大多数REST API都选择JSON作为数据传输格式。
那么,如何用Java对JSON进行读写呢?
常用的用于解析JSON的第三方库有:
注意到上一节提到的那个可以解析XML的浓眉大眼的Jackson也可以解析JSON!因此我们只需要引入以下Maven依赖:
com.fasterxml.jackson.core:jackson-databind:2.10.0 
 
就可以使用下面的代码解析一个JSON文件:
1 2 3 4 5 InputStream input = Main.class.getResourceAsStream("/book.json" ); ObjectMapper mapper = new  ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false ); Book book = mapper.readValue(input, Book.class);
 
核心代码是创建一个ObjectMapper对象。关闭DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES功能使得解析时如果JavaBean不存在该属性时解析不会报错。
把JSON解析为JavaBean的过程称为反序列化。如果把JavaBean变为JSON,那就是序列化。要实现JavaBean到JSON的序列化,只需要一行代码:
1 String json = mapper.writeValueAsString(book);
 
要把JSON的某些值解析为特定的Java对象,例如LocalDate,也是完全可以的。例如:
1 2 3 4 {     "name" : "Java核心技术" ,     "pubDate" : "2016-09-01"  }
 
要解析为:
1 2 3 4 public  class  Book   {     public  String name;     public  LocalDate pubDate; }
 
只需要引入标准的JSR 310关于JavaTime的数据格式定义至Maven:
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.0 
 
然后,在创建ObjectMapper时,注册一个新的JavaTimeModule:
1 ObjectMapper mapper = new  ObjectMapper().registerModule(new  JavaTimeModule());
 
有些时候,内置的解析规则和扩展的解析规则如果都不满足我们的需求,还可以自定义解析。
举个例子,假设Book类的isbn是一个BigInteger:
1 2 3 4 public  class  Book   { 	public  String name; 	public  BigInteger isbn; }
 
但JSON数据并不是标准的整形格式:
1 2 3 4 {     "name" : "Java核心技术" ,     "isbn" : "978-7-111-54742-6"  }
 
直接解析,肯定报错。这时,我们需要自定义一个IsbnDeserializer,用于解析含有非数字的字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  class  IsbnDeserializer  extends  JsonDeserializer <BigInteger >  {     public  BigInteger deserialize (JsonParser p, DeserializationContext ctxt)  throws  IOException, JsonProcessingException  {                  String s = p.getValueAsString();         if  (s != null ) {             try  {                 return  new  BigInteger(s.replace("-" , "" ));             } catch  (NumberFormatException e) {                 throw  new  JsonParseException(p, s, e);             }         }         return  null ;     } }
 
然后,在Book类中使用注解标注:
1 2 3 4 5 6 public  class  Book   {     public  String name;          @JsonDeserialize(using = IsbnDeserializer.class)      public  BigInteger isbn; }
 
类似的,自定义序列化时我们需要自定义一个IsbnSerializer,然后在Book类中标注@JsonSerialize(using = ...)即可。