java:fastjson,jackson自定义反序列化器设计暨jackson反序列化时出现StackOverflowError异常的原因分析

news2024/9/25 9:40:48

问题描述

如下是一个Java Bean类,这是我的sql2java工具根据数据库表自动生成的对应表记录的Java类。
在这里插入图片描述
与之对应的数据库表定义如下:

CREATE TABLE IF NOT EXISTS dc_device_channel (
    `device_id`     int         NOT NULL COMMENT 'X@NAME:设备ID@X',
    `sid`           int         NOT NULL DEFAULT 0 COMMENT 'X@NAME:物理屏幕ID@x',
    `area`          varchar(32) NOT NULL COMMENT 'X@NAME:显示区域ID@x',
    `rect`          varchar(64) DEFAULT NULL COMMENT 'X@NAME:显示区域坐标@x,对应EamPlayer的defineChannel语法TYPE@net.facelib.eam.interpreter.Rectangle@EPYT',
    `channel`       varchar(32) NOT NULL COMMENT 'X@NAME:频道ID@x,显示区域对应的频道',
    `run_tasks`     text        DEFAULT NULL COMMENT 'X@NAME:播放任务@X描述,设备实际播放的任务描述,由设备端写入,对应EamPlayer的definePlanTask语法',
    PRIMARY KEY(`device_id`,`sid`,`area`),
    FOREIGN KEY (device_id)  REFERENCES dc_device(id) ON DELETE CASCADE,
    INDEX (channel)
)COMMENT 'X@NAME:设备显示区域频道记录@X' DEFAULT CHARSET=utf8;

这是一个基本标准的Java Bean,每个数据库字段对应的Java成员都有getter/setter方法。不论是jackson,还是fastjson将序列化为JSON字符串时都没有任何问题。
但是仔细看上面的截图可以发现,在setter方法中会修改initialzied,modified字段,(这是两个额外字段,以bit形式记录该字段是否被修改过)。尤其这个modified字段用于在调用JDBC方法保存记录时,决定是否保存该字段。
也就是说每次调用setter方法都会影响modified,initialized字段的值。
那么可以想见当不做任何特别设置,对JSON字段串进行反序列化时,得到的initialzied,modified字段的值是不确定的。取决于这两个字段被反序列化时的顺序,如果它们被在放在最后反序列化,那它们的值是正确的,否则它们的值与原始输入值肯定是不一致的。

另外如果输入的JSON字符串中字段名是snake-case的数据库字段名(如run_tasks),反序列化时应该能自动识别为camel-case的Java字段名(如runTasks)。这是JSON工具默认反序列化做不到的。

所以为了解决这些问题,只能为它们自定义反序列化实现。

fastjson 反序列化

fastjson反序列化器实现很简单,实现的时候没有遇到太多麻烦,
基本的逻辑就是继承com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer重写deserialze(DefaultJSONParser parser, Type type, Object fieldName)方法,先将输入内容反序列化为JSONObject(也就是Map<String, Object>), 然后识别snake-case的数据库字段名转为camel-case的JavaBean字段名。然后调用JavaBeanDeserializer.createInstance方法对JSONObject逐字段反序列化返回对应的Java Bean对象,并最后调用setter方法赋值modified,initialized字段在确保它们的值与原始输入值一致。

代码如下:

public class FastjsonDeserializer extends JavaBeanDeserializer implements Constant {
	public FastjsonDeserializer(ParserConfig config, Class<? extends BaseBean> beanClass) {
		super(config, checkNotNull(beanClass,"beanClass is null"));
	}
	public FastjsonDeserializer(Class<? extends BaseBean> beanClass) {
		this(ParserConfig.global, beanClass);
	}
	@SuppressWarnings("unchecked")
	@Override
	public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
		/** deserialze to JSONObject */
		JSONObject json = (JSONObject)super.deserialze(parser, JSONObject.class, fieldName);
		// replace key to camel-case if snake-case
		for(String key:Lists.newArrayList(json.keySet()) ) {
			if(isSnakecase(key)) {
				String camelcase = toCamelcase(key);
				if(!json.containsKey(camelcase)) {
					json.put(camelcase, json.get(key));
					json.remove(key);
				}
			}
		}
		/** convert Map to target type based BaseBean  */
		try {
			BaseBean bean = (BaseBean) createInstance(json, parser.getConfig());
			bean.setNew(firstNonNull(json.getBoolean(FIELD_NEW), true));
			Integer modified = json.getInteger(FIELD_MODIFIED);
			Integer initialized = json.getInteger(FIELD_INITIALIZED);
			if(null != initialized){
				bean.setInitialized(initialized);
			}
			if(null != modified){
				bean.setModified(modified);
			}
			return (T) bean;
		} catch (Exception e) {
			throw new JSONException(e.getMessage(), e);
		}
	}
}

