1. 程式人生 > 實用技巧 >Jackson:我是最牛掰的 Java JSON 解析器(有點虛)

Jackson:我是最牛掰的 Java JSON 解析器(有點虛)

在當今的程式設計世界裡,JSON 已經成為將資訊從客戶端傳輸到伺服器端的首選協議,可以好不誇張的說,XML 就是那個被拍死在沙灘上的前浪。

很不幸的是,JDK 沒有 JSON 庫,不知道為什麼不搞一下。Log4j 的時候,為了競爭,還推出了 java.util.logging,雖然最後也沒多少人用。

Java 之所以牛逼,很大的功勞在於它的生態非常完備,JDK 沒有 JSON 庫,第三方類庫有啊,還挺不錯,比如說本篇的豬腳——Jackson,GitHub 上標星 6.1k,Spring Boot 的預設 JSON 解析器。

怎麼證明這一點呢?

當我們通過 starter 新建一個 Spring Boot 的 Web 專案後,就可以在 Maven 的依賴項中看到 Jackson 的身影。

Jackson 有很多優點:

  • 解析大檔案的速度比較快;
  • 執行時佔用的記憶體比較少,效能更佳;
  • API 很靈活,容易進行擴充套件和定製。

Jackson 的核心模組由三部分組成:

  • jackson-core,核心包,提供基於“流模式”解析的相關 API,包括 JsonPaser 和 JsonGenerator。
  • jackson-annotations,註解包,提供標準的註解功能;
  • jackson-databind ,資料繫結包,提供基於“物件繫結”解析的相關 API ( ObjectMapper ) 和基於“樹模型”解析的相關 API (JsonNode)。

01、引入 Jackson 依賴

要想使用 Jackson,需要在 pom.xml 檔案中新增 Jackson 的依賴。

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>

jackson-databind 依賴於 jackson-core 和 jackson-annotations,所以新增完 jackson-databind 之後,Maven 會自動將 jackson-core 和 jackson-annotations 引入到專案當中。

Maven 之所以討人喜歡的一點就在這,能偷偷摸摸地幫我們把該做的做了。

02、使用 ObjectMapper

Jackson 最常用的 API 就是基於”物件繫結” 的 ObjectMapper,它通過 writeValue 的系列方法將 Java 物件序列化為 JSON,並且可以儲存成不同的格式。

  • writeValueAsString(Object value) 方法,將物件儲存成字串
  • writeValueAsBytes(Object value) 方法,將物件儲存成位元組陣列
  • writeValue(File resultFile, Object value) 方法,將物件儲存成檔案

來看一下儲存成字串的程式碼示例:

importcom.fasterxml.jackson.core.JsonProcessingException;
importcom.fasterxml.jackson.databind.ObjectMapper;

/**
*微信搜尋「沉默王二」,回覆Java
*
*@author沉默王二
*@date2020/11/26
*/

publicclassDemo{
publicstaticvoidmain(String[]args)throwsJsonProcessingException{
Writerwanger=newWriter("沉默王二",18);
ObjectMappermapper=newObjectMapper();
StringjsonString=mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(wanger);
System.out.println(jsonString);
}
}

classWriter{
privateStringname;
privateintage;

publicWriter(Stringname,intage){
this.name=name;
this.age=age;
}

publicStringgetName(){
returnname;
}

publicvoidsetName(Stringname){
this.name=name;
}

publicintgetAge(){
returnage;
}

publicvoidsetAge(intage){
this.age=age;
}
}

程式輸出結果如下所示:

{
"name":"沉默王二",
"age":18
}

不是所有的欄位都支援序列化和反序列化,需要符合以下規則:

  • 如果欄位的修飾符是 public,則該欄位可序列化和反序列化(不是標準寫法)。
  • 如果欄位的修飾符不是 public,但是它的 getter 方法和 setter 方法是 public,則該欄位可序列化和反序列化。getter 方法用於序列化,setter 方法用於反序列化。
  • 如果欄位只有 public 的 setter 方法,而無 public 的 getter 方 法,則該欄位只能用於反序列化。

