Jackson - Java序列化

本文讲述如何创建一个JSON结构字符串。我们有三种方式创建JSON:

  1. Java - 最常用的一种方式是将Java对象序列化成JSON。
  2. Tree representation - 我们先创建一个 com.fasterxml.jackson.databind.JsonNode(Jackson 2.x版本用这个包)对象树,然后将这棵树转换成JSON。
  3. Stream generator - 这种方法我们要用Stream generator构建一个json流然后将其转换成一个JSON结构。

Java对象转JSON

Jackson提供了Java对象序列化成JSON和JSON反序列化成Java对象的类。本示例将演示将Java对象序列化成JSON结构。我们将从一个简单的类开始,然后逐步增加复杂性。假设我们有一个音乐公司,想发布一个API供用户来查询唱片集。我们先建一个只有title属性的Album类。

 1 class Album {
 2     private String title;
 3  
 4     public Album(String title) {
 5         this.title = title;
 6     }
 7      
 8     public String getTitle() {
 9         return title;
10     }
11 }

我们用ObjectMapper类将对象转换成JSON。默认情况下,Jackson会用BeanSerializer 类序列化POJO(简单Java对象)。 > 记住Java Bean对象中private修饰的属性要有getter方法,或者该属性被public修饰,可以没有getter方法。如果对象中只有private的属性且没有对应的getter方法,Jackson将报异常,但是如果对象中既有privatepublic修饰的属性,那么private属性没有getter方法也不会报异常,但是如果没有其他Jackson注解,该属性序列化时将不可见,也即JSON中将没有该字段。

转换的JSON结构如下:

{"title":"Kind Of Blue"}

让我们给这个唱片集增加一组链接,当然还有相应的gettersetter方法,方便链接到唱片发布会或是评论。

1 private String[] links;

main方法中增加如下语句:

1 album.setLinks(new String[] { "link1", "link2" });

现在的JSON结构如下,注意Java Array被转换成被[]包围JSON数组。

1 {"title":"Kind Of Blue","links":["link1","link2"]}

现在我们给Album增加一个歌曲List.

1 List<String> Songs = new ArrayList<String>();

main方法中给Album对象增加歌曲List.

1 List<String> songs = new ArrayList<String>();
2 songs.add("So What");
3 songs.add("Flamenco Sketches");
4 songs.add("Freddie Freeloader");
5 album.setSongs(songs);

转换的结构如下,注意List对象和上面的Array对象一样也被转换成json 数组。

1 {"title":"Kind Of Blue","links":["link1","link2"],
2 "songs":["So What","Flamenco Sketches","Freddie Freeloader"]}

接下来,我们将给唱片集增加一个艺术家属性。艺术家类是一个包含姓名和Date实例表示的出生日期的类。

1 class Artist {
2     public String name;
3     public Date birthDate;
4 }

main方法中给唱片集增加一个艺术家。

1 Artist artist = new Artist();
2 artist.name = "Miles Davis";
3 SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy");
4 artist.birthDate = format.parse("26-05-1926");
5 album.setArtist(artist);

Json结构如下:

1 {"title":"Kind Of Blue","links":["link1","link2"],
2 "songs":["So What","Flamenco Sketches","Freddie Freeloader"],
3 "artist":{"name":"Miles Davis","birthDate":-1376027600000}}

为了使Jackson转换出来的JSON字符串更具可读性,我们增加了一行代码。

1 mapper.configure(SerializationFeature.INDENT_OUTPUT, true);

格式化后的JSON结构如下:

1 {
2   "title" : "Kind Of Blue",
3   "links" : [ "link1" , "link2" ],
4   "songs" : [ "So What", "Flamenco Sketches", "Freddie Freeloader" ],
5   "artist" : {
6     "name" : "Miles Davis",
7     "birthDate" : -1376027600000
8   }
9 }

注意这个不应该用于实际发布的时候,仅仅用于开发和测试阶段,因为为了达到换行和缩进的效果,会在每行的头尾部增加\r\n\t等字符,这会显著增加数据传输量。