有了这个类,实现一个为上面DeviceChannelBean定制的fastjson反序列化类就很简单:

    public static class DeviceChannelBeanFastjsonDeserializer extends FastjsonDeserializer{
        public DeviceChannelBeanFastjsonDeserializer() {
            super(DeviceChannelBean.class);
        }
    }

然后我们就可以如下使用@JSONType注解将反序列化器定义在类上:

@JSONType(deserializer=DeviceChannelBeanFastjsonDeserializer.class)
public final class DeviceChannelBean extends BaseRow
    implements Serializable,Constant
{
......
}

这样不论在什么环境 DeviceChannelBean 都可以被faskjson正确反序列化了

jackson反序列化实现(一)

这是我最开始设计的jackson反序列化实现,
基本的逻辑就是继承com.fasterxml.jackson.databind.deser.std.StdDeserializer重写deserialize(JsonParser jp, DeserializationContext ctxt)方法,先将输入内容反序列化为JsonNode ,然后调用ObjectMapper.treeToValue方法将JsonNode 转为指定类的对象,并在最后调用setter方法赋值modified,initialized字段确保它们的值与原始输入值一致。

注意这个实现中没有考虑snake-case的问题。

public class JacksonDeserializer<B extends BaseBean> extends StdDeserializer<B>implements Constant  {
	private static final long serialVersionUID = 7410414787512241455L;
	protected JacksonDeserializer(Class<B> beanClass) {
		super(beanClass);
	}

	@SuppressWarnings("unchecked")
	@Override
	public B deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode node = mapper.readTree(jp);
        B bean = (B) mapper.treeToValue(node,_valueClass);
		bean.setNew(firstNonNull(getBoolean(node,FIELD_NEW), true));
		Integer modified = getInteger(node,FIELD_MODIFIED);
		Integer initialized = getInteger(node,FIELD_INITIALIZED);
		if(null != initialized){
			bean.setInitialized(initialized);
		}
		if(null != modified){
			bean.setModified(modified);
		}
		return bean;
	}
	
	private Boolean getBoolean(JsonNode node,String name) {
		JsonNode vnode = node.get(name);
		if(null != vnode && !vnode.isNull()) {
			return vnode.asBoolean();
		}
		return null;
	}
	private Integer getInteger(JsonNode node,String name) {
		JsonNode vnode = node.get(name);
		if(null != vnode && !vnode.isNull()) {
			return vnode.asInt();
		}
		return null;
	}
}

设计很简单,一切看起来都很美好,
但是这个反序列化器是不能正常工作的,用于反序列化时就会报错:StackOverflowError,很严重的堆栈溢出错误。

为啥呢?
问题出在ObjectMapper.treeToValue方法,仔细研究ObjectMapper的源码发现,treeToValue方法是调用readValue(JsonParser p, Class<T> valueType),而readValue方法最终是调用 _findRootDeserializer(DeserializationContext ctxt,JavaType valueType)来根据treeToValue方法提供的valueType参数(目标类型)获取反序列化实例。那么它返回的还是当前JacksonDeserializer实例,也就是自己。所以就造成了无限递归。所以这里我们不能简单的调用treeToValue(TreeNode n, Class<T> valueType)方法来完成字段反序列化。必须在deserialize(JsonParser jp, DeserializationContext ctxt)实现Java Bean的字段解析。才能避免递归调用问题。

jackson反序列化实现(二)

下面的代码是我实现的第二版的jackson反序列化器。它以com.fasterxml.jackson.databind.deser.BeanDeserializer为基类,BeanDeserializer是Jackson默认的Java Bean反序列化实现。
在下面的deserialize(JsonParser jp, DeserializationContext ctxt)方法中参照BeanDeserializer.vanillaDeserialize方法实现字段解析,在字段解析过程中自动识别snake-case的数据库字段名转为camel-case的JavaBean字段名(参见findProperty方法),并在最后调用setter方法赋值modified,initialized字段确保它们的值与原始输入值一致。

public class JacksonDeserializer extends BeanDeserializer implements Constant  {
	private static final long serialVersionUID = 7410414787512241455L;
	public JacksonDeserializer(Class<? extends BaseBean> beanClass) {
		super(createBeanDeserializer(beanClass));
	}