如果想更改預設的序列化和反序列化規則,需要呼叫 ObjectMapper 的 setVisibility() 方法。否則將會丟擲 InvalidDefinitionException 異常。

ObjectMapper 通過 readValue 的系列方法從不同的資料來源將 JSON 反序列化為 Java 物件。

  • readValue(String content, Class valueType) 方法,將字串反序列化為 Java 物件
  • readValue(byte[] src, Class valueType) 方法,將位元組陣列反序列化為 Java 物件
  • readValue(File src, Class valueType) 方法,將檔案反序列化為 Java 物件

來看一下將字串反序列化為 Java 物件的程式碼示例:

importcom.fasterxml.jackson.core.JsonProcessingException;
importcom.fasterxml.jackson.databind.ObjectMapper;

/**
*微信搜尋「沉默王二」,回覆Java
*
*@author沉默王二
*@date2020/11/26
*/

publicclassDemo{
publicstaticvoidmain(String[]args)throwsJsonProcessingException{
ObjectMappermapper=newObjectMapper();
StringjsonString="{\n"+
"\"name\":\"沉默王二\",\n"+
"\"age\":18\n"+
"}";
WriterdeserializedWriter=mapper.readValue(jsonString,Writer.class);
System.out.println(deserializedWriter);
}
}

classWriter{
privateStringname;
privateintage;

//getter/setter

@Override
publicStringtoString(){
return"Writer{"+
"name='"+name+'\''+
",age="+age+
'}';
}
}

程式輸出結果如下所示:

Writer{name='沉默王二',age=18}

PS:如果反序列化的物件有帶參的構造方法,它必須有一個空的預設構造方法,否則將會丟擲 InvalidDefinitionException 一行。

Exceptioninthread"main"com.fasterxml.jackson.databind.exc.InvalidDefinitionException:Cannotconstructinstanceof`com.itwanger.jackson.Writer`(noCreators,likedefaultconstruct,exist):cannotdeserializefromObjectvalue(nodelegate-orproperty-basedCreator)
at[Source:(String)"{
"
name":"沉默王二",
"
age":18
}"
;line:2,column:3]
atcom.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
atcom.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1589)
atcom.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1055)
atcom.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)
atcom.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)
atcom.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
atcom.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4202)
atcom.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3205)
atcom.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3173)
atcom.itwanger.jackson.Demo.main(Demo.java:19)

Jackson 最常用的 API 就是基於”物件繫結” 的 ObjectMapper,

ObjectMapper 也可以將 JSON 解析為基於“樹模型”的 JsonNode 物件,來看下面的示例。

importcom.fasterxml.jackson.core.JsonProcessingException;
importcom.fasterxml.jackson.databind.JsonNode;
importcom.fasterxml.jackson.databind.ObjectMapper;

/**
*微信搜尋「沉默王二」,回覆Java
*
*@author沉默王二
*@date2020/11/26
*/

publicclassJsonNodeDemo{
publicstaticvoidmain(String[]args)throwsJsonProcessingException{
ObjectMappermapper=newObjectMapper();
Stringjson="{\"name\":\"沉默王二\",\"age\":18}";
JsonNodejsonNode=mapper.readTree(json);
Stringname=jsonNode.get("name").asText();
System.out.println(name);//沉默王二
}
}

藉助 TypeReference 可以將 JSON 字串陣列轉成泛型 List,來看下面的示例:

importcom.fasterxml.jackson.core.JsonProcessingException;
importcom.fasterxml.jackson.core.type.TypeReference;
importcom.fasterxml.jackson.databind.ObjectMapper;

importjava.util.List;

/**
*微信搜尋「沉默王二」,回覆Java
*
*@author沉默王二
*@date2020/11/26
*/

publicclassTypeReferenceDemo{
publicstaticvoidmain(String[]args)throwsJsonProcessingException{
ObjectMappermapper=newObjectMapper();
Stringjson="[{\"name\":\"沉默王三\",\"age\":18},{\"name\":\"沉默王二\",\"age\":19}]";
List<Author>listAuthor=mapper.readValue(json,newTypeReference<List<Author>>(){});
System.out.println(listAuthor);
}
}
classAuthor{
privateStringname;
privateintage;

//getter/setter

//toString
}

