java8的Instant反序列化失败异常总结

2021/10/30

前言

今天专心做工的时候,同事说吐槽在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也拿出来一战

准备工作

  1. 首先创建一个项目,然后把一些类文件给创建,我的做法如下
  • Mian/子包略
    • controller -里面放一个控制器,用来测试mvc参数绑定,还有一个request.http请求文件
    • entity -里面放一个测试专门用来传递的实体类
    • jsonconfig -这里放可能会配置的json配置
  • test/子包略
    • FastJsonTests.java -里面放fastjson的序列化和反序列化代码
    • JacksonTests.java -里面放jackson的序列化和反序列化代码
    • MixTests.java -里面放fastjson和jackson混合序列反序列化代码
    • SummaryTests.java -里面总结打印每个工具序列化Instant的格式
  1. 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>
  1. 编写一个测试的DTO
// 这里我使用了lombok来简化代码
@Data
@SuperBuilder
@NoArgsConstructor
public class TimeDTO {

  /**
   * 充数字段
   */
  private String name;

  /**
   * 重点测试字段
   */
  private Instant instant;

}
  1. 为了减少重复代码,在所有的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());注册一下就好了。其实这里面就是帮你加入了很多反序列化的解析器。源码看进去,无参构造开始疯狂的添加各类时间反序列化解析器。

image-20211031182716930

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

image-20211031192642552

starter-json下可以看到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);
  }

}

image-20211031193133086

样例

结果和我们实验结果一样,前端传"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






下面是评论区,欢迎大家留言探讨或者指出错误哈