fastjson:如何快速查找JSON Object中有没有指定名字的字段?

23 篇文章 1 订阅

需求

如本文标题,这是我最近遇到的一个问题,
如下是Spring服务收到的一个HTTP服务请求BODY中的JSON格式请求参数.
我需要知道这个请求中有没有名为tokenId的字段,如果有则获取该字段值,这决定着后续如何解析这个JSON.

{
    "deviceBean": {
        "_new": "0",
        "modified": "000000",
        "initialized": "7FFFFF",
        "id": "22",
        "groupId": "4",
        "features": "0",
        "name": "测试设备21",
        "physicalAddress": "000000000015",
        "addressType": "MAC",
        "status": "ENABLE",
        "network": "4G",
        "model": "XIAKEDEV0",
        "deviceDetail": "{\"device_name\":\"AN01\",\"made_date\":\"2022-01-02\",\"manufacturer\":\"NXP\"}",
        "props": "{\"defineScreen\":{\"INCHSIZE0\":55,\"SCR0\":\"SCR0 1080 X 960 rectangle {0, 0, 1080, 960}\"},\"last_active_time\":\"2022-06-22 12:12:12\",\"disk_capacity\":\"1.2GB\"}",
        "createTime": "2024-07-13 18:13:07",
        "updateTime": "2024-07-13 18:13:07",
        "onlineFlag": "-1",
        "tokenId": "HELLO",
        "rootGroup": "100"
    },
    "targetGroupId":32,
    "targetGroupName":"test group",
    "array":["test group",false,2323,"tokenId"],
    "geoId":320,
    "tokenId": "TOKEN:bf4f202802020aa8"
}

显然,直接用JSON解析工具解析这个内容,获取 tokenId字段是最简单的方法,但是为了获取这个字段,不论是什么JSON解析工具都要解析完整个JSON数据才能得到.
但只为一个字段,就要解析全部数据,这在时间上这有些不划算呐.
你会问,为什么要执着的这点时间上的损失呢?
这个应用场景是在每个HTTP请求的拦截器HandlerInterceptor,那么如何能尽可能降低拦截器的耗时,对于整体系统响应性能是很重要的.
如果只是为了提取所关注的一个字段,就要对输入的JSON全部解析,这显然不是最有效率的办法.
上面这个例子中JSON数据并不长,还能接受,如果输入的JSON有长达几百KB时,这个时间损失就会非常明显.

解决方案

为了提取JSON Object中关注的字段,我尝试了各种办法:

  1. Antlr4 语法树(JSONVisitor)遍历解析测试

    Antlr4生成的解析器(Parser)对JSON完成解析生成语法树ParseTree,然后用JSONVisitor对语法树进行遍历,找到关注的字段.
    这个方案要先解析所有语法树再遍历语法树,性能与fastjson解析全部内容并没有明显提高(25%左右)

  2. Antlr4 词法分析器(Lexer)解析测试

    方案1的主要时间花在了解析并创建语法树过程,所以方案2基于antlr4,但不生成语法树(ParseTree),用Antlr4生成的词法解析器(Lexer)对JSON进行分析,对于Object类型的JSON,非关注的字段直接跳过.
    即遇到值对(Pair)[STRING ':' value],如果STRING不是匹配的字段名,直接跳到下一个值对(Pair), 遇要闭合的Token(LBRACE,LBRACKET,即'{','[')直接跳过,直到下结束Token(RBRACE,RBRACKET,即'}',']').

  3. fastjson词法分析器(Lexer)解析测试

    基于fastjson的词法解析器(JSONLexer)对Object类型的JSON进行分析,非关注的字段直接跳过.
    方案2的性能也不令人满意,究其原因是自娘胎里带的,也就是anntlr4生成的词法解析器因为要面向通用场景,逻辑太过复杂,造成效率不佳, 所以与方案2思路相同,只是采用fastjson的词法解析器(JSONLexer).

  4. fastjson 词法分析器(JSONScanner)字段名查找(seekObjectToField)测试

    基于fastjson,直接调用词法解析器JSONScanner.seekObjectToField(long fieldNameHash, boolean deepScan)方法获取关注字段的值.

经过反复性能测试比较,最终选择使用方案4,即 fastjson 的词法解析器(JSONScanner)提供的seekObjectToField(long fieldNameHash, boolean deepScan)方法来实现字段查找功能。比ANTLR 4生成的词法解析器快一个数量级。当然也比用fastjson完全解析JSON来得快.

因为都是基于fastjson的词法解析器(JSONLexer),所以方案3和方案4的性能相差不大

JSONScanner

com.alibaba.fastjson.parser.JSONScanner原本就提供了一个方法seekObjectToField(long fieldNameHash, boolean deepScan)查找JSON中指定的字段,
有了这个方法,前面的一切努力和折腾都白费了.用起来很简单:

import com.alibaba.fastjson.parser.DefaultJSONParser;
import com.alibaba.fastjson.parser.JSONLexer;
import com.alibaba.fastjson.parser.JSONScanner;
import com.alibaba.fastjson.parser.JSONToken;
import com.alibaba.fastjson.util.TypeUtils;

