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("