03、更高階的配置

Jackson 之所以牛掰的一個很重要的因素是可以實現高度靈活的自定義配置。

在實際的應用場景中,JSON 中常常會有一些 Java 物件中沒有的欄位,這時候,如果直接解析的話,會丟擲 UnrecognizedPropertyException 異常。

下面是一串 JSON 字串:

StringjsonString="{\n"+
"\"name\":\"沉默王二\",\n"+
"\"age\":18\n"+
"\"sex\":\"男\",\n"+
"}";

但 Java 物件 Writer 中沒有定義 sex 欄位:

classWriter{
privateStringname;
privateintage;

//getter/setter
}

我們來嘗試解析一下:

ObjectMappermapper=newObjectMapper();
WriterdeserializedWriter=mapper.readValue(jsonString,Writer.class);

不出意外,丟擲異常了,sex 無法識別。

Exceptioninthread"main"com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException:Unrecognizedfield"sex"(classcom.itwanger.jackson.Writer),notmarkedasignorable(2knownproperties:"name","age"])
at[Source:(String)"{
"
name":"沉默王二",
"
age":18,
"
sex":""
}"
;line:4,column:12](throughreferencechain:com.itwanger.jackson.Writer["sex"])

怎麼辦呢?可以通過 configure() 方法忽略掉這些“無法識別”的欄位。

mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);

除此之外,還有其他一些有用的配置資訊,來了解一下:

//在序列化時忽略值為null的屬性
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//忽略值為預設值的屬性
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_DEFAULT);

04、處理日期格式

對於日期型別的欄位,比如說 java.util.Date,如果不指定格式,序列化後將顯示為 long 型別的資料,這種預設格式的可讀性很差。

{
"age":18,
"birthday":1606358621209
}

怎麼辦呢?

第一種方案,在 getter 上使用 @JsonFormat 註解。

privateDatebirthday;

//GMT+8是指格林尼治的標準時間,在加上八個小時表示你現在所在時區的時間
@JsonFormat(timezone="GMT+8",pattern="yyyy-MM-ddHH:mm:ss")
publicDategetBirthday(){
returnbirthday;
}

publicvoidsetBirthday(Datebirthday){
this.birthday=birthday;
}

再來看一下結果:

{
"age":18,
"birthday":"2020-11-2603:02:30"
}

具體程式碼如下所示:

ObjectMappermapper=newObjectMapper();
Writerwanger=newWriter("沉默王二",18);
wanger.setBirthday(newDate());
StringjsonString=mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(wanger);
System.out.println(jsonString);

第二種方案,呼叫 ObjectMapper 的 setDateFormat() 方法。

ObjectMappermapper=newObjectMapper();
mapper.setDateFormat(StdDateFormat.getDateTimeInstance());
Writerwanger=newWriter("沉默王二",18);
wanger.setBirthday(newDate());
StringjsonString=mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(wanger);
System.out.println(jsonString);

輸出結果如下所示:

{
"name":"沉默王二",
"age":18,
"birthday":"2020年11月26日上午11:09:51"
}

05、欄位過濾

在將 Java 物件序列化為 JSON 時,可能有些欄位需要過濾,不顯示在 JSON 中,Jackson 有一種比較簡單的實現方式。

@JsonIgnore 用於過濾單個欄位。

@JsonIgnore
publicStringgetName(){
returnname;
}

@JsonIgnoreProperties 用於過濾多個欄位。

@JsonIgnoreProperties(value={"age","birthday"})
classWriter{
privateStringname;
privateintage;
privateDatebirthday;
}

06、自定義序列化和反序列化

當 Jackson 預設序列化和反序列化不能滿足實際的開發需要時,可以自定義新的序列化和反序列化類。

自定義的序列化類需要繼承 StdSerializer,同時重寫 serialize() 方法,利用 JsonGenerator 生成 JSON,示例如下:

/**
*微信搜尋「沉默王二」,回覆Java
*
*@author沉默王二
*@date2020/11/26
*/

