JSON最佳實踐

編程語言 JSON Google XML PHP愛好者 2017-06-07

JSON最佳實踐

JSON是一種文本方式展示結構化數據的方式,從產生的時候開始就由於其簡單好用、跨平臺,特別適合HTTP下數據的傳輸(例如現在很流行的REST)而被廣泛使用。

1、JSON是什麼

JSON起源於1999年的JS語言規範ECMA262的一個子集(即15.12章節描述了格式與解析),後來2003年作為一個數據格式ECMA404(很囧的序號有不有?)發佈。

2006年,作為rfc4627發佈,這時規範增加到18頁,去掉沒用的部分,十頁不到。

JSON的應用很廣泛,這裡有超過100種語言下的JSON庫:json.org。

更多的可以參考這裡,關於json的一切。

2、優缺點、標準與schema

2.1 結構與類型

這估計是最簡單標準規範之一:

- 只有兩種結構:對象內的鍵值對集合結構和數組,對象用{}表示、內部是”key”:”value”,數組用[]表示,不同值用逗號分開

- 基本數值有7個: false / null / true / object / array / number / string

- 再加上結構可以嵌套,進而可以用來表達複雜的數據

- 一個簡單實例:

{

"Image": {

"Width": 800,

"Height": 600,

"Title": "View from 15th Floor",

"Thumbnail": {

"Url": "http://www.example.com/image/481989943",

"Height": 125,

"Width": "100"

},

"IDs": [116, 943, 234, 38793]

}

}

2.2 優點

基於純文本,所以對於人類閱讀是很友好的。

規範簡單,所以容易處理,開箱即用,特別是JS類的ECMA腳本里是內建支持的,可以直接作為對象使用。

平臺無關性,因為類型和結構都是平臺無關的,而且好處理,容易實現不同語言的處理類庫,可以作為多個不同異構系統之間的數據傳輸格式協議,特別是在HTTP/REST下的數據格式。

2.3 缺點

缺點也很明顯:

- 性能一般,文本表示的數據一般來說比二進制大得多,在數據傳輸上和解析處理上都要更影響性能。

- 缺乏schema,跟同是文本數據格式的XML比,在類型的嚴格性和豐富性上要差很多。XML可以藉由XSD或DTD來定義複雜的格式,並由此來驗證XML文檔是否符合格式要求,甚至進一步的,可以基於XSD來生成具體語言的操作代碼,例如apache xmlbeans。並且這些工具組合到一起,形成一套龐大的生態,例如基於XML可以實現SOAP和WSDL,一系列的ws-*規範。但是我們也可以看到JSON在缺乏規範的情況下,實際上有更大一些的靈活性,特別是近年來REST的快速發展,已經有一些schema相關的發展(例如理解JSON Schema,使用JSON Schema, 在線schema測試),也有類似於WSDL的WADL出現。

3. 常用技術與工具

3.1 相關技術以及與XML的關係

使用JSON實現RPC(類似XML-RPC):JSON-RPC

使用JSON實現path查詢操作(類似XML-PATH):JsonPATH

在線查詢工具:JsonPATH

例如上面的示例json,用表達式$.Image.IDs[:1]查詢,得到116:

image

