还原json循环引用对象的一种办法

Spance 发布于 2013/08/02 15:34
阅读 8K+
收藏 14

后端数据实体都是由hibernate生成的,与浏览器客户端交互json时,采用了alibaba FastJson库。

首先要说fastJson的确为众多json类库中数一数二的,api简单易用,性能强悍,测试完整,典型的国产高端货。

由于后端涉及几个数据库三四百张表,生成的实体之间的嵌套关系也非常复杂,再由fastJson将Bean转化为json string时,一个非常的典型的问题就出现了,就是对象间的嵌套循环引用,如果没有合理的json生成策略,那将是一个无底洞的死循环,直到堆栈溢出。(循环引用的数据不能排除掉因为前端需要读取)

(简单说就是A引用B,B引用C,C引用A,实际上比这更复杂的环形引用链)

fastJson内置有合理的循环引用检测,采用了比较广泛的json path表示法,避免了反射Bean时循环引用造成的死循环。类似于这样的形式 {"$ref":"$.data[1]"}输出,关键看图fastJson采用循环引用后输出结果!

 

而这种形式,似乎只有dojo的dojox.json.ref提供了相应的parse支持,其它地方似乎没有找到合适的解析方法。所以在前端依然无法得到相应的数据。 

研究了一下fastJson的循环引用表示,然后对前端ExtJs的decode部分进行了重写,于是可以几乎完整的还原原来Java Bean之间嵌套引用关系。

项目前端是ExtJS v3.4所以直接对Ext方法进行覆盖。


String.prototype.startsWith = function (prefix) {
    return prefix && this.length >= prefix.length && this.substring(0, prefix.length) === prefix;
};

if (!window.JSON)
    JSON = {};

if (typeof JSON.retrocycle !== 'function') {
    JSON.retrocycle = (function () {
        'use strict';

        var t_obj = typeof {}, t_arr = Object.prototype.toString.apply([]) , t_str = typeof "";
        var walk = function (path, _xpath, array) {
            if (path.startsWith('$'))   // 基于xpath直接定位
                return path;
            else {      // 相对回溯定位
                var x , j = path.split('..'), k = -j.length + (array ? 2 : 1), last = j.slice(-1)[0].replace('/', '.');
                x = k < 0 ? _xpath.slice(0, k) : _xpath.slice(0);
                if (last && !last.startsWith('.') && !last.startsWith('['))
                    last = '.' + last;
                path = x.join('.') + last;
            }
            return path;    // 最终得到绝对xpath地址
        };

        return function ($) {
            var xpath = ['$'];
            (function rez(value) {
                var i, item, name, path, _x;
                if (value && typeof value === t_obj) {
                    if (Object.prototype.toString.apply(value) === t_arr) {
                        for (i = 0; i < value.length; i += 1) {
                            item = value[i];
                            if (item && typeof item === t_obj) {
                                xpath.push(xpath.pop() + '[' + i + ']');    // 下标引用要合并分级
                                path = item.$ref;
                                if (typeof path === t_str)
                                    value[i] = eval(walk(path, xpath, true));
                                else
                                    rez(item);
                                if (_x = xpath.pop())
                                    xpath.push(_x.slice(0, _x.indexOf('[')));   // 下标引用还原分级
                            }
                        }
                    } else {
                        for (name in value) {
                            if (value.hasOwnProperty(name) && typeof value[name] === t_obj) {
                                xpath.push(name);
                                item = value[name];
                                if (item) {
                                    path = item.$ref;
                                    if (typeof path === t_str)
                                        value[name] = eval(walk(path, xpath));
                                    else
                                        rez(item);
                                }
                                xpath.pop();
                            }
                        }
                    }
                }
            })($);
            return $;
        }
    })();
}

Ext.onReady(function () {

    Ext.decode = function () {
        var isNative = function () {
            var useNative = null;
            return function () {
                if (useNative === null) {
                    useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
                }
                return useNative;
            };
        }();
        var dc,
            doDecode = function (json) {
                return json ? eval("(" + json + ")") : "";
            };

        return function (json) {
            if (!dc) {
                dc = isNative() ? JSON.parse : doDecode;
            }
//            return dc(json);
            return JSON.retrocycle(dc(json));
        }
    }();

    Ext.apply(Ext.util.JSON, {
        decode: Ext.decode
    });
});

通过覆盖以上方法,便可以还原到原java Bean的嵌套引用关系。

透过console观察一下json解析后并作了复原循环引用后的对象属性,如图:

可能有人担心性能问题,简单的用两个例子测试了一下,跑Ext.decode() 100遍的结果:


 

browsers 用时:ms
chrome 28 20-30
firefox 22 20-35
ie6 300-400
ie9 23-30




@wenshao

 目前来看,基本还算满意,不知谁有更好的方法希望也能拿出来分享一下。

以下是话题补充:

