添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

今天专心做工的时候,同事说吐槽在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的格式
  • <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());注册一下就好了。其实这里面就是帮你加入了很多反序列化的解析器。源码看进去,无参构造开始疯狂的添加各类时间反序列化解析器。

    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工具的情况

    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

    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);
    

    结果和我们实验结果一样,前端传"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,写自己的项目我也没改回来)。