现在我们再在唱片集中添加一个作曲家和他们所使用的乐曲之间的Map。

1 private Map<String,String> musicians = new HashMap<String, String>();
2 public Map<String, String> getMusicians() {
3         return Collections.unmodifiableMap(musicians);
4 }
5 public void addMusician(String key, String value){
6         musicians.put(key, value);
7 }

main方法中添加:

1 album.addMusician("Miles Davis", "Trumpet, Band leader");
2 album.addMusician("Julian Adderley", "Alto Saxophone");
3 album.addMusician("Paul Chambers", "double bass");

json结构如下:

 1 {
 2   "title" : "Kind Of Blue",
 3   "links" : [ "link1", "link2" ],
 4   "songs" : [ "So What", "Flamenco Sketches", "Freddie Freeloader" ],
 5   "artist" : {
 6     "name" : "Miles Davis",
 7     "birthDate" : -1376027600000
 8   },
 9   "musicians" : {
10     "Julian Adderley" : "Alto Saxophone",
11     "Paul Chambers" : "double bass",
12     "Miles Davis" : "Trumpet, Band leader"
13   }
14 }

我们还可以给这个转换过程添加更多的特性,先告诉mapper按照Map中keys的自然顺序排序:

1 mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);

这里有一个问题,细心的读者可能已经发现birthDate字段已经被格式化成新纪元时间,这不是我们想要到,因此还应当将时间格式化为人类更可读的形式。

1 SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM yyyy");
2 mapper.setDateFormat(outputFormat);

默认情况下,Jackson会使用Java成员属性名作为Json字段名。你可以像本教程一样用Jackson Annotations来改变默认的命名策略。然而有时你可能无法直接访问Java Bean对象,比如那是一个第三方库,你无法在其类的源码上添加Annotations,又或者你不想Java bean和Jackson Annotation混在一起。对于这些情况,Jackson也提供了一种优雅的方法来改变默认的字段命名策略。可以在mapper上使用setPropertyNamingStrategy方法来为field设置命名策略。如果在bean中有public域,可以重写nameForField方法;如果bean中有getter方法,那么可以重写nameForGetter方法。在下面的example中,我们将albem的title域改为json的Album-Title字段,artist的name域改为json的Artist-Name。实现这些,只需在main中做如下改变:

 1 mapper.setPropertyNamingStrategy(new PropertyNamingStrategy() {
 2 @Override
 3 public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
 4    if (field.getFullName().equals("com.studytrails.json.jackson.Artist#name"))
 5         return "Artist-Name";
 6         return super.nameForField(config, field, defaultName);
 7 }
 8  
 9 @Override
10 public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
11   if (method.getAnnotated().getDeclaringClass().equals(Album.class) && defaultName.equals("title"))
12         return "Album-Title";
13         return super.nameForGetterMethod(config, method, defaultName);
14   }
15 });

json结构如下:

 1 {
 2   "Album-Title" : "Kind Of Blue",
 3   "links" : [ "link1", "link2" ],
 4   "songs" : [ "So What", "Flamenco Sketches", "Freddie Freeloader" ],
 5   "artist" : {
 6     "Artist-Name" : "Miles Davis",
 7     "birthDate" : "26 May 1926"
 8   },
 9   "musicians" : {
10     "Julian Adderley" : "Alto Saxophone",
11     "Miles Davis" : "Trumpet, Band leader",
12     "Paul Chambers" : "double bass"
13   }
14 }

现在来看一下Jackson如何处理null 域。我们为Artist类新增三个属性,并且不赋值初始化。新增的属性如下:

1 public int age;
2 public String homeTown;
3 public List<String> awardsWon = new ArrayList<String>();

转换后的json结构如下:

1 {
2 "age" : 0,
3  "homeTown" : null,
4  "awardsWon" : [ ]
5 }

可以通过配置项来忽略空属性。

1 mapper.setSerializationInclusion(Include.NON_EMPTY);

