一文读懂Json序列化与反序列化
一文读懂Json序列化与反序列化
通常我们在Web
开发中,都使用Json
来传输和交换数据。今天我们来揭开Json
神秘的面纱,看一看:
Json
的5w
(why
、what
、who
、when
、where
)Java
开发中,如何将Java
对象序列化成Json
和如何将Json
反序列化成Java
对象SpringBoot
开发Web项目时,Json
如何在Http
协议和SpringWeb
框架中发挥作用
最后,我们再补充一点在实际项目中可行的一些实践。
Json
的5w
-
Json
是什么? -> 一种可以表示kv
键值对、array
集合、value
三种结构的文本数据格式,key
是**String
**,value
可以是任意数据类型{ "k-str":"a string", "k-num":100, "key-null":null, "key-object":{ "id":1234, "name":"a object" }, "k-list":[ 1, 2, 3, 4, 5 ], "k-map":{ "kv-k1":"k1", "kv-k2":2, "kv-k3":null } }
-
为什么用
Json
? -> 结构简单,易于读写,便于传输 -
Json
用于哪些场景?数据传输交换
Json
序列化与反序列化
补充一个关于反序列化的一个有意思的小知识。反序列化产生一个Java对象,而在java中,对象的创建有2种方式:
- 通过构造器
new
一个clone
一个已存在的实例前者通过构造器创建,会默认执行构造器中定义的初始化逻辑,后者通过内存复制不会走初始化方法但需要实现
Cloneable
接口。那么Json
反序列化使用的是哪种方式呢?是第一种啦!所以不要在构造器中埋坑哦~
这里特指的是在Java
中,Java
对象序列化成Json
文本和与Json
文本反序列化成Java
对象。
上面我们已经看到,Json
其实就是一种kv
结构,如果把Java
对象的属性名作为k
,属性值作为v
,那Json
和我们的Java
对象结构是相似的。
而对于集合/数组和Map
类型呢?
Json
同样支持集合类型,对于Map
天然的就是kv
结构,直接将Map
对象的key
作为Json
的k
,将Map
的value
作为Json
的v
即可。这就是**Java
序列化成Json
**,反过来则是反序列化。
那当然对于Map
需要注意的一点是,Json
的k
只支持String
类型,而Map
的key
可以是任意对象,所以通常会使用toString()
将Map
的key
转成Json
的k
。
另外,对于Jdk
自带的如Jsr310
的日期类型等,通常根据Json
工具的支持会序列化成Json
的数值或字符串。我们自定义的类型则直接序列化成Json
的对象。
我们就不班门弄斧,直接使用一些Json
工具来实例一下:
package com.gitee.theskyone.bird.web;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import lombok.Data;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author theskyzero
* @date 2022-04-16
*/
public class JsonDemoTest {
@Data
static class JsonDemo {
Long id = 10L;
String name = "demo";
int[] ints = new int[]{1, 2, 3};
Double[] doubles = new Double[]{4.1, 4.2, 4.33};
List<Byte[]> bytesList = Arrays.asList(new Byte[]{5, 6, 7}, new Byte[]{8, 9});
List<Byte>[] byteLists = new List[]{Lists.newArrayList(3, 4, 5), Lists.newArrayList(6, 7, 8)};
Map<String, List<Long>> stringListMap = Lists.newArrayList(2L, 34L, 5L).stream().collect(Collectors.groupingBy(id -> id + ""));
Map<JsonDemo, List<String>> jsonDemoMap = Lists.newArrayList("string").stream().collect(Collectors.groupingBy(id -> this));
@Override
public String toString() {
return "JsonDemo{" +
"id=" + id +
", name='" + name + '\\'' +
'}';
}
}
public static void main(String[] args) throws JsonProcessingException {
JsonDemo jsonDemo = new JsonDemo();
ObjectMapper mapper = new ObjectMapper();
// 序列化
String serialized = mapper.writeValueAsString(jsonDemo);
System.out.println(serialized);
// 反序列化
JsonDemo readValue = mapper.readValue(serialized, JsonDemo.class);
System.out.println(readValue);
}
}
// 输出
{"id":10,"name":"demo","ints":[1,2,3],"doubles":[4.1,4.2,4.33],"bytesList":[[5,6,7],[8,9]],"byteLists":[[3,4,5],[6,7,8]],"stringListMap":{"34":[34],"2":[2],"5":[5]},"jsonDemoMap":{"JsonDemo{id=10, name='demo'}":["string"]}}
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot find a (Map) Key deserializer for type [simple type, class com.gitee.theskyone.bird.web.JsonDemoTest$JsonDemo]
at [Source: (String)"{"id":10,"name":"demo","ints":[1,2,3],"doubles":[4.1,4.2,4.33],"bytesList":[[5,6,7],[8,9]],"byteLists":[[3,4,5],[6,7,8]],"stringListMap":{"34":[34],"2":[2],"5":[5]},"jsonDemoMap":{"JsonDemo{id=10, name='demo'}":["string"]}}"; line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)
at com.fasterxml.jackson.databind.deser.DeserializerCache._handleUnknownKeyDeserializer(DeserializerCache.java:603)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findKeyDeserializer(DeserializerCache.java:168)
at com.fasterxml.jackson.databind.DeserializationContext.findKeyDeserializer(DeserializationContext.java:669)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.createContextual(MapDeserializer.java:302)
at com.fasterxml.jackson.databind.DeserializationContext.handlePrimaryContextualization(DeserializationContext.java:825)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:550)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:294)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:642)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4805)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4675)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3629)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3597)
at com.gitee.theskyone.bird.web.JsonDemoTest.main(JsonDemoTest.java:45)
翻车了是不是,我们序列化正常,反序列化失败,为什么?很简单,我们序列化出来的jsonDemoMap
的key
是JsonDemo
的toString()
,不是个Json
格式,无法反序列化出来JsonDemo
作为我们JsonDemo
字段的key
。所以我们在使用Map时要注意在Json
反序列化时的影响。Map
其实不建议在我们正常的编码中去使用的,正常的业务我们会定义为对象。即使类似变化的kv
结构元数据配置也可以转换成集合,因为Map<K,V> = List<Map.Entry<K,V>>
。
Json的序列化反序列化就到这里,并不复杂,我们只要知道它就是在Java对象和Json文本间相互转换就行。那么剩下的无非是利用各种Json
工具来实现这个相互转换功能,一般常用的有Jackson
、FastJSON
、Gson
等,原理和功能都大同小异。
Jackson
SpringBoot
默认使用Jackson
,这里我们通过Jackson
来瞅一瞅一般使用Json
的姿势吧~
最简单序列化/反序列化工具类
使用了默认的ObjectMapper
,可以满足我们一般的需要,但是对于Jsr310
时间类型不支持,对于集合/泛型也不支持。
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static <T> String serialize(T data) {
try {
return MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T> T deserialize(String json, Class<T> clazz) {
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
最佳实践1: 支持泛型和Jdk8
类型的工具类
- 通过
spi
扫描注册默认支持的modules
- 添加一个重载方法,使用
TypeReference
支持泛型反序列化
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
}
public static <T> String serialize(T data) {
try {
return MAPPER.writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T> T deserialize(String json, Class<T> clazz) {
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T> T deserialize(String json, TypeReference<T> clazz) {
try {
return MAPPER.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
这个工具类基本上已经能满足日常需要了。之后的是一些个性化的配置特性及功能。
最佳实践2:序列化/反序列化特性
忽略错误
忽略Json
数据和Java
对象不一致产生的错误。
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
// 忽略序列化异常
MAPPER.disable(
// 忽略比如使用new Object()作为返回值无法序列化的错误 -> 写这样的代码不会被打吗?
SerializationFeature.FAIL_ON_EMPTY_BEANS,
// 忽略get返回this的错误
SerializationFeature.FAIL_ON_SELF_REFERENCES
);
// 忽略反序列化异常
MAPPER.disable(
// 忽略Json中存在Java对象木有的字段
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
);
}
}
时间处理
时间类型交互时可以考虑使用时间戳。另外,关于日期类型,还可以结合使用@JsonFormat
做一些定制。
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
// 日期以时间戳序列化
MAPPER.enable(
// date以时间戳序列化,单独列出是因为springboot默认给禁用了
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
).disable(
// 禁用写 nanos
SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS
// 使用系统默认时区
).setTimeZone(TimeZone.getDefault());
}
}
枚举处理
枚举默认使用name()
序列化成String
。可以调整使用toString()
,可以简单的支持自定义枚举类型序列化。
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
// 枚举处理
MAPPER.enable(
// 使用toString()序列化
SerializationFeature.WRITE_ENUMS_USING_TO_STRING
).enable(
// 使用toString()反序列化
DeserializationFeature.READ_ENUMS_USING_TO_STRING
);
}
}
空值处理
Jackson默认会序列化所有字段,对于空值我们可以设置不序列化,减少数据传输等。或者比如使用spring-cache分布式缓存,也可以减少缓存存储。
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
// 仅序列化非NULL字段 -> NON_EMPTY和NON_DEFAULT杀伤力更大,NON_NULL符合一般实践认知
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}
忽略大小写
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper();
static {
// 通过spi注册支持的modules(对象映射器),如JavaTimeModule等
MAPPER.findAndRegisterModules();
// 大小写不敏感
MAPPER.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
}
}
最佳实践3:项目实战特性
一些适合在项目中玩转的功能特性。
字段映射
默认情况下,我们推荐Java
序列化成Json
的字段名使用驼峰格式,不过有些外部系统可能设计不同或概念差异,字段名格式或名称与我们系统内部定义不一样,这时候我们通常会做字段映射,不必将外部系统的差异引入到系统内部。
使用@JsonProperties
或@JsonAlias
指定Json
字段名称。顾名思义,前者改写(反)序列化时的字段名称,后者是别名,在反序列化时生效。
public class JsonDemo {
public static void main(String[] args) {
A a = new A();
// 序列化成a_instant
String serialize = JsonUtils.serialize(a);
System.out.println(serialize);
// bDate别名可被解析
serialize = "{\\"bDate\\":1650116489517,\\"localDateTime\\":[2022,4,16,21,41,29,522],\\"a_instant\\":1650116489517}";
A deserialize = JsonUtils.deserialize(serialize, A.class);
System.out.println(deserialize);
}
@Data
static class A {
@JsonProperty("a_instant")
Instant a = Instant.now();
@JsonAlias("bDate")
Date b = new Date();
LocalDateTime localDateTime = LocalDateTime.now();
}
}
// 输出
{"b":1650116519832,"localDateTime":[2022,4,16,21,41,59,837],"a_instant":1650116519832}
JsonUtils.A(a=+54260-02-10T11:31:57Z, b=Sat Apr 16 21:41:29 CST 2022, localDateTime=2022-04-16T21:41:29.000000522)
格式化输出
这个功能在数据传输的场景不实用,后端开发通常也不会关心Json
数据的样式。了解即可。
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static <T> String formatSerialize(T data) {
try {
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
忽略字段
有一些特殊的场景,比如我们对象中有些方法或字段仅系统内部使用,不需要提供外部。或者更多的比如创建请求和更新请求,更新必须携带id而创建则不需要。这些场景下需要我们在序列化/反序列化时忽略某些不需要关心、暴露的字段。
这部分已经比较深入使用Jackson
了,还是更建议使用不同的对象来区分不同的使用场景。简单的场景下我们可以使用@JsonIgnore
或@JsonignoreProperties
来静态忽略字段。而对于创建和更新请求这个场景,简单一点可以在业务处理上忽略创建请求携带的id,复杂点的实现则要通过@JsonView
和@JsonFilter
来动态处理。
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static <T, V> String serializeWithView(T data, Class<V> view) {
try {
return MAPPER.writerWithView(view).writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static <T, V> T deSerializeWithView(String data, Class<T> clazz, Class<V> view) {
try {
return MAPPER.readerWithView(view).readValue(data, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
深拷贝
我们可以借助JSON来实现对象深拷贝,即序列化+反序列化。
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static <S, T> T convert(S source, Class<T> clazz) {
return MAPPER.convertValue(source, clazz);
}
public static <S, T> T convert(S source, TypeReference<T> typeReference) {
return MAPPER.convertValue(source, typeReference);
}
}
SpringWeb
中的Json
应用
在SpringMvc
中如何使用处理application/**+json
?
SpringMVC
请求处理流程
一般即在RequestMappingHandlerMapping
中解析请求、处理响应。我们可以在RequestMappingHandlerMapping
中看到很多解析器来处理Http
消息,其中RequestResponseBodyMethodProcessor
即用来处理@RequestBody
和@ResponseBody
。而处理又是通过各种HttpMessageConverter
来完成HttpMessage
到Java
对象的相互转换。对于application/**+json
默认使用MappingJackson2HttpMessageConverter
。
MappingJackson2HttpMessageConverter
MappingJackson2HttpMessageConverter
中即使用了Jackson
的ObjectMapper
。那我们这里其实就是看在MappingJackson2HttpMessageConverter
中ObjectMapper
的使用姿势。其中到底用到了Jackson
哪些玩法呢?
package org.springframework.http.converter.json;
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
setDefaultCharset(DEFAULT_CHARSET);
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
this.ssePrettyPrinter = prettyPrinter;
}
/**
* Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
* <pre class="code">
* ObjectMapper mapper = new ObjectMapper();
* mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
* converter.setObjectMapper(mapper);
* </pre>
*/
public void setPrettyPrint(boolean prettyPrint) {
this.prettyPrint = prettyPrint;
configurePrettyPrint();
}
private void configurePrettyPrint() {
if (this.prettyPrint != null) {
this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint);
}
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz, null);
return readJavaType(javaType, inputMessage);
}
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(type, contextClass);
return readJavaType(javaType, inputMessage);
}
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
return this.objectMapper.readerWithView(deserializationView).forType(javaType).
readValue(inputMessage.getBody());
}
}
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter = (serializationView != null ?
this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* @param type the generic type to return the Jackson JavaType for
* @param contextClass a context class for the target type, for example a class
* in which the target type appears in a method signature (can be {@code null})
* @return the Jackson JavaType
*/
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
}
}
总结一下:支持的ObjectMapper
特性:
- 29行:根据配置开启格式化序列化
- 128行:根据
Class
获取JavaType
,可以支持@JsonTypeInfo
等 - 54行:反序列化(读)支持运行时
@JsonView
- 84行:序列化(写)支持运行时
@JsonView
- 86行:序列化(写)支持运行时
@JsonFilter
@JsonView
和@JsonFilter
是个比较好玩的特性,可以动态忽略字段的序列化/反序列化。比较典型的场景是POST
和PUT
对象通常是一样的,但是POST不需要传id
而PUT
必须要传id
,这时候通常没必要单独为POST
建一个对象,那么可以通过@JsonView
创建一个POST
读视图忽略读取id
。
SpringWeb
在JsonViewResponseBodyAdvice
中默认提供了对@JsonView
的支持,但是开启@JsonFilter
需要一些额外的工作,这里不展开了,有兴趣的同学可以自己尝试玩一玩比如结合@JsonIngoreProperties
动态忽略响应字段~