public class FastjsonPairMatcher0 {
	/**
	 * 查找JSON中key指定名称的字段,如果找到解析并返回字段值,否则返回{@code null},
	 * 如果字段的值为'null'也同样返回{@code null}
	 * @param src
	 * @param key
	 */
	public static Object seekField(String src, String key) {
		String str = asInputString(src);
		@SuppressWarnings("resource")
		DefaultJSONParser parser = new DefaultJSONParser(src);
		JSONScanner lexerBase = (JSONScanner)parser.getLexer();
		/** 如果不是json object直接返回null */
		if (JSONToken.LBRACE == lexerBase.token()) {
			/**
			 * 参照 com.alibaba.fastjson.JSONPath.PropertySegment 中
			 * 对 JSONLexerBase.seekObjectToField方法的调用
			 */
			long propertyNameHash = TypeUtils.fnv1a_64(key);
			int matchStat = lexerBase.seekObjectToField(propertyNameHash, false);
			if (matchStat == JSONLexer.VALUE) {
				/** 如果有指定的则解析并返回 */
				return parser.parse();
			}
		}
        return null;
	}
}

具体数据请执行性能测试。

性能测试比较

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.gitee.l0km.jsonvisitor.PerformanceTest
[main] (Timecost.java:36) ==fastjson 解析测试[基准]
[main] (Timecost.java:39) TIME COST: [79.0963ms]<<<首次执行时间
[main] (Timecost.java:39) TIME COST: [5.7771ms]<<<第二次执行时间
[main] (Timecost.java:41) TOTAL TIME COST: [164.3285ms]
[main] (Timecost.java:42) AVG TIME COST: [1.6433ms] for 100 times<<<100次测试平均值
[main] (Timecost.java:36) ==Antlr4 语法树(JSONVisitor)遍历解析测试
[main] (Timecost.java:39) TIME COST: [95.7972ms]<<<首次执行时间
[main] (Timecost.java:39) TIME COST: [29.3285ms]<<<第二次执行时间
[main] (Timecost.java:41) TOTAL TIME COST: [940.5354ms]
[main] (Timecost.java:42) AVG TIME COST: [9.4054ms] for 100 times<<<100次测试平均值
[main] (Timecost.java:36) ==Antlr4 词法分析器(Lexer)解析测试
[main] (Timecost.java:39) TIME COST: [29.8893ms]<<<首次执行时间
[main] (Timecost.java:39) TIME COST: [5.8947ms]<<<第二次执行时间
[main] (Timecost.java:41) TOTAL TIME COST: [653.2166ms]
[main] (Timecost.java:42) AVG TIME COST: [6.5322ms] for 100 times<<<100次测试平均值
[main] (Timecost.java:36) ==fastjson词法分析器(Lexer)解析测试
[main] (Timecost.java:39) TIME COST: [6.7722ms]<<<首次执行时间
[main] (Timecost.java:39) TIME COST: [3.6805ms]<<<第二次执行时间
[main] (Timecost.java:41) TOTAL TIME COST: [220.0746ms]
[main] (Timecost.java:42) AVG TIME COST: [2.2007ms] for 100 times<<<100次测试平均值
[main] (Timecost.java:36) ==fastjson 词法分析器(JSONScanner)字段名查找(seekObjectToField)测试
[main] (Timecost.java:39) TIME COST: [5.1285ms]<<<首次执行时间
[main] (Timecost.java:39) TIME COST: [2.7439ms]<<<第二次执行时间
[main] (Timecost.java:41) TOTAL TIME COST: [76.3522ms]
[main] (Timecost.java:42) AVG TIME COST: [0.7635ms] for 100 times<<<100次测试平均值
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.42 sec

不同的JSON数据得到的结果不一样,

上面这个测试数据中关注的tokenId字段在JSON的末尾,fastjson 词法分析器(JSONScanner)字段名查找(seekObjectToField)测试测试结果比fastjson解析[基准]快一倍。

如果关注的tokenId字段是JSON的第一个字段,fastjson 词法分析器(JSONScanner)字段名查找(seekObjectToField)测试测试结果比fastjson解析[基准]快两个数量级,因为它找到关注的字段后,就不需要再解析后面的数据。

总结

经此一役,深刻体会antlr4虽然好学好用,但其面向通用场景生成的JSON词法语法分析器,与手撸的针对场景优化过的JSON解析器在性能上差距还是挺大的.

完整代码

关于上面所有方案的实现代码参见码云仓库:

https://gitee.com/l0km/jsonvisitor

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在使用fastjson进行反序列化Java实体的枚举字段时,如果出现错误,可以检查以下几个方面: 1. 确认枚举类的枚举值是否能够正确映射。fastjson默认使用枚举值的名称进行序列化和反序列化,因此需要确保枚举值的名称和实际值之间的映射关系正确。 2. 确认实体类的枚举字段的类型是否正确。如果枚举字段的类型不是枚举类本身,而是其它类型(如String),则需要使用@JSONField注解来指定fastjson进行序列化和反序列化时应该使用的类型。例如: ``` public class Entity { @JSONField(deserializeUsing = ColorDeserializer.class) private Color color; // ... } ``` 在这个例子,ColorDeserializer是一个自定义的反序列化器,用于将枚举值的名称转换为枚举对象。在实体类使用@JSONField注解指定字段应该使用ColorDeserializer进行反序列化。 3. 如果仍然出现错误,可以尝试使用fastjson的自定义反序列化器来处理枚举字段。例如: ``` public class ColorDeserializer implements ObjectDeserializer { @Override public Color deserialze(DefaultJSONParser parser, Type type, Object fieldName) throws JSONException { String value = parser.getLexer().stringVal(); for (Color color : Color.values()) { if (color.name().equalsIgnoreCase(value)) { return color; } } throw new JSONException("Invalid color value: " + value); } @Override public int getFastMatchToken() { return JSONToken.LITERAL_STRING; } } ``` 然后,在进行反序列化时,可以将ColorDeserializer传递给ParserConfig对象: ``` ParserConfig.getGlobalInstance().putDeserializer(Color.class, new ColorDeserializer()); ``` 这样,fastjson就会使用ColorDeserializer来处理枚举字段的反序列化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值