序列化工具使用及注意事项
前言
序列化工具的作用主要是将对象转换为字节流或者字符流,以便可以将对象的状态保存到存储介质(如文件、数据库)中,或者通过网络传输对象。反过来,这些工具还可以将字节流恢复成原来的 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不一致,举个栗子,还是上面的checkDate和endDate,我们来看下默认的行为:
"{"endDate":1723166828099,"checkDate":1723166828099}"
是不是也不一样。
Hutool工具中是不支持注解类型,必须通过配置文件来格式化输出数据。
如果其他同学对Hutool的工具封装感兴趣,可以看下官网的文档及源码,使用上可以带来一定的方便。
总结
1、现在的所有Java企业项目都是Springboot的,自带有序列化工具Jackson,基本上无需引入其他序列化工具,如果你对Jackson不熟悉,可以找篇文章学习下,简单易用;
2、不想使用Jackson的同学,例如已经对FastJson有一定的程序的了解了,当然也可以使用,但是注意,不要引入多版本造成不必要的麻烦;
3、对于全局配置的内容,一般情况下是在WebMvcConfig中,可以在项目中找找,看下目前已经实现了哪些格式转化,另外,如果使用FastJson进行全局配置,它会重新覆盖原先的Jackson;
4、如果你很不幸,在你所在的项目中混用了上述所有的序列化工具,请看清楚每个JSON对象所使用包,尽量统一;