以下是本节内容的完整代码:

  1 import java.io.IOException;
  2 import java.text.ParseException;
  3 import java.text.SimpleDateFormat;
  4 import java.util.ArrayList;
  5 import java.util.Collections;
  6 import java.util.Date;
  7 import java.util.HashMap;
  8 import java.util.List;
  9 import java.util.Map;
 10  
 11 import com.fasterxml.jackson.annotation.JsonInclude.Include;
 12 import com.fasterxml.jackson.databind.ObjectMapper;
 13 import com.fasterxml.jackson.databind.PropertyNamingStrategy;
 14 import com.fasterxml.jackson.databind.SerializationFeature;
 15 import com.fasterxml.jackson.databind.cfg.MapperConfig;
 16 import com.fasterxml.jackson.databind.introspect.AnnotatedField;
 17 import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
 18  
 19 public class SerializationExample {
 20  
 21     public static void main(String[] args) throws IOException, ParseException {
 22         ObjectMapper mapper = new ObjectMapper();
 23         Album album = new Album("Kind Of Blue");
 24         album.setLinks(new String[] { "link1", "link2" });
 25         List<String> songs = new ArrayList<String>();
 26         songs.add("So What");
 27         songs.add("Flamenco Sketches");
 28         songs.add("Freddie Freeloader");
 29         album.setSongs(songs);
 30         Artist artist = new Artist();
 31         artist.name = "Miles Davis";
 32         SimpleDateFormat format = new SimpleDateFormat("dd-MM-yyyy");
 33         artist.birthDate = format.parse("26-05-1926");
 34         album.setArtist(artist);
 35         album.addMusician("Miles Davis", "Trumpet, Band leader");
 36         album.addMusician("Julian Adderley", "Alto Saxophone");
 37         album.addMusician("Paul Chambers", "double bass");
 38         mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
 39         mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
 40         SimpleDateFormat outputFormat = new SimpleDateFormat("dd MMM yyyy");
 41         mapper.setDateFormat(outputFormat);
 42         mapper.setPropertyNamingStrategy(new PropertyNamingStrategy() {
 43             @Override
 44             public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
 45                 if (field.getFullName().equals("com.studytrails.json.jackson.Artist#name"))
 46                     return "Artist-Name";
 47                 return super.nameForField(config, field, defaultName);
 48             }
 49  
 50             @Override
 51             public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
 52                 if (method.getAnnotated().getDeclaringClass().equals(Album.class) && defaultName.equals("title"))
 53                     return "Album-Title";
 54                 return super.nameForGetterMethod(config, method, defaultName);
 55             }
 56         });
 57         mapper.setSerializationInclusion(Include.NON_EMPTY);
 58         mapper.writeValue(System.out, album);
 59     }
 60 }
 61  
 62 class Album {
 63     private String title;
 64     private String[] links;
 65     private List<String> songs = new ArrayList<String>();
 66     private Artist artist;
 67     private Map<String , String> musicians = new HashMap<String , String>();
 68  
 69     public Album(String title) {
 70         this.title = title;
 71     }
 72  
 73     public String getTitle() {
 74         return title;
 75     }
 76  
 77     public void setLinks(String[] links) {
 78         this.links = links;
 79     }
 80  
 81     public String[] getLinks() {
 82         return links;
 83     }
 84  
 85     public void setSongs(List<String> songs) {
 86         this.songs = songs;
 87     }
 88  
 89     public List<String> getSongs() {
 90         return songs;
 91     }
 92  
 93     public void setArtist(Artist artist) {
 94         this.artist = artist;
 95     }
 96  
 97     public Artist getArtist() {
 98         return artist;
 99     }
100  
101     public Map<String , String> getMusicians() {
102         return Collections.unmodifiableMap(musicians);
103     }
104  
105     public void addMusician(String key, String value) {
106         musicians.put(key, value);
107     }
108 }
109  
110 class Artist {
111     public String name;
112     public Date birthDate;
113     public int age;
114     public String homeTown;
115     public List<String> awardsWon = new ArrayList<String>();
116 }

