序列化工具使用及注意事项

511 阅读8分钟

序列化工具使用及注意事项

前言

序列化工具的作用主要是将对象转换为字节流或者字符流,以便可以将对象的状态保存到存储介质(如文件、数据库)中,或者通过网络传输对象。反过来,这些工具还可以将字节流恢复成原来的 Java 对象。

近期,由于在项目中混用多种序列化工具而导致问题,从而将对序列化工具做一个概述。

常见的序列化工具:

  • Java内置序列化:java.io.Serializable
  • Jackson:Spring Boot自动配置了Jackson作为默认的JSON序列化和反序列化库,无需手动配置;
  • FastJson:Alibaba提供的JSON序列化工具,目前有fastJson和fastJson2两个版本,项目中用的较多;
  • Gson:Google提供的转换JSON的工具;

在项目中,由于同时存在Jackson、fastJson、fastJson2、hutool-json四种序列化工具,其中fastJson和fastJson2的行为是不一致的,后续将会说到,且这几种工具都支持全局化配置,不熟悉框架本身的同学可能都不了解,导致在混用的过程中,可能使用hutool的json去接受参数,使用fastJson去完整序列化,以至于最后输出的结果和预期不一致,特别是打印日志的过程中使用了与参数相同的序列化包,发现日志是正确的,从而未能正确排查问题所在。

Serializable

还是先从原生的开始说吧。我们直接来举个栗子:

//首先定义一个person对象@Data
@Accessors(chain = true)
public class Person implements Serializable {
    
    private static final long serialVersionUID = 1L;
    static String country = "ITALY";
    private int age;
    private String name;
    //transient关键字表示该属性不参与序列化
    transient int height;
​
}
​
try
{
    Person person = new Person();
    person.setAge(20);
    person.setName("Joe");
​
    //序列化,将person对象转成字节流
    FileOutputStream fileOutputStream
      = new FileOutputStream("yourfile.txt");
    ObjectOutputStream objectOutputStream
      = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(person);
    objectOutputStream.flush();
    objectOutputStream.close();
​
    //反序列化,将字节流转成对象
    FileInputStream fileInputStream
      = new FileInputStream("yourfile.txt");
    ObjectInputStream objectInputStream
      = new ObjectInputStream(fileInputStream);
    Person p2 = (Person) objectInputStream.readObject();
    System.out.println(p2.toString());
    objectInputStream.close();
}
catch (Exception ex)
{
  ex.printStackTrace();
}

Serializable的确在工作中很少用到,一般都是使用业内标准的JSON字符。当然不用它也是有原因的,虽然在java语言中简单易用,但是性能不高,序列化数据较大,数据传输不就讲究个高效嘛。

Jackson

springboot默认使用Jackson对请求载体进行反序列化和对响应数据进行序列化,那么,在序列化过程中,当我们希望加入一些特定规则时,spring-boot提供了几种解决方案,如下:

1、使用Jackson注解,可针对单个对象的某个属性配置特定的序列化规则:

//针对于将Date数据在序列化时格式化为如下形式
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss")
//@DateTimeFormat这个注解是针对于反序列化的
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createDate;

当前,特别是针对于Long型,在前端会被截断,例如雪花算法等:

@JsonSerialize(using = ToStringSerializer.class)
private Long id;

2、使用配置文件,可全局的配置特定的序列化规则:

spring:
  jackson:
    date-format: yyyy-MM-dd hh:mm:ss

3、注入自定义的ObjectMapper, 覆盖默认的ObjectMapper,可全局配置特定的序列化规则:

@Bean
public ObjectMapper objectMapper(){
    ObjectMapper objectMapper = new ObjectMapper();
    //日期格式
    objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    //设置时区
    objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
    //序列化-忽略null值的属性
    objectMapper.setSerializationInclusion(Include.NON_NULL);
    //序列化-允许序列化空对象
    objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    //反序列化-在遇到未知属性的时候不抛出异常
    objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    return objectMapper;
}

如果对Spring IOC和@Bean不了解,那就要回炉了。

FastJson

FastJson是由Alibaba开发的JSON序列化工具,分为两个版本,分别是FastJson和FastJson2,为什么要强调两个版本,是因为它两的默认行为不一致,正常情况下版本都是向前兼容的,更可怕的是,它两可以一起使用,什么意思,就是你可以在一个项目里同时引用两个大版本,就想这样:

@Data
@Accessors(chain = true)
public class FuliTaskSubmitDto {
    //fastjson2
    @com.alibaba.fastjson2.annotation.JSONField(format = "yyyy-MM-dd HH:mm")
    //fastjson
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime checkDate;
​
}

