處理JSON迴圈引用序列化與反序列化問題的終極方案

来源:https://www.cnblogs.com/harry-lien/archive/2020/06/30/13212792.html
-Advertisement-
Play Games

迴圈引用對象序列化?這似乎是一個老生常談的問題,但是99.9%的人所謂的『解決』,都是在『逃避』這個問題。 ...


  重要聲明:此博借鑒了阿裡巴巴 Fastjson 的一點思想

  The important declaration: This mind comes form Fastjson Lib (Alibaba).

 

  說點『科(fei)普(hua)』先:

  1. 對於web前端,JSON序列化可以說是在 與服務端通訊(ajax+json) ,和使用 localStorage(讀 + 寫) 時。
  2. 對於服務端,我相信絕大多數人遇到問題是在於輸出JSON序列化數據。

  『科(fei)普(hua)』完畢。

  Let's talk about some basic things first:

  1. For the web front-end, JSON is usually used on conmunication with server part, and localStorage.
  2. For the back-end, I think where the most people have problem is serialization.

  Basic things done.

  迴圈引用對象序列化?這似乎是一個老生常談的問題,但是99.9%的人所謂的『解決』,都是在『逃避』這個問題,不信你搜搜『迴圈引用 JSON』試試?
  後端相關文章一定告訴你要『禁用迴圈引用檢測』,『截斷對象』和『配置序列化規則( 各種Filter )』等降( tao )魔( bi )大法

  前端相關文章最多就是告訴你可以設置一下序列化深度。
  於是導致了數據丟失,花大量時間在序列化和反序列化邏輯上做判斷和處理,出現許多難以維護的bug,浪費社會資源。
  說到底,JSON不就是數據交換嗎?它不支持表示迴圈引用對象,那就對它的語法進行擴展,讓它能夠表示不就好了?迎刃而解。
  如何表示迴圈引用/重覆引用對象?阿裡的Fastjson已經告訴了我們答案:

  1. 創建一個對象表示引用對象,它僅有一個 key="$ref",value=對象引用路徑
  2. 對象引用路徑使用 "$" 表示根對象的引用,使用 "[數字]" 表示數組元素,使用 ".property" 表示對象欄位
  3. 形如{ "$ref":"$.key1.array[3].key" }

  Fastjson 是 Java 的庫,服務於後端,我在這裡用 TypeScript 手寫一下它的實現以便前端能夠享受這個人類寶貴的精神財富。

  首先寫個序列化和反序列化方法:( serializeCircular 和 parseCircular )

 

  Serialization Circular? It seems to be a very familiar issue? But what 99.9% of people call "handle" is "escape".Or you can try searching "Circular JSON".

  The information about back-end would tell you to "disable circular checking", "cut object" and "create filters" to "h( e )a( s )n( c )d( a )l( p )e" it.

  The information about front-end would tell you to change/set the deep/level in the settings of serialization.

  And all above would cause losing data, and you will take huge time at the further logics of serialization, fixing bugs. It's a sheer waste.

  But all in all, JSON is just for data-exchanging. It doesn't support circular , why not expand the role to make it supports? 

  How to express circular or repeatition? The answer has already been in the Fastjson lib, which built by Alibaba:

  1. Create an object to express the circular, which has only one property named "$ref" and the value of it is a path-expression, which express the position of the circular from the root.
  2. It uses "$" to express the reference of the root, "[index:number]" to express the item of an array, and ".key" to express the key of an common object.
  3. Just like { "$ref":"$.key1.array[3].key" }

  But Fastjson is a Java Lab for back-end, so I implement it by TypeScript for front-end.

  At the first, make serialization and parse function: ( serializeCircular and parseCircular )

 1 const _parseCircular = (root: any, parent: any, objkey: string | number) => {
 2   const obj = parent[objkey];
 3   if (null === obj || typeof obj !== "object") {
 4     //
 5   } else if (Array.isArray(obj)) {
 6     for (let i = 0; i < obj.length; i++) {
 7       _parseCircular(root, obj, i);
 8     }
 9   } else if (!!obj["$ref"]) {
10     let paths = (obj["$ref"] as string).split(/\.|\[|\]/).filter(s => !!s);
11     paths.shift();
12     parent[objkey] = paths.reduce((a, b) => a[b], root);
13   } else {
14     Object.keys(obj).forEach(key => {
15       _parseCircular(root, obj, key);
16     });
17   }
18 };
19 const _serializeCircular = (parent: any, base: string, objkey: string | number, obj_key_map: Map<string, any>, result: any) => {
20   const obj = parent[objkey];
21   if (null === obj || typeof obj !== "object") {
22     result[objkey] = obj;
23   } else if (obj_key_map.has(obj)) {
24     result[objkey] = { $ref: obj_key_map.get(obj) };
25   } else {
26     const endFix = Array.isArray(parent) ? `[${objkey}]` : `.${objkey}`;
27     let objrefstr = `${base}${endFix}`;
28     obj_key_map.set(obj, objrefstr);
29     if (Array.isArray(obj)) {
30       result = result[objkey] = [];
31       for (let i = 0; i < obj.length; i++) {
32         _serializeCircular(obj, objrefstr, i, obj_key_map, result);
33       }
34     } else {
35       result = result[objkey] = {};
36       Object.keys(obj).forEach(key => {
37         _serializeCircular(obj, objrefstr, key, obj_key_map, result);
38       });
39     }
40   }
41 };
42 const serializeCircular = (root: any) => {
43   const map = new Map();
44   map.set(root, "$");
45   if (Array.isArray(root)) {
46     let result = [];
47     for (let i = 0; i < root.length; i++) {
48       _serializeCircular(root, "$", i, map, result);
49     }
50     return result;
51   } else if (null !== root && typeof root === "object") {
52     let result = {};
53     Object.keys(root).forEach(key => {
54       _serializeCircular(root, "$", key, map, result);
55     });
56     return result;
57   } else {
58     return root;
59   }
60 };
61 const parseCircular = (root: any): any => {
62   if (Array.isArray(root)) {
63     for (let i = 0; i < root.length; i++) {
64       _parseCircular(root, root, i);
65     }
66   } else if (null !== root && typeof root === "object") {
67     Object.keys(root).forEach(key => {
68       _parseCircular(root, root, key);
69     });
70   }
71   return root;
72 };

  然後你可以僅僅只是用在某些特定的地方 ( 推薦 ),如 RPC 和 localStorage

  或者直接替換掉原本的 JSON.stringify 和 JSON.parse ( 也推薦 ) ^_^ 

  Then you can just use it at some special places. ( recommend ) such as RPC and localStorage

  Or just replace the original JSON.stringify and JSON.parse.( recommend too ) ^_^

 1 let stringifyInited = false;
 2 if (!stringifyInited && (stringifyInited = true)) {
 3   const oriStringify = JSON.stringify;
 4   const oriParse = JSON.parse;
 5   const serialize = serializeCircular;
 6   const parse = parseCircular;
 7   JSON.stringify = function (this: any) {
 8     const args = Array.from(arguments) as any;
 9     args[0] = serialize(args[0]);
10     return oriStringify.apply(this, args);
11   } as any;
12   JSON.parse = function (this: any) {
13     const args = Array.from(arguments) as any;
14     let res = oriParse.apply(this, args);
15     return parse(res);
16   } as any;
17 }

  測試:

  Test:

 1 // 測試 test
 2 
 3 (() => {
 4     let abc = {} as any;
 5     abc.a = {};
 6     abc.b = {};
 7     abc.a.b = abc.b;
 8     abc.b.a = abc.a;
 9     let abcSerialization = JSON.stringify(abc);
10     console.log(abcSerialization);
11     let abcParse = JSON.parse(abcSerialization);
12     console.log(abcParse.a===abcParse.b.a);
13 })();
14 
15 // {"a":{"b":{"a":{"$ref":"$.a"}}},"b":{"$ref":"$.a.b"}}
16 // true

 

  最後,需要 Javascript 版本的朋友們,只需把類型限定標記 ( : ??? 和 as ??? ) 去掉。

  感謝閱讀

  At the end, my friends, if need a JavaScript version, just remove the limitation marks ( : ??? 和 as ??? )

  Thanks for reading

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在sparkSQL 中將計算結果保存為視圖,關聯其他表後出現結果匹配錯誤,通過分析發現,是因為sql語句中使用了表達式 row_number() over(order by 1) 其實該表達式並沒有執行,真正執行的時候是需要觸發action (例如 show, count, top .......) ...
  • 序 平時在Oracle的日常相關開發這種,總會遇到各種各樣的坑,索性在這裡記錄下,後續慢慢更新 如有問題,望指正,不勝感激! 1、Oracle 12c登錄問題 背景:之前一直在 Oracle 11g 資料庫上進行相關開發,最近新進項目組使用的是 Oracle 12c 遂入坑12c,瞭解一下新特性 問 ...
  • Redis的發佈與訂閱,有點類似於消息隊列,發送者往頻道發送消息,頻道的訂閱者接收消息。 1. 發佈與訂閱示例 首先,在本機開啟第1個Redis客戶端,執行如下命令訂閱blog.redis頻道: SUBSCRIBE "blog.redis" 然後,在本機開啟第2個Redis客戶端,執行相同的命令訂閱 ...
  • 本文更新於2019-06-23,使用MySQL 5.7,操作系統為Deepin 15.4。 和大多數資料庫不同,插件式存儲引擎是MySQL最重要的特性之一。 InnoDB InnoDB表提供事務安全。 InnoDB表支持外鍵。創建外鍵時,要求父表必須有對應的索引,子表在創建外鍵時也會自動創建對應的索 ...
  • 索引用於快速找出在某個列中某一特定值的行。不使索引,資料庫必須從第一條記錄開始讀完整個表,直到找到相關行。如果表中查詢的列有一個索引,資料庫能快速到達一個位置去搜尋數據,而不必查看所有數據。 索引的含義和特點: 索引是一個單獨的、存儲在磁碟上的數據結構,他們包含著對數據表裡所有記錄的應用指針。使用索 ...
  • 前言: 最近在整理自己的技術棧,收集了一些自己認為比較重要的知識點分享給大家。 runloop 1. iOS中觸摸事件傳遞和響應原理 2. 為什麼只有主線程的runloop是開啟的 3. 為什麼只在主線程刷新UI 4. PerformSelector和runloop的關係 KVO 1. 實現原理 2 ...
  • 為了不顯示標題欄,所以主題使用了 NoActionBar,這也直接導致選項菜單無處顯示,添加一個ToolBar,自定義標題欄。添加menu資源文件夾,添加menu文件,在activity創建的時候創建menu,重載onCreateOptionsMenu,添加菜單項點擊事件onOptionsItemS... ...
  • 切記,一定要先綁定viewpage再添加tab,否則tab的標題無法正常顯示出來。setupwithviewpager會在後臺做很多事,比如清空tabs等,為了更安全的顯示我們的tabs,在綁定viewpager之後,先清空一下tabs再添加 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...