我們看到JSON與XML是如此之像,實際上這兩個格式可以看做一個是學院排,一個是平民派。一個對象從POJO轉換成XML與JSON的過程,基本是一致的(絕大部分工作可以複用,以後有機會再詳細聊這個過程),10年前我自己也做過一個基於XML的RPC(http://code.google.com/p/rpcfx/,貌似已經被牆),裡面實現了Java和dotnet、JS的XML序列化與反序列化,同時作為一個副產品,實現了JSON序列化。

後來thoughtsworks公司出品的XStream就是同時做了XML與JSON的序列化。而創建Jackson庫的組織本來叫fasterxml,就是處理xml的。當然從這個角度來看,Fastjson庫,稍微改改也是一個高性能的XML序列化庫。

只是XML有著更嚴格的結構,更豐富的工具生態,拿查詢與操作來說,XML還有XQuery、XLST等工具。處理方式上也有DOM方式與SAX流模式,這兩個絕然不同的技術。

單從性能來考慮,XML更是有VTD-XML這種解決了DOM消耗太大內存與SAX只能單向每個節點讀一次不能隨機讀的缺點的高性能處理方式。

3.2 Java類庫

Fastjson

Jackson

Gson

Xstream

3.3 工具

格式化工具:jsbeautifier

chrome插件:5個Json View插件

在線Mock: 在線mock

其他Mock:SoapUI可以支持,SwaggerUI也可以,RestMock也可以。

image

image

4. JSON編碼指南

4.1 Google JSON風格指南

遵循好的設計與編碼風格,能提前解決80%的問題:

- 英文版Google JSON Style Guide:https://google.github.io/styleguide/jsoncstyleguide.xml

- 中文版Google JSON風格指南:https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md

簡單摘錄如下:

- 屬性名和值都是用雙引號,不要把註釋寫到對象裡面,對象數據要簡潔

- 不要隨意結構化分組對象,推薦是用扁平化方式,層次不要太複雜

- 命名方式要有意義,比如單複數表示

- 駝峰式命名,遵循Bean規範

- 使用版本來控制變更衝突

- 對於一些關鍵字,不要拿來做key

- 如果一個屬性是可選的或者包含空值或null值,考慮從JSON中去掉該屬性,除非它的存在有很強的語義原因

- 序列化枚舉類型時,使用name而不是value

- 日期要用標準格式處理

- 設計好通用的分頁參數

- 設計好異常處理

4.2 使用JSON實現API

JSON API與Google JSON風格指南有很多可以相互參照之處。

JSON API是數據交互規範,用以定義客戶端如何獲取與修改資源,以及服務器如何響應對應請求。

JSON API設計用來最小化請求的數量,以及客戶端與服務器間傳輸的數據量。在高效實現的同時,無需犧牲可讀性、靈活性和可發現性。

5. REST

todo list

- dubbox

- resteasy

- restlet

- jersey

image

6. SwaggerUI實現API文檔管理與在線測試

todo list

image

7. JSON使用場景分析

JSON的使用,依據不同用途,有幾個典型的場景:

1. 內部後臺系統之間的數據傳輸,此種情況下基於HTTP的JSON格式其實沒有優勢。

2. 前後臺之間的API調用,典型的是前端作為React/VUE/AngularJS/ExtJS等框架做的,前後端使用JSON交互。

- 此時可以使用類似Dubbox之類的框架,或者原始一些SpringMVC的Controller上直接@ResponseBody或@RestController也可以。

- 強烈建議在Dubbox之類的rest之上再加一個Nginx轉發,這樣一些策略的控制,比如同源的控制、簡單的緩存策略、安全策略等都可以放到Nginx上來做,也利於多個機器時的負載均衡。

- 建議使用swaggerUI來自動實現API文檔和在線測試。功能很強大,操作簡單,而且可以mock接口,在後臺沒有做好之前,前臺就可以先開發了。

- 可以使用RestUnit或SoapUI來實現自動化測試與壓力測試。

提供給第三方的開發接口API

基本同上,可以參考Google JSON風格指南與JSON API章節。

8.JSON的一些經驗

最近在協助處理一些Fastjson的bug問題,發現最常見的其實是大家使用的不規範性,這樣碰到各種坑的可能性就很大。根據我平時使用的經驗,以及總結大家常見的問題,歸納如下:

7.1 遵循Java Beans規範與JSON規範

實踐告訴我們:遵循beans規範和JSON規範的方式,能減少大部分的問題,比如正確實現setter、getter,用別名就加annotation。注意基本類型的匹配轉換,比如在fastjson的issue見到試圖把”{“a”:{}}”中的a轉換成List的。

7.2 使用正常的key

儘量不要使用數字等字符開頭的key,儘量使用符合Java的class或property命名規範的key,這樣會減少不必要的衝突。在jsonpath或js裡,a.1可能會被解釋成a[1]或a[“1”],這些都會帶來不必要的麻煩。

7.3 關於日期處理

這一點前面的Google JSON風格指南里也提到了,儘量使用標準的日期格式。或者序列化和反序列化裡都是用同樣的datePattern格式。

7.4 關於自定義序列化與反序列化(包括過濾器)

對於新手來說,自定義序列化是一切罪惡的根源。

儘量不要使用自定義序列化,除非萬不得已,優先考慮使用註解過濾,別名等方式,甚至是重新建一個VO類來組裝實際需要的屬性。使用自定義序列化時一切要小心,因為這樣會導致兩個問題:

- 改變了pojo <-> jsonstring 的自然對應關係,從而不利於閱讀代碼和排查問題,你改變的關係無法簡單的從bean和json上看出來了;

- 反序列化可能出錯,因為對應不上原來的屬性了。

如果只是序列化發出去(響應)的是JSON數據、傳過來(請求)的數據格式跟JSON無關或者是標準的,此時自定義序列化就無所謂了,反正是要接收方來處理。

7.5 JSONObject的使用

JSONObject是JSON字符串與pojo對象轉換過程中的中間表達類型,實現了Map接口,可以看做是一個模擬JSON對象鍵值對再加上多層嵌套的數據集合,對象的每一個基本類型屬性是map裡的一個key-value,一個非基本類型屬性是一個嵌套的JSONObject對象(key是屬性名稱,value是表示這個屬性值的對象的JSONObject)。如果以前用過apache beanutils裡的DynamicBean之類的,就知道JSONObject也是一種動態描述Bean的實現,相當於是拆解了Bean本身的結構與數據。這時候由於JSONObject裡可能會沒有記錄全部的Bean類型數據,例如泛型的具體子類型之類的元數據,如果JSONObject與正常的POJO混用,出現問題的概率較高。

下列方式儘量不要使用:

public class TestBean{

@Setter @Getter

private TestBean1 testBean1;

@Setter @Getter

private JSONObject testBean2; // 儘量不要在POJO裡用JSONObject

}

應該從設計上改為都用POJO比較合適:

public class TestBean{

@Setter @Getter

private TestBean1 testBean1;

@Setter @Getter

private TestBean2 testBean2;; // 使用POJO

}

相對的,寫一些臨時性的測試代碼,demo代碼,可以直接全部用JSONObject先快速run起來。

同理,jsonstring中嵌套jsonstring也儘量不要用,例如:

{

"name":"zhangsan",

"score":"{"math":78,"history":82}"

}

應該改為全部都是JSON風格的結構:

{

"name":"zhangsan",

"score":{

"math":78,

"history":82

}

}

另外,對於jsonstring轉POJO(或POJO轉jsonstring),儘量使用直接轉的方式,而不是先轉成JSONObject過渡的方式。特別是對於Fastjson,由於性能優化的考慮,這兩個執行的代碼是不一樣的,可能導致不一樣的結果。

String jsonstring = "{"a":12}";

// 不推薦這種方式

// 除非這裡需要對jsonObject做一些簡單處理

JSONObject jsonObject = JSON.parseObject(jsonstring);

A a = jsonObject.toJavaObject(A.class);

// 推薦方式

A a = JSON.parseObject(jsonstring, A.class);

7.6 Hibernate相關問題

懶加載與級聯,可能導致出現問題,例如hibernate,建議封裝一層VO類型來序列化。使用VO類還有一個好處,就是可以去掉一些沒用的屬性,減少數據量,同時可以加上額外的屬性。

7.7 深層嵌套與泛型問題

儘量不要在使用過多的層次嵌套的同時使用泛型(List、Map等),可能導致類型丟失,而且問題比較難查。

7.8 抽象類型與子類型問題

儘量不要在同一個Bean的層次結構裡使用多個子類型對象,可能導致類型丟失,而且問題比較難查。當然我們可以通過代碼顯示的傳遞各種正確的類型,但是這樣做引入了更多的不確定性。良好的做法應該是一開始設計時就避免出現這些問題。

7.9 避免循環引用

儘量避免循環引用,這個雖然可以通過序列化特性禁掉,但是如果能避免則避免。

7.10 注意編碼和不可見字符(特別是二進制數據流)

對於InputStream、OutputStream的處理,有時候會報一些奇怪的錯誤,not match之類的,這時候也許我們看日誌裡的json字符串可能很正常,但就是出錯。

這時可能就是編碼的問題了,可能是導致字符錯亂,也可能是因為UTF-8文件的BOM頭,這些潛在的問題可能在二進制數據轉文本的時候,因為一些不可見字符無法顯示,導致日誌看起來只有正常字符而是正確的,問題很難排查。

處理辦法就是按二進制的方式把Stream保存起來,然後按hex方式查看,看看是否有多餘字符,或者其他錯誤。

8.fastjson的最佳實踐

8.1 Maven下引入Fastjson

pom.xml文件裡添加依賴即可:

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>fastjson</artifactId>

<version>1.2.32</version>

</dependency>

8.2 序列化一個對象成JSON字符串

User user = new User();

user.setName("校長");

user.setAge(3);

user.setSalary(new BigDecimal("123456789.0123"));

String jsonString = JSON.toJSONString(user);

System.out.println(jsonString);

// 輸出 {"age":3,"name":"校長","old":false,"salary":123456789.0123}

8.3 反序列化一個JSON字符串成Java對象

String jsonString = "{"age":3,"birthdate":1496738822842,"name":"校長","old":true,"salary":123456789.0123}";

User u = JSON.parseObject(jsonString ,User.class);

System.out.println(u.getName());

// 輸出 校長

String jsonStringArray = "[{"age":3,"birthdate":1496738822842,"name":"校長","old":true,"salary":123456789.0123}]";

List<User> userList = JSON.parseArray(jsonStringArray, User.class);

System.out.println(userList.size());

// 輸出 1

8.4 日期格式處理

Fastjson能識別下面這麼多種日期格式的字符串:

private final static String defaultPatttern = "yyyy-MM-dd HH:mm:ss";

private final static DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern(defaultPatttern);

private final static DateTimeFormatter formatter_dt19_tw = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_cn = DateTimeFormatter.ofPattern("yyyy年M月d日 HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_cn_1 = DateTimeFormatter.ofPattern("yyyy年M月d日 H時m分s秒");

private final static DateTimeFormatter formatter_dt19_kr = DateTimeFormatter.ofPattern("yyyy년M월d일 HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_us = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_eur = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_de = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss");

private final static DateTimeFormatter formatter_dt19_in = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm:ss");

private final static DateTimeFormatter formatter_d8 = DateTimeFormatter.ofPattern("yyyyMMdd");

private final static DateTimeFormatter formatter_d10_tw = DateTimeFormatter.ofPattern("yyyy/MM/dd");

private final static DateTimeFormatter formatter_d10_cn = DateTimeFormatter.ofPattern("yyyy年M月d日");

private final static DateTimeFormatter formatter_d10_kr = DateTimeFormatter.ofPattern("yyyy년M월d일");

private final static DateTimeFormatter formatter_d10_us = DateTimeFormatter.ofPattern("MM/dd/yyyy");

private final static DateTimeFormatter formatter_d10_eur = DateTimeFormatter.ofPattern("dd/MM/yyyy");

private final static DateTimeFormatter formatter_d10_de = DateTimeFormatter.ofPattern("dd.MM.yyyy");

private final static DateTimeFormatter formatter_d10_in = DateTimeFormatter.ofPattern("dd-MM-yyyy");

private final static DateTimeFormatter ISO_FIXED_FORMAT =

DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());

private final static String formatter_iso8601_pattern = "yyyy-MM-dd'T'HH:mm:ss";

private final static DateTimeFormatter formatter_iso8601 = DateTimeFormatter.ofPattern(formatter_iso8601_pattern);

默認序列化Date輸出使用”yyyy-MM-dd HH:mm:ss”格式,可以用UseISO8601DateFormat特性換成”yyyy-MM-dd’T’HH:mm:ss”格式。

JSON.defaultTimeZone = TimeZone.getTimeZone("Asia/Shanghai");

JSON.defaultLocale = Locale.US;

public static class Model {

@JSONField(format = "MMM dd, yyyy h:mm:ss aa")

private java.util.Date date;

public java.util.Date getDate() {

return date;

}

public void setDate(java.util.Date date) {

this.date = date;

}

@JSONField(format = "MMM-dd-yyyy h:mm:ss aa")

public java.sql.Date date2;

}

8.5 常見序列化特性的使用

Fastjson的序列化特性定義在枚舉類com\alibaba\fastjson\serializer\SerializerFeature.java中,目前正好有30項。

可以通過設置多個特性到FastjsonConfig中全局使用,也可以在某個具體的JSON.writeJSONString時作為參數使用。

1. QuoteFieldNames, //key使用引號

2. UseSingleQuotes, //使用單引號

3. WriteMapNullValue, //輸出Map的null值

4. WriteEnumUsingToString, //枚舉屬性輸出toString的結果

5. WriteEnumUsingName, //枚舉數據輸出name

6. UseISO8601DateFormat, //使用日期格式

7. WriteNullListAsEmpty, //List為空則輸出[]

8. WriteNullStringAsEmpty, //String為空則輸出””

9. WriteNullNumberAsZero, //Number類型為空則輸出0

10. WriteNullBooleanAsFalse, //Boolean類型為空則輸出false

11. SkipTransientField,

12. SortField, //排序字段

13. WriteTabAsSpecial,

14. PrettyFormat, // 格式化JSON縮進

15. WriteClassName, // 輸出類名

16. DisableCircularReferenceDetect, // 禁止循環引用

17. WriteSlashAsSpecial, // 對斜槓’/’進行轉義

18. BrowserCompatible,

19. WriteDateUseDateFormat, // 全局修改日期格式,默認為false。JSON.DEFFAULT_DATE_FORMAT = “yyyy-MM-dd”;JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat);

20. NotWriteRootClassName,

21. DisableCheckSpecialChar,

22. BeanToArray,

23. WriteNonStringKeyAsString,

24. NotWriteDefaultValue,

25. BrowserSecure,

26. IgnoreNonFieldGetter,

27. WriteNonStringValueAsString,

28. IgnoreErrorGetter,

29. WriteBigDecimalAsPlain,

30. MapSortField

使用示例如下(可以參見此處):

Word word = new Word();

word.setA("a");

word.setB(2);

word.setC(true);

word.setD("d");

word.setE("");

word.setF(null);

word.setDate(new Date());

System.out.println(JSON.toJSONString(word));

System.out.println(JSON.toJSONString(word, SerializerFeature.PrettyFormat,

SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty,

SerializerFeature.DisableCircularReferenceDetect,

SerializerFeature.WriteNullListAsEmpty));

8.6 Annotation註解的使用

1) JSONField

