Java - JsonProperty中首字母大小写对JSON反序列化的影响问题解决思路(不用Fastjson)
前言
我先来说下我这遇到的问题背景:
- 我们平时调用的接口,可能来自于一个
Java
服务端,也可能来自于一个NodeJs
服务端。 - 如今我们有个小需求:希望在
Java
服务端中调用NodeJs
服务端的接口去做一层代理。 - 主要操作:
Java
里面将Request
封装成普通的JSON
串给NodeJs
,由NodeJs
去做真实的请求。最终得到一个ResponseFromNode
,然后我们再将ResponseFromNode
进行JSON
化,返回给Java
服务端去反序列化。
这时候遇到问题了:
- 由于公司框架的某些原因。
ResponseFromNode
里面的字段可能都以大写开头或者以小写开头。而Java
里面的ResponseFromJava
,每个字段一般都有@JsonProperty()
进行标记。 - 那么这种就可能导致由于两端大小写不一致的情况,从而导致在
Java
中,JSON
反序列化的结果为null
。
一. 简单的案例复现
我们这里给一个很简单的案例来说明这个问题。我们准备一个很简单的Pojo
类:lombok
和jackson
的依赖我就不贴了。
@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
)?
我的思路如下:
Java
端将需要反序列化对象的各个字段的大小写映射关系给到NodeJs
服务。NodeJs
服务进行真实请求后,将拿到的ResponseFromNode
对象根据映射关系进行转换。最终再输出为JSON
返回Java
。- 这样
Java
就能正确地进行反序列化了。
备注:如果觉得太麻烦或者没必要,直接用fastjson
就能解决各种大小写和兼容问题(例如Calendar
类型),就不必往下看啦。
二. Java端输出对应的字段映射关系
我们准备写一个FieldMappingUtil
映射工具类,它的功能主要分为这么几个:
- 递归处理当前类的每一个字段。拿到对应
JsonProperty
标记的值作为映射值。字段名则作为原始值。 - 能判断当前字段类型是否需要递归?例如八大基本数据类型就不需要判断。我们一般针对的都是自己封装的对象。
代码如下:
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
定义了。这样一来,反序列化问题也就解决了。
最后再提一嘴:
- 如果大家遇到类似的情况,有更好的解决方案欢迎讨论交流。
- 如果使用
Fastjson
,其实这些情况都不需要考虑的。默认下这个框架无视首字母大小写的。 - 其实这种通过原始
JSON
来传递请求体和返回体,再进行序列化/反序列化的问题还是很多的。我这里举个例子,如果某个类型是Calendar
,但是NodeJs
端返回给Java
的JSON
对应字段的值是:/Date(1680844034150+0800)/
,那么Jackson
如果你不去设置一些东西,反序列化的时候会报错的。我这里暂时是在Node
端进行格式化处理。
同一个问题的解决方案有很多,就看大家怎么选择啦。这里就当分享一下反射和递归在工作中的实际应用了~