本小节我们看到了如何将Java对象序列化成json的用法,下一小节,我们将讲解如何用Jackson的tree方法构建json.

用Tree Model构建JSON

用简单的Tree Model构建JSON有时是很有用的,例如你不想为你的JSON结构创建一个相应的Java Bean类。我们将再次使用上一节的example,有一个album类,他有一个Array的歌曲,一个艺术家和一个Array的作曲家。在建a tree之前,你先得做下面这些准备:

  • 生成JsonNodeFactory对象来创建节点。
  • JsonFactory生成JsonGenerator对象,并指定输出方式,本例中我们的输出方式为输出到控制台。
  • 生成一个ObjectMapper对象,它将用jsonGenerator和root node创建JSON。

在一切准备好后,我们为album创建单独的root node,注意,默认情况下ObjectMapper没有为root node命名。

 1 import java.io.IOException;
 2  
 3 import com.fasterxml.jackson.core.JsonFactory;
 4 import com.fasterxml.jackson.core.JsonGenerator;
 5 import com.fasterxml.jackson.databind.JsonNode;
 6 import com.fasterxml.jackson.databind.ObjectMapper;
 7 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 8  
 9 public class SerializationExampleTreeModel {
10     public static void main(String[] args) throws IOException {
11         // Create the node factory that gives us nodes.
12         JsonNodeFactory factory = new JsonNodeFactory(false);
13  
14         // create a json factory to write the treenode as json. for the example
15         // we just write to console
16         JsonFactory jsonFactory = new JsonFactory();
17         JsonGenerator generator = jsonFactory.createGenerator(System.out);
18         ObjectMapper mapper = new ObjectMapper();
19  
20         // the root node - album
21         JsonNode album = factory.objectNode();
22         mapper.writeTree(generator, album);
23  
24     }
25  
26 }

结果将是:

1 {}

你没有看错,我们输入了这么多的代码,得到的回报仅仅是两个大括号。现在我们来开始真正构建JSON,先给album的属性赋值,例如Album-Title

1 album.put("Album-Title", "Kind Of Blue");

现在JSON中有内容了:

1 {"Album-Title":"Kind Of Blue"}

现在开始给唱片集增加链接Array:

1 ArrayNode links = factory.arrayNode();
2 links.add("link1").add("link2");
3 album.put("links", links);
4 
5 //JSON结果如下
6 {"Album-Title":"Kind Of Blue","links":["link1","link2"]}

接下来增加艺术家对象,记住,艺术家本身也是一个jsonObject:

1 ObjectNode artist = factory.objectNode();
2 artist.put("Artist-Name", "Miles Davis");
3 artist.put("birthDate", "26 May 1926");
4 album.put("artist", artist);
5 
6 //JSON结果如下
7 
8 {"Album-Title":"Kind Of Blue","links":["link1","link2"],
9 "artist":{"Artist-Name":"Miles Davis","birthDate":"26 May 1926"}}

目前为止我们还没有添加音乐家对象,音乐家不是Array形式而是一个musicians类型对象:

1 ObjectNode musicians = factory.objectNode();
2 musicians.put("Julian Adderley", "Alto Saxophone");
3 musicians.put("Miles Davis", "Trumpet, Band leader");
4 album.put("musicians", musicians);
5 
6 //JSON结果如下
7 {"Album-Title":"Kind Of Blue","links":["link1","link2"],
8 "artist":{"Artist-Name":"Miles Davis","birthDate":"26 May 1926"},
9 "musicians":{"Julian Adderley":"Alto Saxophone","Miles Davis":"Trumpet, Band leader"}}

以类似的方式你还可以添加其他元素,如果你有多个唱片集,你可以考虑通过循环生成一个album数组.Tree Model方式生成JSON普遍要比从Java 对象序列化成JSON快。

Streaming Parser and Generator