	@Override
	public BaseBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
		// see also BeanDeserializer.vanillaDeserialize
		BaseBean bean = (BaseBean) _valueInstantiator.createUsingDefault(ctxt);
		Boolean _new = null;
		Integer initialized = null,modified = null;
		
        for(String propName = jp.nextFieldName();propName != null;propName = jp.nextFieldName()) {
        	jp.nextToken();
        	SettableBeanProperty prop = findProperty(this,propName);
        	if (prop != null) { // normal case
        		try {
        			switch(propName) {
        			case FIELD_NEW:
        				_new = (Boolean) prop.deserialize(jp,ctxt);
        				break;
        			case FIELD_INITIALIZED:
        				initialized = (Integer) prop.deserialize(jp,ctxt);
        				break;
        			case FIELD_MODIFIED:
        				modified = (Integer) prop.deserialize(jp,ctxt);
        				break;
        			default:
        				prop.deserializeAndSet(jp, ctxt, bean);
        			}
        		} catch (Exception e) {
        			wrapAndThrow(e, bean, propName, ctxt);
        		}
        		continue;
        	}
            handleUnknownVanilla(jp, ctxt, bean, propName);
        }
        if(null != _new) {
        	bean.setNew(_new);
        }
		if(null != initialized){
			bean.setInitialized(initialized);
		}
		if(null != modified){
			bean.setModified(modified);
		}
		return bean;
	}
	private SettableBeanProperty findProperty(BeanDeserializer beanDeserializer,String propName) {
		SettableBeanProperty prop = beanDeserializer.findProperty(propName);
		if(null == prop && isSnakecase(propName)) {
			prop = beanDeserializer.findProperty(toCamelcase(propName));
		}
		return prop;
	}
	/**
	 * 创建{@code beanClass}对应的{@link BeanDeserializerBase}实例用于父类构造方法的参数,
	 * 将{@code beanClass}的序列化参数注入到当前实例中
	 * @param beanClass
	 */
	private static BeanDeserializerBase createBeanDeserializer(Class<?> beanClass){
		try {
			ObjectMapper mapper = new ObjectMapper();
			DefaultDeserializationContext defctx = (DefaultDeserializationContext) mapper.getDeserializationContext();
			DefaultDeserializationContext ctxt = defctx.createInstance(mapper.getDeserializationConfig(),	null,null);
			JavaType type = ctxt.constructType(beanClass);
			BasicBeanDescription beanDesc = (BasicBeanDescription)ctxt.getConfig().introspect(type);
			BeanDeserializerFactory factory = (BeanDeserializerFactory) ctxt.getFactory();
			BeanDeserializer beanDeserializer = (BeanDeserializer) factory.buildBeanDeserializer(ctxt, type, beanDesc);
			beanDeserializer.resolve(ctxt);
			return beanDeserializer;
		} catch (IOException e) {
			throw new ExceptionInInitializerError(e);
		}
	}
}

有了这个jackson反序列化实现类,实现一个为上面DeviceChannelBean定制的jackson反序列化类就很简单:

    public static class DeviceChannelBeanJacksonDeserializer extends JacksonDeserializer{
        private static final long serialVersionUID = 359505789687575302L;
        public DeviceChannelBeanJacksonDeserializer() {
            super(DeviceChannelBean.class);
        }
    }

然后我们就可以如下使用@JsonDeserialize注解将反序列化器定义在类上:

@JsonDeserialize(using=DeviceChannelBeanJacksonDeserializer.class)
public final class DeviceChannelBean extends BaseRow
    implements Serializable,Constant
{
......
}

这样不论在什么环境 DeviceChannelBean 都可以被jackson正确反序列化了

createBeanDeserializer

这里最难实现并不是deserialize(JsonParser jp, DeserializationContext ctxt)中的逐字段反序列化。而是构造方法。BeanDeserializer最简单的构造方法的参数也是一个BeanDeserializerBase实例,这个实例包含了一个JavaBean类所有的反序列化配置,没有以Class<?>为参数的构造方法,jackson源码中也没有找到直接的方法或简单示例根据一个Java Bean类型创建一个BeanDeserializer实例。
所以我参照com.fasterxml.jackson.databind.deser.DeserializerCache.(DeserializationContext ctxt,DeserializerFactory factory, JavaType type)com.fasterxml.jackson.databind.deser.DeserializerFactory.createBeanDeserializer(DeserializationContext ctxt, JavaType type, BeanDescription beanDesc)方法结合实际测试,设计了上面的静态方法createBeanDeserializer(Class<?> beanClass)实现从一个Java Bean类型创建一个BeanDeserializerBase实例。用于为JavaBeanDeserializer构造方法提供参数。