可以配置在屬性(setter、getter)和字段(必須是public field)上。

詳情參見此處:JSONField用法

package com.alibaba.fastjson.annotation;

public @interface JSONField {

// 配置序列化和反序列化的順序,1.1.42版本之後才支持

int ordinal() default 0;

// 指定字段的名稱

String name() default "";

// 指定字段的格式,對日期格式有用

String format() default "";

// 是否序列化

boolean serialize() default true;

// 是否反序列化

boolean deserialize() default true;

}

@JSONField(name="ID")

public int getId() {return id;}

// 配置date序列化和反序列使用yyyyMMdd日期格式

@JSONField(format="yyyyMMdd")

public Date date1;

// 不序列化

@JSONField(serialize=false)

public Date date2;

// 不反序列化

@JSONField(deserialize=false)

public Date date3;

// 按ordinal排序

@JSONField(ordinal = 2)

private int f1;

@JSONField(ordinal = 1)

private int f2;

2) JSONType

自定義序列化:ObjectSerializer

子類型處理:SeeAlso

JSONType.alphabetic屬性: fastjson缺省時會使用字母序序列化,如果你是希望按照java fields/getters的自然順序序列化,可以配置JSONType.alphabetic,使用方法如下:

@JSONType(alphabetic = false)

public static class B {

public int f2;

public int f1;

public int f0;

}

8.7 自定義序列化與反序列化

自定義序列化

只需要2步:參見此處

1)實現ObjectSerializer

public class CharacterSerializer implements ObjectSerializer {

public void write(JSONSerializer serializer,

Object object,

Object fieldName,

Type fieldType,

int features) throws IOException {

SerializeWriter out = serializer.out;

Character value = (Character) object;

if (value == null) {

out.writeString("");

return;

}

char c = value.charValue();

if (c == 0) {

out.writeString("

相關推薦

推薦中...