Jackson提供了一个低层次的API来解析JSON字符串。这个API为每个JSON对象提供一个token。例如,JSON的开始标志{是解析器提供的第一个对象。键值对是另一个单一对象。客户端代码能够利用tokens并得到json属性,或者如果需要的话,可以通过流构建出一个Java对象。低层次的API功能很强大,但是需要写更多的代码。下面我们提供了两个例子。第一个example演示json解析,第二个example演示json生成。

Json Parsing Example:

 1 import java.io.IOException;
 2 import java.net.MalformedURLException;
 3 import java.net.URL;
 4  
 5 import com.fasterxml.jackson.core.JsonFactory;
 6 import com.fasterxml.jackson.core.JsonParser;
 7 import com.fasterxml.jackson.core.JsonToken;
 8  
 9 /**
10  * The aim of this class is to get the list of albums from free music archive
11  * (limit to 5)
12  *
13  */
14 public class StreamParser1 {
15     public static void main(String[] args) throws MalformedURLException, IOException {
16         // Get a list of albums from free music archive. limit the results to 5
17         String url = "http://freemusicarchive.org/api/get/albums.json?api_key=60BLHNQCAOUFPIBZ&limit=5";
18         // get an instance of the json parser from the json factory
19         JsonFactory factory = new JsonFactory();
20         JsonParser parser = factory.createParser(new URL(url));
21  
22         // continue parsing the token till the end of input is reached
23         while (!parser.isClosed()) {
24             // get the token
25             JsonToken token = parser.nextToken();
26             // if its the last token then we are done
27             if (token == null)
28                 break;
29             // we want to look for a field that says dataset
30  
31             if (JsonToken.FIELD_NAME.equals(token) && "dataset".equals(parser.getCurrentName())) {
32                 // we are entering the datasets now. The first token should be
33                 // start of array
34                 token = parser.nextToken();
35                 if (!JsonToken.START_ARRAY.equals(token)) {
36                     // bail out
37                     break;
38                 }
39                 // each element of the array is an album so the next token
40                 // should be {
41                 token = parser.nextToken();
42                 if (!JsonToken.START_OBJECT.equals(token)) {
43                     break;
44                 }
45                 // we are now looking for a field that says "album_title". We
46                 // continue looking till we find all such fields. This is
47                 // probably not a best way to parse this json, but this will
48                 // suffice for this example.
49                 while (true) {
50                     token = parser.nextToken();
51                     if (token == null)
52                         break;
53                     if (JsonToken.FIELD_NAME.equals(token) && "album_title".equals(parser.getCurrentName())) {
54                         token = parser.nextToken();
55                         System.out.println(parser.getText());
56                     }
57  
58                 }
59  
60             }
61  
62         }
63  
64     }
65 }

Json Generation Example:

 1 import java.io.File;
 2 import java.io.FileWriter;
 3 import java.io.IOException;
 4  
 5 import com.fasterxml.jackson.core.JsonFactory;
 6 import com.fasterxml.jackson.core.JsonGenerator;
 7  
 8 /**
 9  *
10  * In This example we look at generating a json using the jsongenerator. we will
11  * be creating a json similar to
12  * http://freemusicarchive.org/api/get/albums.json?
13  * api_key=60BLHNQCAOUFPIBZ&limit=1, but use only a couple of fields
14  *
15  */
16 public class StreamGenerator1 {
17     public static void main(String[] args) throws IOException {
18         JsonFactory factory = new JsonFactory();
19         JsonGenerator generator = factory.createGenerator(new FileWriter(new File("albums.json")));
20  
21         // start writing with {
22         generator.writeStartObject();
23         generator.writeFieldName("title");
24         generator.writeString("Free Music Archive - Albums");
25         generator.writeFieldName("dataset");
26         // start an array
27         generator.writeStartArray();
28         generator.writeStartObject();
29         generator.writeStringField("album_title", "A.B.A.Y.A.M");
30         generator.writeEndObject();
31         generator.writeEndArray();
32         generator.writeEndObject();
33  
34         generator.close();
35     }
36 }