@Spance:这是fastJson 对于循环引用的处理 语法 {"$ref":"$"} // 引用根对象 {"$ref":"$"} // 引用根对象 {"$ref":"@"} // 引用自己 {"$ref":".."} // 引用父对象 {"$ref":"../.."} // 引用父对象的父对象 {"$ref":"$.members[0].reportTo"} // 基于路径的引用 (2013/08/02 15:43)
加载中
0
我是潮汐
我是潮汐
既然dojo有,何不把dojo的借鉴一下.
Spance
Spance
对dojo不是很熟悉,没时间仔细研究。。。
0
wenshao
wenshao
不错,我一直希望有人能够做这个事情,在客户端解析fastjson的应用。
wenshao
wenshao
回复 @gohsy : 谢谢的你支持。使用好了并参与其中,才是更好的使用开源方式。也就是所谓的社区能读能改。我打算开一个项目用javascript实现fastjson的引用解析,希望你能够参与其中。
Spance
Spance
很早就在项目中引入了温少侠的fastjson druid,绝对达到商业软件的水准了,屡用不爽,越用越爽。
0
南湖船老大
南湖船老大
很高端。只是想知道,大部分语言的JSON API应该都不支持循环引用吧,那么循环引用是什么样的需求产生的?可以避免不?
Spance
Spance
文章里面已经描述了,由hibernate生成的实体,包含着大量的关联引用,在稍大的一点的项目中,实体对象之间的关联关系会比较复杂,要么就手动处理有选择性的输出关联关系,要么就用fastjson这样能够处理循环引用的库,在数据使用方的底层,在做引用还原。
0
风里的叶子
风里的叶子
可以对每个域模型继承一个接口,接口提供一个将模型转为map的方法,map里可以包含引用,但也是对方转成map的,同时自己在转map那个方法里防止递归引用。
0
刀锋之熠
Ext.define('overrides.JSON', {
    override : 'Ext.JSON',

    decode : function(json, safe) {
        me = this;
        if (typeof me.JSON.retrocycle !== 'function') {
            me.JSON.retrocycle = (function() {
                'use strict';

                var t_obj = typeof {}, t_arr = Object.prototype.toString
                        .apply([]), t_str = typeof "";
                var walk = function(path, _xpath, array) {
                    if (path.startsWith('$')) // 基于xpath直接定位
                        return path;
                    else { // 相对回溯定位
                        var x, j = path.split('..'), k = -j.length
                                + (array ? 2 : 1), last = j.slice(-1)[0]
                                .replace('/', '.');
                        x = k < 0 ? _xpath.slice(0, k) : _xpath.slice(0);
                        if (last && !last.startsWith('.')
                                && !last.startsWith('['))
                            last = '.' + last;
                        path = x.join('.') + last;
                    }
                    return path; // 最终得到绝对xpath地址
                };

                return function($) {
                    var xpath = ['$'];
                    (function rez(value) {
                        var i, item, name, path, _x;
                        if (value && typeof value === t_obj) {
                            if (Object.prototype.toString.apply(value) === t_arr) {
                                for (i = 0; i < value.length; i += 1) {
                                    item = value[i];
                                    if (item && typeof item === t_obj) {
                                        xpath.push(xpath.pop() + '[' + i + ']'); // 下标引用要合并分级
                                        path = item.$ref;
                                        if (typeof path === t_str)
                                            value[i] = eval(walk(path, xpath,
                                                    true));
                                        else
                                            rez(item);
                                        if (_x = xpath.pop())
                                            xpath.push(_x.slice(0, _x
                                                            .indexOf('['))); // 下标引用还原分级
                                    }
                                }
                            } else {
                                for (name in value) {
                                    if (value.hasOwnProperty(name)
                                            && typeof value[name] === t_obj) {
                                        xpath.push(name);
                                        item = value[name];
                                        if (item) {
                                            path = item.$ref;
                                            if (typeof path === t_str)
                                                value[name] = eval(walk(path,
                                                        xpath));
                                            else
                                                rez(item);
                                        }
                                        xpath.pop();
                                    }
                                }
                            }
                        }
                    })($);
                    return $;
                }
            })();
        }
        var isNative = function() {
            var useNative = null;
            return function() {
                if (useNative === null) {
                    useNative = Ext.USE_NATIVE_JSON && window.JSON
                            && JSON.toString() == '[object JSON]';
                }
                return useNative;
            };
        }();
        var decodingFunction;
        doDecode = function(json) {
            return json ? eval("(" + json + ")") : "";
        };
        if (!decodingFunction) {
            // setup decoding function on first access
            decodingFunction = isNative() ? JSON.parse : doDecode;
        }
        try {
            return this.JSON.retrocycle(decodingFunction(json));
        } catch (e) {
            if (safe === true) {
                return null;
            }
            Ext.Error.raise({
                        sourceClass : "Ext.JSON",
                        sourceMethod : "decode",
                        msg : "我尝试解析 an invalid JSON String: " + json
                    });
        }
    }
});

Ext.decode = Ext.JSON.decode;

在Extjs 4.2 里的写法。放在与app目录平齐的overrides里面。

然后在APP.js里面加入下面的东西。

Ext.application({
    name: 'admin',

    extend: 'admin.Application',
    requires: [
//               'overrides.grid.RowEditor'
    'overrides.JSON'
           ],
    autoCreateViewport: true
});



0
不允许改性别
不允许改性别

这个解析的算法还有BUG。就是当A引用B一个集合,A在引用B单个的时候解析出来可能B指向的A就会错误。

举个例子:客户与客户联系人。客户有一个客户联系人的集合的属性,客户还有一个主联系人的属性。同时客户联系人也指向客户有一个属性,当这种对应关系的时候解析就会出错!

我尝试着想要去解决,但是智商有限搞不了。求作者在查看一下。


0
Spance
Spance

引用来自“刘思作”的评论

这个解析的算法还有BUG。就是当A引用B一个集合,A在引用B单个的时候解析出来可能B指向的A就会错误。

举个例子:客户与客户联系人。客户有一个客户联系人的集合的属性,客户还有一个主联系人的属性。同时客户联系人也指向客户有一个属性,当这种对应关系的时候解析就会出错!

我尝试着想要去解决,但是智商有限搞不了。求作者在查看一下。


看来这个问题还是有人关注的哈。 

你可以给点数据,我有空的时候的看看。

返回顶部
顶部