Java - JsonProperty中首字母大小写对JSON反序列化的影响问题解决思路(不用Fastjson)

文章讲述了在Java服务端调用Node.js接口时,由于字段首字母大小写不一致导致的反序列化问题。作者提出了一种解决方案,即Java端生成字段映射关系传递给Node.js,Node.js根据映射关系转换返回对象的大小写,以确保与Java对象匹配,从而成功反序列化。此外,文中还提及了Fastjson作为替代方案可以自动忽略大小写。
摘要由CSDN通过智能技术生成

Java - JsonProperty中首字母大小写对JSON反序列化的影响问题解决思路(不用Fastjson)

前言

我先来说下我这遇到的问题背景:

  1. 我们平时调用的接口,可能来自于一个Java服务端,也可能来自于一个NodeJs服务端。
  2. 如今我们有个小需求:希望在Java服务端中调用NodeJs服务端的接口去做一层代理。
  3. 主要操作:Java里面将Request封装成普通的JSON串给NodeJs,由NodeJs去做真实的请求。最终得到一个ResponseFromNode,然后我们再将ResponseFromNode进行JSON化,返回给Java服务端去反序列化。

这时候遇到问题了:

  1. 由于公司框架的某些原因。ResponseFromNode里面的字段可能都以大写开头或者以小写开头。而Java里面的ResponseFromJava,每个字段一般都有@JsonProperty()进行标记。
  2. 那么这种就可能导致由于两端大小写不一致的情况,从而导致在Java中,JSON反序列化的结果为null

一. 简单的案例复现

我们这里给一个很简单的案例来说明这个问题。我们准备一个很简单的Pojo类:lombokjackson的依赖我就不贴了。

@Data
@ToString
public class Model {
    @JsonProperty("Name")
    public String name;
    @JsonProperty("Age")
    public Integer age;
    @JsonProperty("BookList")
    public List<Book> books;
}

@Data
public class Book {
    @JsonProperty("BookName")
    public String bookName;
}

写一个简单的Demo:这里主要用的是jackson的序列化。具体内容我就不写了,是内部封装的代码。

@org.junit.Test
public void test2() {
    Model model = new Model();
    model.setAge(1);
    model.setName("LJJ");

    ArrayList<Book> books = new ArrayList<>();
    Book book = new Book();
    book.setBookName("Hello");
    books.add(book);

    model.setBooks(books);
    System.out.println(JsonUtil.toJson(model));
}

拿到对应的JSON串:

{"Name":"LJJ","Age":1,"BookList":[{"BookName":"Hello"}]}

这个JSON串如果反序列化,是能够成功的。但是我们模拟一下NodeJs服务端请求返回,返回给Java一个JSON串如下:

{"name":"LJJ","Age":1,"books":[{"bookName":"Hello"}]}

那么这个JSON串反序列化肯定是失败的:

@org.junit.Test
public void test3() {
    String json = "{\"name\":\"LJJ\",\"Age\":1,\"books\":[{\"bookName\":\"Hello\"}]}";
    Model model = JsonUtil.fromJson(Model.class, json);
    System.out.println(model);
}

如图:
在这里插入图片描述

那么这种情况改怎么保证NodeJs端返回的JSON串符合Java端对象属性定义的格式呢(@JsonProperty)?

我的思路如下:

  1. Java端将需要反序列化对象的各个字段的大小写映射关系给到NodeJs服务。
  2. NodeJs服务进行真实请求后,将拿到的ResponseFromNode对象根据映射关系进行转换。最终再输出为JSON返回Java
  3. 这样Java就能正确地进行反序列化了。

备注:如果觉得太麻烦或者没必要,直接用fastjson就能解决各种大小写和兼容问题(例如Calendar类型),就不必往下看啦。

二. Java端输出对应的字段映射关系

我们准备写一个FieldMappingUtil映射工具类,它的功能主要分为这么几个:

  1. 递归处理当前类的每一个字段。拿到对应JsonProperty标记的值作为映射值。字段名则作为原始值。
  2. 能判断当前字段类型是否需要递归?例如八大基本数据类型就不需要判断。我们一般针对的都是自己封装的对象。

代码如下:

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.ClassUtils;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

/**
 * @Date 2023/4/7 14:56
 * @Created by jj.lin
 */
public class FieldMappingUtil {
    /**
     * 除去 8大基本数据类型 及 对应包装类 的其他支持对象类型
     */
    private static final String[] NORMAL_TYPE = new String[]{
            "String", "BigDecimal", "Calendar",
    };
    /**
     * 需要排除校验的对象类型
     */
    private static final String[] EXCLUDE_TYPE = new String[]{
            
    };
    /**
     * 需要排除的字段名称
     */
    private static final String[] EXCLUDE_FIELD = new String[]{
            
    };

    private static final Set<String> NORMAL_TYPE_SET = new HashSet<String>(Arrays.asList(NORMAL_TYPE));
    private static final Set<String> EXCLUDE_TYPE_SET = new HashSet<String>(Arrays.asList(EXCLUDE_TYPE));
    private static final Set<String> EXCLUDE_FIELD_SET = new HashSet<String>(Arrays.asList(EXCLUDE_FIELD));

