ObjectMapper自定义反序列化器遇到的问题记录

文章讨论了一个项目中同时使用fastjson和jackson导致的问题,当JSONObject为空时希望转换为null。作者定义了一个自定义的JsonDeserializer,但在初始尝试中出现了其他字段丢失的问题。通过深入理解JsonParser和DeserializationContext,作者发现可以通过JsonParser的getCodec().readTree(p)获取完整的JsonNode,然后转换为字符串,从而正确处理空JSONObject。最后,作者反思需要更深入地学习和理解Jackson框架。

发现问题

因项目里面同时存在fastjson与jackson两种Json处理的框架,部分对象中包含了fastjson的JSONObject类,反/序列化对象时使用的是jackson,现因一些情况需要将JSONObject为{}(空对象)时变成null返回。

则定义了以下的方法:


public class MainTest {
    
    public static void main(String[] args) throws JsonProcessingException {
        JSONObject otherInfoForNull = null;
        JSONObject otherInfo = new JSONObject();
        
        TestDTO dto = TestDTO.builder().name("小明").age(12).sex("男").address("光明小区").birthday("2015-05-14").otherInfo(otherInfoForNull).build();
        
        String json = objectMapper().writeValueAsString(dto);
        
        TestDTO testDTO = objectMapper().readValue(json, TestDTO.class);
        System.out.println(testDTO);
    }


    static ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        //忽略空Bean转json的错误
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        //忽略 在json字符串中存在,但是在java对象中不存在对应属性的情况。防止错误
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //注册序列化提供能力的对象s
        SimpleModule simpleModule = new SimpleModule();
        // JsonObject
        simpleModule.addDeserializer(JSONObject.class, new JSONObjectDeserializer());
        mapper.registerModule(simpleModule);
        return mapper;
    }

}


/**
 * 反序列化JSONObject
 */
class JSONObjectDeserializer extends JsonDeserializer<JSONObject> {


    @Getter
    private final JSONObject defaultValue;

    public JSONObjectDeserializer() {
        this(null);
    }

    public JSONObjectDeserializer(JSONObject defaultValue) {
        this.defaultValue = defaultValue;
    }

    @Override
    public JSONObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getValueAsString();
        if (StringUtils.isBlank(value)) {
            return this.defaultValue;
        }
        JSONObject result = JSON.parseObject(value);
        return MapUtils.isEmpty(result) ? this.defaultValue : result;
    }
}



@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
class TestDTO {

    private String name;

    private Integer age;

    private String sex;

    private JSONObject otherInfo;

    private String address;

    private String birthday;
}

最开始的个人理解:

  • 定义一个该类型的反序列化器
  • jackson判断属性类型为JSONObject后会拿到该字段的一些上下文或字段内容来调用自定义的反序列化器

乍一看没有什么问题,来运行一下:

  • 当otherInfo的值设置为otherInfoForNull时,运行如下:
    TestDTO(name=小明, age=12, sex=男, otherInfo=null, address=光明小区, birthday=2015-05-14)
  • 当otherInfo的值设置为otherInfo时,运行如下:
    TestDTO(name=小明, age=12, sex=男, otherInfo=null, address=null, birthday=null)

此时发现addressbirthday的值都是空的,说明反序列化器写的有问题,这时候就需要从deserialize方法的入参着手排查下了。


问题探究

DeserializationContext

用于在反序列化过程中提供上下文信息和功能。为JsonDeserializer提供了访问和操作反序列化过程中的各种上下文环境的方法。

  • 可以获取配置信息,如日期格式、空值处理等配置
  • 可以创建新的Java对象实例,并将反序列化的数据填充到该对象中

这个对象不是重点,下面看一下JsonParser

JsonParser

Json解析器作用是从JSON字符串中提取出符合JSON格式要求的数据,并将其传递给后续的JsonProcessor或其他Json工具类进行进一步处理。可以编写自定义的JSON解析器,以满足不同的需求。自定义的过程可以通过实现JsonFactory中的createJsonParser方法来实现。个人感觉解析过程有点像编译原理的词法分析。

其中比较常用的方法:

  • 词单元相关
    • nextToken()
    • currentToken()
  • 值相关
    • getText()
    • getIntValue()
    • getShortValue()
    • getLongValue()
  • 字段名相关
    • getCurrentName()
    • nextFieldName()
public static void main(String[] args) throws IOException {
        JSONObject otherInfo = null;
        TestDTO dto = TestDTO.builder().name("小明").age(12).sex("男").address("光明小区").birthday("2015-05-14").otherInfo(otherInfo).build();
        String json = objectMapper().writeValueAsString(dto);

        JsonFactory jsonFactory = new JsonFactory();
        JsonParser parser = jsonFactory.createParser(json);
        while (!parser.isClosed()) {
            JsonToken jsonToken = parser.nextToken();
            if (null == jsonToken) {
                continue;
            }
            System.out.printf("currentName: %s \t type: %s \t value: %s\n", parser.getCurrentName(), jsonToken.name(), parser.getText());
        }

    }

输出:

    currentName: null 	 type: START_OBJECT 	 value: {
    currentName: name 	 type: FIELD_NAME 	 value: name
    currentName: name 	 type: VALUE_STRING 	 value: 小明
    currentName: age 	 type: FIELD_NAME 	 value: age
    currentName: age 	 type: VALUE_NUMBER_INT 	 value: 12
    currentName: sex 	 type: FIELD_NAME 	 value: sex
    currentName: sex 	 type: VALUE_STRING 	 value: 男
    currentName: otherInfo 	 type: FIELD_NAME 	 value: otherInfo
    currentName: otherInfo 	 type: VALUE_NULL 	 value: null
    currentName: address 	 type: FIELD_NAME 	 value: address
    currentName: address 	 type: VALUE_STRING 	 value: 光明小区
    currentName: birthday 	 type: FIELD_NAME 	 value: birthday
    currentName: birthday 	 type: VALUE_STRING 	 value: 2015-05-14
    currentName: null 	 type: END_OBJECT 	 value: }

