前言
今天专心做工的时候,同事说吐槽在openfeign调用的时候遇mvc参数绑定Instant类型的值时反序列化失败,然后试了各种方式都没办法解析成功。我拿他demo试了一下,还真的是不行,于是吭哧吭哧折腾一晚上…后来发现他对象里面包了一个String, 然后这个String其实是他为了通用类型自己序列化成json的,然后下游解析和序列化方式不一样,然后就报错了。
然后我跳脱出项目自己测试了一下,人家mvc参数绑定是能支持Instant序列化和反序列化的…..只是每一家JSON框架对Instant的序列化处理不一样,所以导致兼容性会差一些。因为已经研究一晚上了,更加激发了我想要试验一下看看市面上各个JSON框架对于Instant的处理和兼容性是怎么样的,一方面是为了以后避坑另外是好奇好奇。
如果你有兴趣或者遇到了以下报错,我觉得你看完本篇可以找到思路。本篇文章代码在这里
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
java.lang.NoSuchMethodError:
com.fasterxml.jackson.databind.DeserializationContext.extractScalarFromObject
Caused by: java.lang.UnsupportedOperationException
at com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec.deserialze
java.lang.IllegalArgumentException:
The HTTP header line [{*}] does not conform to RFC 7230 and has been ignored.
Instant和参战序列化工具
Instant
Instant是java8新增类,表示一个高精度的时间戳。本质上来说和System.currentTimeMillis()没啥区别。它和System.currentTimeMillis()返回的long相比,只是多了更高精度的纳秒。因为Influxdb的time主键需要用到Instant,所以项目中使用Instant作为时间。
因为他的精度比较高,又是java8(虽然java8已经不新了)的新特性,可能大家支持情况还是没有统一。
三个JSON工具
重点看两个JSON序列化工具,一个是jackson这个不用多说,springmvc内部用的默认序列化工具,另外一个是fastjson这个也不用多说,相信大家也是常用。还有一个就是hutool的json序列化,因为我本身是hutool工具的重度爱好者,因为我总觉得当自己写代码写的很苦的时候,hutool总能给我一丝甜的慰藉。既然是检测兼容性,那把hutool也拿出来一战
准备工作
- 首先创建一个项目,然后把一些类文件给创建,我的做法如下
- Mian/子包略
- controller -里面放一个控制器,用来测试mvc参数绑定,还有一个request.http请求文件
- entity -里面放一个测试专门用来传递的实体类
- jsonconfig -这里放可能会配置的json配置
- test/子包略
- FastJsonTests.java -里面放fastjson的序列化和反序列化代码
- JacksonTests.java -里面放jackson的序列化和反序列化代码
- MixTests.java -里面放fastjson和jackson混合序列反序列化代码
- SummaryTests.java -里面总结打印每个工具序列化Instant的格式
- maven导包
<dependencies>
<!-- 引入web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
- 编写一个测试的DTO
// 这里我使用了lombok来简化代码
@Data
@SuperBuilder
@NoArgsConstructor
public class TimeDTO {
/**
* 充数字段
*/
private String name;
/**
* 重点测试字段
*/
private Instant instant;
}
- 为了减少重复代码,在所有的Test类集成一下含有如下代码的类。子类就直接调用
/**
* 固定Instant实例
*/
protected final Instant instant = Instant.now();
/**
* jackson的类全局使用
*/
protected final ObjectMapper objectMapper = new ObjectMapper();
FastJson测试
这个单元测试就只有一个,看看fastjson自己序列化的自己能不能解析
@Test
void allFastJsonTest() {
TimeDTO timeDTO = TimeDTO.builder().
name("fastJson测试").
instant(instant).build();
// fastjson序列化(序列化好看一点, 然后打印出来)
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 然后在使用fastjson反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
结果很显然,是可以的
{
"instant":"2021-10-30T08:00:03.210Z",
"name":"fastJson测试"
}
TimeDTO(name=fastJson测试, instant=2021-10-30T08:00:03.210Z)
并且我们可以看到序列化以后的json里面,Instant的格式变成了带Z的UTC时间
Jackson测试
Jackson测试就多了很多,因为jackson他默认的序列化和注册了时间模块的序列化对Insant序列化有区别,所以分开来测试一下。
时间模块是jackson提供的jsr310包下的类,一般spring已经导入了所以不需要额外导入。这是使用jackson的时候需要objectMapper.registerModule(new JavaTimeModule());
注册一下就好了。其实这里面就是帮你加入了很多反序列化的解析器。源码看进去,无参构造开始疯狂的添加各类时间反序列化解析器。
jackson(不注册时间模块)Tojackson(不注册时间模块)
@Test
@SneakyThrows
void allJacksonWithNonJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson不注册时间模块序列化").
instant(instant).build();
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson不注册时间模块序列化",
"instant" : {
"epochSecond" : 1635582033,
"nano" : 590000000
}
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
好家伙直接报错,当我们可以看到,没有注册时间模块的jackson序列化Instant以后是序列化成一个对象,里面又套了一个json串
jackson(注册时间模块)Tojackson(注册时间模块)
@Test
@SneakyThrows
void allJacksonWithJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化").
instant(instant).build();
// 给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
result:
{
"name" : "jackson注册时间模块序列化",
"instant" : 1635582344.024000000
}
TimeDTO(name=jackson注册时间模块序列化, instant=2021-10-30T08:25:44.024Z)
注册了时间模块的jackson可以正常序列化Instant的类,并且仔细看序列化后的json,是一个浮点型,前面是秒钟值,后面是纳秒值。
jackson(注册时间模块)Tojackson(没注册时间模块)
@Test
@SneakyThrows
void JacksonWithJava8ToJacksonWithNonJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化").
instant(instant).build();
// 给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用重新创建一个jackson反序列化(和这个是没有注册时间模块的)
TimeDTO obj = new ObjectMapper().readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化",
"instant" : 1635582847.359000000
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
看到没有注册时间模块的jackson是不能解析注册了时间模块jackson序列化出来的xxx.xxx格式的浮点型
jackson(没有注册时间模块)Tojackson(注册时间模块)
@Test
@SneakyThrows
void JacksonNonWithJava8ToJacksonWithJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化").
instant(instant).build();
// 用不注册时间模块的jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// 然后再jackson反序列化(现在已经注册时间模块的)
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化",
"instant" : {
"epochSecond" : 1635583012,
"nano" : 867000000
}
}
!!!报错
java.lang.NoSuchMethodError:
com.fasterxml.jackson.databind.DeserializationContext.extractScalarFromObject
没有jackson注册时间模块的序列化格式,注册了的jackson也不能正常解析。就是这种对象形式。总结就是jackson自己序列化出的这种对象形式的Instant,不管怎么样自己都无法序列化…不管是添没添加这个时间模块真的是尴尬
混合测试
上面我们可以看到FastJson还是挺可以,至少自己序列化的自己可以反序列化,jackson只有序列化和反序列化加了时间模块才行,如果都没加会看到自己序列化自己都无法反序列化的尴尬局面。现在我们来混合测一下
jackson(没注册时间模块) To FastJson
@Test
@SneakyThrows
void JackSonWithNonJava8ToFastJSON() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson不注册java8时间模块序列化, 然后用FastJson反序列化").
instant(instant).build();
// 用没用注册java8时间模块的jackson序列化
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 用fastjson直接反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson不注册java8时间模块序列化, 然后用FastJson反序列化",
"instant" : {
"epochSecond" : 1635610701,
"nano" : 940000000
}
}
TimeDTO(name=jackson不注册java8时间模块序列化, 然后用FastJson反序列化, instant=2021-10-30T16:18:21.940Z)
实在是太牛了,居然jackson自己都没办法反序列化的这种对象格式,fastjson可以反序列成功json对象格式的Instant
jackson(注册了时间模块) To FastJson
@Test
@SneakyThrows
void JackSonWithJava8ToFastJSON() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册java8时间模块序列化, 然后用FastJson反序列化").
instant(instant).build();
// 用注册java8时间模块的jackson序列化
objectMapper.registerModule(new JavaTimeModule());
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 用fastjson直接反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册java8时间模块序列化, 然后用FastJson反序列化",
"instant" : 1635610909.898000000
}
Caused by: java.lang.UnsupportedOperationException
at com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec.deserialze
出人意料,jackson注册了时间模块以后序列化出来的Instant类型,fastjson无法解析。也就是说fastjson无法解析xxx.xxx的浮点型格式成Instant
FastJson To jackson(没注册时间模块)
@Test
@SneakyThrows
void FastJSONToJackSonWithNonJava8() {
TimeDTO timeDTO = TimeDTO.builder().
name("使用FastJson序列化, 然后使用没用注册java8时间模块的jackson反序列化").
instant(instant).build();
// 使用FastJson进行序列化
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 使用没用注册java8时间模块的jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
result:
{
"instant":"2021-10-30T16:27:11.054Z",
"name":"使用FastJson序列化, 然后使用没用注册java8时间模块的jackson反序列化"
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
报错了,jackson没注册时间模块的话无法解析Fastjson的UTC格式,相当于没有注册时间模块的jackson所有Instant序列化的格式都反序列化不了,包括自己序列化的格式。
FastJson To jackson(注册了时间模块)
@Test
@SneakyThrows
void FastJSONToJackSonWithJava8() {
TimeDTO timeDTO = TimeDTO.builder().
name("使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化").
instant(instant).build();
// 使用FastJson进行序列化
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 使用注册java8时间模块的jackson反序列化
objectMapper.registerModule(new JavaTimeModule());
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"instant":"2021-10-30T16:30:07.833Z",
"name":"使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化"
}
TimeDTO(name=使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化, instant=2021-10-30T16:30:07.833Z)
成功,那么结论是注册了时间模块的jackson可以反序列化xxx.xxx的格式,也可以反序列化带Z的UTC格式
总结(不愿意看,可以到这里看结论)
结论
上面例子其实有点杂,自己跑的话有非常清晰的认识,但是如果你只需要一个结论,那么请看这里。
我们先看看不同序列化工具对Instant序列化后是一个什么样子
@Test
@SneakyThrows
void sumTest() {
TimeDTO timeDTO = TimeDTO.builder().instant(instant).build();
timeDTO.setName("FastJson序列化以后的结果");
String str1 = JSON.toJSONString(timeDTO);
timeDTO.setName("Jackson没注册时间模块序列化以后的结果");
String str2 = objectMapper.writeValueAsString(timeDTO);
timeDTO.setName("Jackson注册了时间模块序列化以后的结果");
String str3 = new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(timeDTO);
timeDTO.setName("HuTools工具序列化以后的结果");
String str4 = JSONUtil.toJsonStr(timeDTO);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);
}
{"name":"FastJson序列化以后的结果","instant":"2021-10-30T16:59:29.896Z"}
{"name":"Jackson没注册时间模块序列化以后的结果","instant":{"epochSecond":1635613169,"nano":896000000}}
{"name":"Jackson注册了时间模块序列化以后的结果","instant":1635613169.896000000}
{"name":"HuTools工具序列化以后的结果","instant":1635613169896}
可以看到每一家对Instant的序列化真的是不一样,那么经过我的测试,我做了一个表方便大家查看,里面也加了hutool工具的情况
名字 | Instant序列化后的格式 | 反序列情况 |
---|---|---|
FastJson | “2021-10-30T16:59:29.896Z” | FstJson可以 Hutool可以 jackson(未注册时间模块)不可以 jackson(注册时间模块)可以 |
Hutool | 1635613169896 | FstJson可以 Hutool可以 jackson(未注册时间模块)不可以 jackson(注册时间模块)不可以(不会报错,但是会把毫秒解析成秒) |
Jackson(未注册时间模块) | {“epochSecond”:1635613169,”nano”:896000000} | FstJson可以 Hutool不可以(不会报错,但是为null) jackson(未注册时间模块)不可以 jackson(注册时间模块)不可以 |
Jackson(注册了时间模块) | 1635613169.896000000 | FstJson不可以 Hutool不可以(不报错,但是为null) jackson(未注册时间模块)不可以 jackson(注册时间模块)可以 |
上面可以看到,加粗的没注册时间模块的jackson实在是太菜了,一个都不行。相反fastjson虽然被天天被大家dis,但是兼容性还是很强的。并且fastjson序列化和反序列化是可以自定义的(当然jackson也可以),只不过项目中fastjson用的更加顺手一些,所以下面写一个自定义的解析器,让fastjson完美补上这支持不了的。
对fastjson做增强
直接继承ObjectDeserializer,然后重写里面的deserialze方法。重点看里面的两个参数,一个是parser需要从这里面取出你现在要反解析的对象(注意只能取一次)。还一个是name,这个name虽然是object,但其实他是parser的key。取出来然后强转成string,然后对其做解析就好了,xxx.xxx格式,前面是秒,后面是纳秒。使用Instant的静态方法拆一下就可以生成,然后返回。
public class InstantDeserialize implements ObjectDeserializer {
@Override
@SuppressWarnings("unchecked")
public Instant deserialze(DefaultJSONParser parser, Type type, Object name) {
// 参数在parser里面, name是参数名字(虽然用object接收, 其实是字符串)
Object value = parser.parse(name);
// 通过'.'分割, 然后拿到list
List<String> split = StrUtil.split(Convert.toStr(value), '.');
// 把前部分变成秒, 后部分变成纳秒, 然后生成Instant返回. 如果发生异常 返回一个null
return Try.of(() -> Instant.ofEpochSecond(Convert.toInt(split.get(0)), Convert.toInt(split.get(1)))).getOrNull();
}
@Override
public int getFastMatchToken() {
return 0;
}
}
解析写好以后,上面不需要加bean之类的,因为我们不需要全局设置,所以把它配置到到你需要的类的需要的字段就好了。
@Data
@SuperBuilder
@NoArgsConstructor
public class TimeDTO {
/**
* 充数字段
*/
private String name;
/**
* 重点测试字段
*/
@JSONField(deserializeUsing = InstantDeserialize.class)
private Instant instant;
}
现在试一下就可以解析xxx.xxx的格式啦
最后测一下mvc参数绑定接收参数
SpringBoot的参数绑定序列化和反序列化默认使用的是jackson
问题来了,那么mvc参数绑定的jackson是否注册了时间模块呢,其实上面截图里面其实已经可以看到jsr310依赖了,说明大概率是注册了,show code。
我在main包(刚刚测试用例都是在test包下)下写了一个控制器,并且在同包下有一个.http的请求样例
@RestController
public class TestController {
/**
* 接口测试样例请看同包下.http文件
* @param timeDTO 测试实体类
* @return 测试返回数据
*/
@PostMapping("/test")
public ResponseEntity<TimeDTO> parameterBindingTest(@RequestBody TimeDTO timeDTO) {
return ResponseEntity.ok(timeDTO);
}
}
结果和我们实验结果一样,前端传"instant":"2021-10-30T16:59:29.896Z"
和"instant":"2021-10-30T16:59:29.896Z"
可以正常解析,传其他的不是报错就是解析错误。
当然你也可以通过配置把默认springmvc序列化工具换成fastjson,把这个方法写到你的fastjson配置类里面就好啦
/**
* 序列化机制改为fastJson
* @return
*/
@Bean
@Primary
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteBigDecimalAsPlain
);
fastConverter.setFastJsonConfig(fastJsonConfig);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
return new HttpMessageConverters(fastConverter);
}
后言
内容稍稍多,不知道怎么写能够有条理一些,里面除了样例还有fastjson的自定义反序列化,替换mvc的默认序列化配置,都稍稍提了一嘴,其实这里面也有很多门道,如果我文章中没说情况的话,代码都已经上传Github,有兴趣可以clone下来自己跑一下,代码里面注解非常翔实,样例非常完善,除了代码风格是谷歌的224格式我看的感觉稍稍别扭(公司给要求遵循的style,写自己的项目我也没改回来)。
(转载本站文章请注明作者和出处 没有气的汽水)
┌┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┬┐ ├ 文章已经完啦, 想要第一时间收到文章更新可以关注↓ ┤ └┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┴┘
Post Directory
下面是评论区,欢迎大家留言探讨或者指出错误哈