    public static <T> HashMap<String, String> getMapping(Class<T> tClass) {
        HashMap<String, String> fieldMapping = new HashMap<>();
        getMapping(fieldMapping, tClass);
        return fieldMapping;
    }

    private static <T> boolean isNormalType(Class<T> tClass) {
    	// 8 大基本数据类型和对应包装类的判断
        if (ClassUtils.isPrimitiveOrWrapper(tClass)) {
            return true;
        }
        // 其他类型的判断,这里就需要我们自定义了
        if (FieldMappingUtil.contain(tClass, NORMAL_TYPE_SET)) {
            return true;
        }
        return false;
    }

    private static <T> boolean contain(Class<T> tClass, Set<String> set) {
        return set.contains(tClass.getName()) || set.contains(tClass.getSimpleName());
    }

    private static <T> void getMapping(HashMap<String, String> fieldMapping, Class<T> tClass) {
        for (Field field : tClass.getDeclaredFields()) {
        	// 如果是我们需要排除映射的类型,就跳过
            if (FieldMappingUtil.contain(tClass, EXCLUDE_TYPE_SET)) {
                continue;
            }
            String originName = field.getName();
            // 如果是我们不需要序列化的一些属性,跳过,例如序列化ID、静态成员变量
            if (EXCLUDE_FIELD_SET.contains(originName)) {
                continue;
            }
            Class<?> type = field.getType();
            // 如果是集合
            if (type == java.util.List.class) {
                // 取出对应的泛型类型
                Type genericType = field.getGenericType();
                if (genericType instanceof ParameterizedType) {
                    ParameterizedType pt = (ParameterizedType) genericType;
                    // 得到泛型里的class类型对象
                    Class<?> genericClazz = (Class<?>) pt.getActualTypeArguments()[0];
                    // 继续递归
                    getMapping(fieldMapping, genericClazz);
                }
                // 加入当前集合名称的映射关系
                JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
                if (jsonProperty != null) {
                    fieldMapping.putIfAbsent(originName, jsonProperty.value());
                }
                continue;
            }
            // 如果不是常规对象类型,继续递归处理
            if (!FieldMappingUtil.isNormalType(type)) {
                getMapping(fieldMapping, type);
                continue;
            }
            // 增加当前的字段映射关系
            JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
            if (jsonProperty != null) {
                fieldMapping.putIfAbsent(originName, jsonProperty.value());
            }
        }
    }
}

我们来测试一下这个工具:

@org.junit.Test
public void test4() {
    HashMap<String, String> mapping = FieldMappingUtil.getMapping(Model.class);
    System.out.println(JsonUtil.toJson(mapping));
}

结果如下:
在这里插入图片描述

那么我们将这个映射Mapping关系,传给NodeJs,让NodeJs在返回真实Response之前,做一次转换即可。

三. Node端进行大小写映射转换

这里我同样进行Mock,我们看下一般的返回是怎样的:

const responseFromNode = {
    name: "LJJ",
    Age: 1,
    BookList: [
        { bookName: "Hello" }
    ]
}
// 这里就是要返回给Java的原始报文
console.log(responseFromNode)

那么现在,我们就要根据Mapping映射关系去做一次映射:

const responseFromNode = {
    name: "LJJ",
    Age: 1,
    books: [
        { bookName: "Hello" }
    ]
}
const fieldMapping = { "books": "BookList", "name": "Name", "bookName": "BookName", "age": "Age" };

const transMapping = (jsonObj, mapping) => {
    // 数组则递归每一项
    if (jsonObj instanceof Array) {
        return jsonObj.map(item => transMapping(item));
    } else if (typeof (jsonObj) === 'object') {
        for (const key in jsonObj) {
            // 根据映射关系拿到新的Key
            const mappingNewKey = fieldMapping[key];
            if (!mappingNewKey) {
                continue;
            }
            // 递归处理该Key对应的Value
            jsonObj[mappingNewKey] = transMapping(jsonObj[key]);
            // 记得删掉老的 K-V
            if (mappingNewKey !== key) {
                delete jsonObj[key];
            }
        }
        return jsonObj;
    }
    return jsonObj;
};


console.log('old:', responseFromNode)
console.log('new:', transMapping(responseFromNode, fieldMapping))

结果如下:
在这里插入图片描述
可以看到,Node端最后输出的对象属性大小写,已经完全吻合Java对象中的JSON定义了。这样一来,反序列化问题也就解决了。

最后再提一嘴:

  1. 如果大家遇到类似的情况,有更好的解决方案欢迎讨论交流。
  2. 如果使用Fastjson,其实这些情况都不需要考虑的。默认下这个框架无视首字母大小写的。
  3. 其实这种通过原始JSON来传递请求体和返回体,再进行序列化/反序列化的问题还是很多的。我这里举个例子,如果某个类型是Calendar,但是NodeJs端返回给JavaJSON对应字段的值是:/Date(1680844034150+0800)/,那么Jackson如果你不去设置一些东西,反序列化的时候会报错的。我这里暂时是在Node端进行格式化处理。

同一个问题的解决方案有很多,就看大家怎么选择啦。这里就当分享一下反射和递归在工作中的实际应用了~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zong_0915

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

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

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

打赏作者

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

抵扣说明:

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

余额充值