可以看到Json被拆解了,需要通过Token判断类型来获取字段名以及值。可以看下简单的用法

public static void main(String[] args) throws IOException {
        JSONObject otherInfo = buildJSONObject();
        TestDTO dto = TestDTO.builder().name("小明").age(12).sex("男").address("光明小区").birthday("2015-05-14").otherInfo(otherInfo).build();
        String json = objectMapper().writeValueAsString(dto);

        JsonFactory jsonFactory = new JsonFactory();
        JsonParser parser = jsonFactory.createParser(json);

        print(parser, "", -1, false);
    }

    // 初始化数据
    public static JSONObject buildJSONObject() {
        JSONObject otherInfo = new JSONObject();
        // 年级
        List<GradeInfo> gradeInfoList = new ArrayList<>();
        gradeInfoList.add(GradeInfo.builder().name("一年级").teacherList(Arrays.asList(
                TeacherInfo.builder().name("张老师").type("数学").idCard(IdCard.builder().number("123456789").build()).build(),
                TeacherInfo.builder().name("王老师").type("语文").idCard(IdCard.builder().number("987654321").build()).build())
        ).build());
        gradeInfoList.add(GradeInfo.builder().name("三年级").teacherList(Collections.singletonList(
                TeacherInfo.builder().name("刘老师").type("英语").idCard(IdCard.builder().number("753951468").build()).build())
        ).build());
        otherInfo.put("gradeInfoList", gradeInfoList);

        otherInfo.put("idCard", IdCard.builder().number("0112244").build());
        return otherInfo;
    }

    public static void print(JsonParser parser, String name, int arrayIndex, boolean isArray) throws IOException {
        while (!parser.isClosed()) {
            JsonToken jsonToken = parser.nextToken();

            // 因为存在递归调用,需要给一个退出条件(token为空 || 对象结束 || 数组结束)时需要退出递归
            if (jsonToken == null || JsonToken.END_OBJECT.equals(jsonToken) || JsonToken.END_ARRAY.equals(jsonToken)) {
                return;
            }

            String currentName = name;
            if (JsonToken.FIELD_NAME.equals(jsonToken)) {
                parser.nextToken();
                currentName = buildName(name, arrayIndex, parser);
            }

            if (parser.isExpectedStartObjectToken()) {
                print(parser, currentName, isArray ? arrayIndex++ : -1, false);
                continue;
            }

            if (parser.isExpectedStartArrayToken()) {
                print(parser, currentName, 0, true);
                continue;
            }

            String value = parser.getValueAsString();
            System.out.printf("%s:%s\n", currentName, value);
        }
    }

    // 构建名称
    private static String buildName(String name, int arrayIndex, JsonParser parser) throws IOException {
        String currentName = parser.getCurrentName();
        if (StringUtils.isBlank(name)) {
            return currentName;
        }
        // 数组
        if (arrayIndex >= 0) {
            return name + "[" + arrayIndex + "] -> " + currentName;
        }
        // 对象
        return name + " -> " + currentName;
    }

输出:

name:小明
age:12
sex:男
otherInfo -> idCard -> number:0112244
otherInfo -> gradeInfoList[0] -> name:一年级
otherInfo -> gradeInfoList[0] -> teacherList[0] -> name:张老师
otherInfo -> gradeInfoList[0] -> teacherList[0] -> type:数学
otherInfo -> gradeInfoList[0] -> teacherList[0] -> idCard -> number:123456789
otherInfo -> gradeInfoList[0] -> teacherList[1] -> name:王老师
otherInfo -> gradeInfoList[0] -> teacherList[1] -> type:语文
otherInfo -> gradeInfoList[0] -> teacherList[1] -> idCard -> number:987654321
otherInfo -> gradeInfoList[1] -> name:三年级
otherInfo -> gradeInfoList[1] -> teacherList[0] -> name:刘老师
otherInfo -> gradeInfoList[1] -> teacherList[0] -> type:英语
otherInfo -> gradeInfoList[1] -> teacherList[0] -> idCard -> number:753951468
address:光明小区
birthday:2015-05-14

问题解决

这样看下来,仅仅使用JsonParsergetValueAsString()方法是没有办法获取到当前完整json的,但也不用像上面那样复杂的去解析,jackson有另一种方法可以直接拿到完整json。

static class JSONObjectDeserializer extends JsonDeserializer<JSONObject> {


    @Getter
    private final JSONObject defaultValue;

    public JSONObjectDeserializer() {
        this(null);
    }

    public JSONObjectDeserializer(JSONObject defaultValue) {
        this.defaultValue = defaultValue;
    }

    @Override
    public JSONObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // 重点拿到json树转为string!!
        JsonNode treeNode = p.getCodec().readTree(p);
        String value = treeNode.toString();
        if (StringUtils.isBlank(value)) {
            return this.defaultValue;
        }
        JSONObject result = JSON.parseObject(value);
        return MapUtils.isEmpty(result) ? this.defaultValue : result;
    }
}

将反序列化方法改为上面的即可解决问题。

反思

出现这种问题主要是自己对jackson框架了解不深,该框架功能非常强大,不能停留在简单的使用,需要进一步提高自身的熟练程度。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值