完整代码

以上FastjsonDeserializer,JacksonDeserializer的完整代码参见我的码云仓库:
https://gitee.com/l0km/sql2java/blob/master/sql2java-base/src/main/java/gu/sql2java/json/FastjsonDeserializer.java
https://gitee.com/l0km/sql2java/blob/master/sql2java-base/src/main/java/gu/sql2java/json/JacksonDeserializer.java

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/487990.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一种具备过载检测和打嗝式保护功能的MBUS主机电路

关于MBUS电路原理和简化电路见本篇文章&#xff0c; MBUS主机端简化版电路设计_Tech-Wang的博客-CSDN博客https://blog.csdn.net/jsf120/article/details/123378064?spm1001.2014.3001.5502 上篇中的电路已经验证通过并在产品中实际应用&#xff0c;本篇在此基础上增加了打嗝…

itop-3568开发板驱动学习笔记(25)设备树(四)GPIO 实例分析

《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记 文章目录 GPIO 控制器必要属性其他属性 指定 GPIO 引脚 和时钟类似&#xff0c;GPIO 在设备树中也存在两层定义&#xff0c;首先是 GPIO 控制器&#xff0c;这部分由芯片原厂工程师编写&#xff0c;相当于 GPIO 底层…

恺撒密码太简单?这样一改,秒变地狱级难度(37)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 欢迎和猫妹一起&#xff0c;趣味学Python。 今日主题 看过上一篇文章的朋友&#xff0c;可能觉得这样加密&#xff0c;未免太简单了吧&#xff1f; 一共26个字母&#xf…

【java】一文让你了解透彻Java中的IO模型

文章目录 前言一、什么是IO二、常见的IO模型BIO&#xff08;Blocking IO&#xff09;如何优化NIO的面世 三、NIO&#xff08;Non-blocking/New IO&#xff09;同步非阻塞IO模型IO多路复用模型Java中的NIO 四、AIO&#xff08;Asynchronous IO&#xff09;总结 前言 本文只是说…

不要一昧的给孩子做骨密度检查,这些情况才要做

国内&#xff0c;骨密度检测应用非常广泛。也成了很多医院儿科常规的体检项目之一。而在儿科领域&#xff0c;对骨密度检查的必要性、临床参考意义一直颇有争议。 有专家说&#xff0c;“孩子需要及时检测骨密度&#xff0c;但是要用科学有效的检测方式……只有双能X线的检测方…

BetaFlight统一硬件配置文件研读

BetaFlight统一硬件配置文件研读 1. 源由2. 分析2.1 硬件SOC2.2 统一配置文件2.3 cli命令2.4 板级配置主要命令2.4.1 board_name2.4.2 manufacturer_id2.4.3 resource2.4.4 timer2.4.5 dma2.4.6 serial2.4.7 set2.4.8 feature 3. 实例研读3.1 C遗留配置3.2 BoardName - AOCODAR…

asp.net+C#基于web的失物招领系统ccA1A9程序

根据分析&#xff0c;该系统应该有二个角色&#xff1a;管理员、注册用户。系统需要赋予他们对应的功能和权限&#xff0c;具体如下&#xff1a; 管理员具有&#xff1a;管理管理注册用户、管理新闻模块、管理留言的功能和权限。 具有&#xff1a;浏览新闻、管理失物信息、参与…

【Linux内幕】DMA原理

1.前言 I/O设备与主存信息传送的控制方式分为程序轮询、中断、DMA、RDMA等。 2.DMA介绍 DMA&#xff0c;全称Direct Memory Access&#xff0c;即直接存储器访问。 DMA传输将数据从一个地址空间复制到另一个地址空间&#xff0c;提供在外设和存储器之间或者存储器和存储器之…

GAT1400-视图库标准

一、简述&#xff1a; GA/T 1400是于2017年首次发布关于图片、视频片段、文件等属性对象的传输协议。 GA/T 1400《公安视频图像信息应用系统》分成4个部分&#xff1a; 第一部分–通用技术要求&#xff1b; 第二部分–应用平台技术要求&#xff1b; 第三部分–数据库技术要…

前端学习之使用JavaScript