那么问题来了,如果我同事使用了两个fastjson的注解,最后应该听谁的呢,就是你所使用的JSON-Util是哪个版本的包,就用的哪个注解,其实也可以理解为FastJson和FastJson2就是两个不同的包,而并不是版本上的差异,其artifactId就是不同的。

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.83</version>
</dependency><dependency>
  <groupId>com.alibaba.fastjson2</groupId>
  <artifactId>fastjson2</artifactId>
  <version>2.0.52</version>
</dependency>

另外,@JSONField@JsonFormat的作用是相同的,只不过一个是给fastJson序列化时用,另一个给Jackson序列化时用,但是,敲黑板,在使用FastJson的情况下,@JsonFormat是不生效的,你必须使用@JSONField显示声明,而在使用FastJson2的时候它是生效的,所以你使用@JsonFormat也能玩,这是第一个不同。

那么,我用FastJson2且同时使用@JSONField@JsonFormat,且二者注解pattern不同的情况下,程序应该使用谁的呢,当然是@JSONField呀,他们可是一家人。

另外这两个版本不同的是,序列化时对应的工具类,一般来说最需要格式化的当属时间属性LocalDateTime,对于不同版本fastJson,所使用的序列化工具不同,导致最后默认行为不一致。

FastJson中,

默认展示的是{"checkDate":"2024-08-08T20:53:07.412836000"}这个样子的,而在FastJson2中,是{"checkDate":"2024-08-08 20:54:30.405236"}这个样子,看出来区别了吗?是不是很相近。

增加一个时间属性,类型为java.util.Date后,

FastJson展示的是时间戳{"checkDate":"2024-08-08T20:57:54.731388000","endDate":1723121874731}endDate,FastJson2展示的时间格式{"checkDate":"2024-08-08 20:59:00.897533","endDate":"2024-08-08 20:59:00.897"},看懂了吗。目前针对的都是时间相关类型,其他类型未探究过。

特别是对于默认使用时间戳的同学,想使用FastJson2的新特性,在切换版本后就是error

和Jackson一致,FastJson也支持全局配置:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
​
    /**
     * fastjson的全局序列化方式
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 1、需要先定义一个convert转换消息的对象;
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        // 2、添加fastjson的配置信息,比如是否要格式化返回json数据
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        // 3、在convert中添加配置信息.
        fastConverter.setFastJsonConfig(fastJsonConfig);
        // 4、将convert添加到converters当中.
        converters.add(fastConverter);
    }
}

对于全局类的配置,大家还是要慎重。一是在其他项目中,某些同学在全局配置中进行Long2String的转换,导致多个完好功能报错;二是部分同学并不知道全局配置的存在,可能无法理解序列化后的结果预期。

优先级方面:属性注解 > 全局配置

Hutool

Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类。所以在这个工具包下,也含有自己的JSON序列化工具。

以下Hutool官网自己的概述:

JSON在现在的开发中做为跨平台的数据交换格式已经慢慢有替代XML的趋势(比如RESTful规范),我想大家在开发中对外提供接口也越来越多的使用JSON格式。

不可否认,现在优秀的JSON框架非常多,我经常使用的像阿里的FastJSON,Jackson等都是非常优秀的包,性能突出,简单易用。Hutool开始也并不想自己写一个JSON,但是在各种工具的封装中,发现JSON已经不可或缺,因此将json.org官方的JSON解析纳入其中,进行改造。在改造过程中,积极吸取其它类库优点,优化成员方法,抽象接口和类,最终形成Hutool-json。

也就是说,Hutool的序列化工具基本上是他们自己写的,只不过吃了百家饭。所以他们的序列化行为也和Jackson和FastJson不一致,举个栗子,还是上面的checkDateendDate,我们来看下默认的行为:

"{"endDate":1723166828099,"checkDate":1723166828099}"

是不是也不一样。

Hutool工具中是不支持注解类型,必须通过配置文件来格式化输出数据。

如果其他同学对Hutool的工具封装感兴趣,可以看下官网的文档及源码,使用上可以带来一定的方便。

总结

1、现在的所有Java企业项目都是Springboot的,自带有序列化工具Jackson,基本上无需引入其他序列化工具,如果你对Jackson不熟悉,可以找篇文章学习下,简单易用;

2、不想使用Jackson的同学,例如已经对FastJson有一定的程序的了解了,当然也可以使用,但是注意,不要引入多版本造成不必要的麻烦;

3、对于全局配置的内容,一般情况下是在WebMvcConfig中,可以在项目中找找,看下目前已经实现了哪些格式转化,另外,如果使用FastJson进行全局配置,它会重新覆盖原先的Jackson;

4、如果你很不幸,在你所在的项目中混用了上述所有的序列化工具,请看清楚每个JSON对象所使用包,尽量统一;