publicclassCustomSerializerextendsStdSerializer<Man>{
protectedCustomSerializer(Class<Man>t){
super(t);
}

publicCustomSerializer(){
this(null);
}

@Override
publicvoidserialize(Manvalue,JsonGeneratorgen,SerializerProviderprovider)throwsIOException{
gen.writeStartObject();
gen.writeStringField("name",value.getName());
gen.writeEndObject();
}
}

classMan{
privateintage;
privateStringname;

publicMan(intage,Stringname){
this.age=age;
this.name=name;
}

publicintgetAge(){
returnage;
}

publicvoidsetAge(intage){
this.age=age;
}

publicStringgetName(){
returnname;
}

publicvoidsetName(Stringname){
this.name=name;
}
}

定義好自定義序列化類後,要想在程式中呼叫它們,需要將其註冊到 ObjectMapper 的 Module 中,示例如下所示:

ObjectMappermapper=newObjectMapper();
SimpleModulemodule=
newSimpleModule("CustomSerializer",newVersion(1,0,0,null,null,null));
module.addSerializer(Man.class,newCustomSerializer());
mapper.registerModule(module);
Manman=newMan(18,"沉默王二");
Stringjson=mapper.writeValueAsString(man);
System.out.println(json);

程式輸出結果如下所示:

{"name":"沉默王二"}

自定義序列化類 CustomSerializer 中沒有新增 age 欄位,所以只輸出了 name 欄位。

再來看一下自定義的反序列化類,繼承 StdDeserializer,同時重寫 deserialize() 方法,利用 JsonGenerator 讀取 JSON,示例如下:

publicclassCustomDeserializerextendsStdDeserializer<Woman>{
protectedCustomDeserializer(Class<?>vc){
super(vc);
}

publicCustomDeserializer(){
this(null);
}

@Override
publicWomandeserialize(JsonParserp,DeserializationContextctxt)throwsIOException,JsonProcessingException{
JsonNodenode=p.getCodec().readTree(p);
Womanwoman=newWoman();
intage=(Integer)((IntNode)node.get("age")).numberValue();
Stringname=node.get("name").asText();
woman.setAge(age);
woman.setName(name);
returnwoman;
}
}
classWoman{
privateintage;
privateStringname;

publicWoman(){
}

//getter/setter

@Override
publicStringtoString(){
return"Woman{"+
"age="+age+
",name='"+name+'\''+
'}';
}
}

通過 JsonNode 把 JSON 讀取到一個樹形結構中,然後通過 JsonNode 的 get 方法將對應欄位讀取出來,然後生成新的 Java 物件,並返回。

定義好自定義反序列化類後,要想在程式中呼叫它們,同樣需要將其註冊到 ObjectMapper 的 Module 中,示例如下所示:

ObjectMappermapper=newObjectMapper();
SimpleModulemodule=
newSimpleModule("CustomDeserializer",newVersion(1,0,0,null,null,null));
module.addDeserializer(Woman.class,newCustomDeserializer());
mapper.registerModule(module);
Stringjson="{\"name\":\"三妹\",\"age\":18}";
Womanwoman=mapper.readValue(json,Woman.class);
System.out.println(woman);

程式輸出結果如下所示:

Woman{age=18,name='三妹'}

07、結語

哎呀,好像不錯哦,Jackson 絕對配得上“最牛掰”這三個字,雖然有點虛。如果只想簡單的序列化和反序列化,使用 ObjectMapper 的 write 和 read 方法即可。

如果還想更進一步的話,就需要對 ObjectMapper 進行一些自定義配置,或者加一些註解,以及直接自定義序列化和反序列化類,更貼近一些 Java 物件。

需要注意的是,對日期格式的欄位要多加小心,儘量不要使用預設配置,可讀性很差。

好了,通過這篇文章的系統化介紹,相信你已經完全摸透 Jackson 了,我們下篇文章見。

PS:如果你恰好需要一份 Java 精進路線的話,我這裡有一份,我差不多花了 3 天的時間整理的,還挺受歡迎的,已經 2000 多讚了,每個階段都很詳細。

https://www.zhihu.com/question/267403723/answer/1520053322

如果你恰好需要的話,可以點選上面的連結看一看,希望能給你一點點幫助(加油)。