前情回顾&#xff1a;网页布局 JavaScript 简介 avaScript诞生于1995年&#xff0c;它的出现主要是用于处理网页中的前端验证。所谓的前端验证&#xff0c;就是指检查用户输入的内容是否符合一定的规则。比如&#xff1a;用户名的长度&#xff0c;密码的长度&#xff0c;邮箱的…

刘强:作业帮给OceanBase提了九条意见

3 月 25 日&#xff0c;第一届 OceanBase 开发者大会在北京举行&#xff0c; 作业帮数据库架构师刘强为大家带来了《作业帮基于 OceanBase 的 HTAP 实践》的分享 &#xff0c;为大家介绍了 OceanBase 上线作业帮半年来的体验与心得。 以下内容由大会演讲整理而成&#xff1a; …

提示工程玩转 ChatGPT

Prompt engineering is the skill of the future. 目录&#xff1a; 1. 简介 (Introduction) 2. 提示指南 (Prompt Guidelines) 2.1 指令要清晰明确 2.2 给模型时间思考 3. 迭代提示 (Iterative Prompt) 3.1 迭代过程 3.2 案例展示 4. 文本概括 (Text Summarization) 4.1 单文本…

五个有用的工具帮助您提高代码质量

前言 对于开发人员而言&#xff0c;代码质量一直是一个非常重要的话题。高质量的代码不仅可以提高应用程序的性能&#xff0c;还可以减少代码错误和维护成本。然而&#xff0c;如何确保代码质量呢&#xff1f;下面介绍五个有用的工具&#xff0c;可以帮助您提高代码质量。 So…

SLAM论文速递:SLAM—— NICER-SLAM: RGB SLAM的神经隐式场景编码—5.04(1)

论文信息 题目&#xff1a; NICER-SLAM:Neural Implicit Scene Encoding for RGB SLAM NICER-SLAM: RGB SLAM的神经隐式场景编码论文地址&#xff1a; https://arxiv.org/pdf/2302.03594.pdf发表期刊&#xff1a; Computer Vision and Pattern Recognition (cs.CV)标签 xxxx…

机器学习:协同过滤推荐算法

目录标题 题目&#xff1a;使用协同过滤&#xff08;基于用户&#xff09;构建简单的电影推荐系统1.1.1 实验目的1.1.2 实验内容及步骤1.1.3 程序运行过程、方法和运行结果1.1.4 实验小结 题目&#xff1a;使用协同过滤&#xff08;基于用户&#xff09;构建简单的电影推荐系统…

详解Mybatis之参数传递问题

编译软件&#xff1a;IntelliJ IDEA 2019.2.4 x64 操作系统&#xff1a;win10 x64 位 家庭版 Maven版本&#xff1a;apache-maven-3.6.3 Mybatis版本&#xff1a;3.5.6 目录 一. Mybatis中参数传递问题1.1 单个普通参数传递1.2 多个普通参数传递1.3 命名参数1.4 POJO(java Bean…

MySQL知识学习02(MySQL索引详解)

1、索引介绍&#xff1f; 索引是一种用于快速查询和检索数据的数据结构&#xff0c;其本质可以看成是一种排序好的数据结构。 索引的作用就相当于书的目录。打个比方: 我们在查字典的时候&#xff0c;如果没有目录&#xff0c;那我们就只能一页一页的去找我们需要查的那个字&…

鸿蒙Hi3861学习四-Huawei LiteOS介绍

一、什么是LitesOS Huawei LiteOS是华为针对物联网领域推出的轻量级物联网操作系统&#xff0c;是华为物联网战略的重要组成部分&#xff0c;具备轻量级、低功耗、互联互通、组件丰富、快速开发等关键能力。基于物联网领域业务特征打造领域性技术栈&#xff0c;为开发者提供“一…

MEET开发者 | 从无代码小白到大神,95后精神小伙的职场初体验

「无代码开发者故事」第一期的嘉宾是一位初入职场的95后&#xff0c;他保留了白羊座直率、热情的孩子天性&#xff0c;即便长期驻场也能“苦中作乐”&#xff0c;发挥自己旅游和摄影的爱好。初入职场的他在项目中主要承担需求分析的角色&#xff0c;2021年初受公司委派学习和评…

VR全景展示--探索无限可能

随着科技的发展&#xff0c;虚拟现实&#xff08;Virtual Reality&#xff0c;简称VR&#xff09;技术逐渐成为人们关注的焦点。VR技术使得人们可以在虚拟现实中体验到与现实世界不同的感官体验。在VR全景展示中&#xff0c;观众可以通过虚拟现实技术&#xff0c;沉浸式地感受到…