一、LocalDateTime
反序列化异常
首先我们定义一个java POJO实体类,其中关键的成员变量时birthDate
,我们没有采用Date数据类型,而是采用了Java8 新的日期类型LocalDateTime
,使用LocalDateTime
的好处我就不多说了,有很多的文章解释说明。我们把精力放回到Jackson的JSON格式序列化与反序列化内容上来。
@Data
public class PlayerStar4 {
private String name; //姓名
private LocalDateTime birthDate; //出生日期
}
下面的代码,我们首先定义了一个PlayerStar4类的对象player,然后
- 使用writeValueAsString方法将player对象序列化为JSON字符串jsonString
- 然后使用readValue方法将JSON字符串jsonString ,反序列化为PlayerStar4类的对象
@Test
void testJSON2Object() throws IOException {
ObjectMapper mapper = new ObjectMapper();
PlayerStar4 player = new PlayerStar4();
player.setName("curry");//我并不知道库里的生日,这里是编造的
player.setBirthDate(LocalDateTime.of(1986,4,5,12,50));
//将player对象以JSON格式进行序列化为String对象
String jsonString = mapper.writeValueAsString(player);
System.out.println(jsonString);
//将JSON字符串反序列化为java对象
PlayerStar4 curry = mapper.readValue(jsonString, PlayerStar4.class);
System.out.println(curry);
}
但是上面的代码报错了,从下图中可以看出
- 将player对象序列化为JSON字符串jsonString 的过程被正常执行了,但是LocalDateTime序列化之后的结果,是图中”黄框中的黄框“内容。
- 将JSON字符串反序列化的过程报错了,因为Jackson默认情况下,根本不认识图中”黄框中的黄框“内容这种LocalDateTime序列化之后的JSON字符串数据结构。无法把它反序列化为java对象。
怎么办?我们需要自定义序列化及反序列化类型转换器,有两种方法
- 继承StdConverter类,自定义实现String与LocalDateTime相互转换
- 继承JsonSerializer和JsonDeserializer类,自定义实现String与LocalDateTime相互转换
二、方法一:继承StdConverter类
继承StdConverter类,将LocalDateTime序列化为String数据类型
public class LocalDateTimeToStringConverter extends StdConverter<LocalDateTime, String> {
static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
@Override
public String convert(LocalDateTime value) {
return value.format(DATE_FORMATTER);
}
}
继承StdConverter类,将String数据类型反序列化为LocalDateTime
public class StringToLocalDatetimeConverter extends StdConverter<String, LocalDateTime> {
@Override
public LocalDateTime convert(String value) {
return LocalDateTime.parse(value, LocalDateTimeToStringConverter.DATE_FORMATTER);
}
}
自定义的转换器完成之后,我们就可以在对应的成员变量上,使用@JsonSerialize
指定序列化转换器,@JsonDeserialize
指定反序列化转换器。
@JsonSerialize(converter = LocalDateTimeToStringConverter.class)
@JsonDeserialize(converter = StringToLocalDatetimeConverter.class)
private LocalDateTime birthDate;
然后调用第一小节中的测试用例,就不会出现异常了。控制台打印输出结果如下,第一行是序列化结果JSON格式字符串,第二行是Java 对象的toString()方法的打印结果。
{"name":"curry","birthDate":"1986-4-5 12:50:00"}
PlayerStar4(name=curry, birthDate=1986-04-05T12:50)
三、方法二:继承JsonSerializer和JsonDeserializer类
继承JsonSerializer<LocalDateTime>
类,将LocalDateTime序列化为String数据类型
public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
String s = value.format(DATE_FORMATTER);
gen.writeString(s);
}
}
继承JsonDeserializer<LocalDateTime>
类,将String数据类型反序列化为LocalDateTime
public class LocalDatetimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String str = p.getText();
return LocalDateTime.parse(str, LocalDateTimeSerializer.DATE_FORMATTER);
}
}
四、如果上面的你都没看懂
对于相对小白的读者,上面的自定义序列化及反序列化转换过程你都没懂,对于LocalDateTime的异常你也不要慌,Jackson已经给出了解决方案。
需要特别和大家强调的是LocalDateTimeSerializer和LocalDateTimeDeserializer其实并不需要我们自己去定义,因为Jackson已经帮我们定义好了。 之所以我还做了自定义的实现的介绍,是因为要为大家讲解这个自定义序列化和反序列化类型转换的实现过程,以后你再遇到其他的特殊的数据类型转换,或者LocalDateTime类型的特殊日期格式等,都可以自己来定义JsonSerialize和JsonDeserialize来实现数据类型的转换。
下面的这两个类就是Jackson已经帮我们定义好的LocalDateTimeSerializer和LocalDateTimeDeserializer。
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
使用方法是在对应的成员变量上,使用@JsonSerialize
指定序列化转换器,@JsonDeserialize
指定反序列化转换器。
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime birthDate;
执行之后的序列化和反序列化结果,和方法一、方法二自定义的实现效果是一样的。
以下是解决类型为Date
日常Web开发中对日期格式的序列化与反序列化是必不可少,在微服务下若没有一套完善且统一的配置,会出现各种奇奇怪怪的问题,如@JsonFormat(pattern = "yyyy-MM-dd")
默认的是GMT时区,而中国是GMT+8的东八区,若不声明时区会少一个小时,又比如若两服务序列化配置不一致,会导致远程调用失败等
需求点
- 接口入参无论是
yyyy-MM-dd
还是yyyy-MM-dd HH:mm:ss
均支持反序列化 - 反参序列化,默认为
yyyy-MM-dd HH:mm:ss
,但支持某些字段以@JsonFormat(pattern = "yyyy-MM-dd")
定义 - 不会有时区问题
方案1:无任何配置
- 默认返回的是时间戳,时区是系统自带的时区
- 需在每一个字段都加上@JsonFormat 进行配置
虽说这样做没有问题,但需要在每一个dto上面的日期字段加注解,肯定不科学
方案2:使用配置文件指定
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
- 指定后,序列化和反序列化都只能是一个格式
- 若入参是yyyy-MM-dd,会报错,就算使用
@JsonFormat(pattern = "yyyy-MM-dd")
也无济于事,此注解对反序列化无效
# 方案3:拓展 DateFormat
@Bean
public ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new ObjectMapperDateFormat());
return objectMapper;
}
/**
* 扩展jackson日期格式化支持格式
*/
public static class ObjectMapperDateFormat extends DateFormat {
/**
* 序列化
*/
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
return new StringBuffer(DateUtil.formatDateTime(date));
}
/**
* 反序列化
*/
@Override
public Date parse(String source, ParsePosition pos) {
source = source.trim();
pos.setIndex(source.length());
return DateUtil.parse(source);
}
@Override
public Object clone() {
return new WebConfig.ObjectMapperDateFormat();
}
/**
* 此方法无效,不止何解
*/
@Override
public TimeZone getTimeZone() {
return TimeZone.getTimeZone("GMT+8");
}
}
- 这样做后入参的反序列化可以自行拓展,比如支持
yyyy-MM-dd
、HH:mm:ss
- 序列化只能一种格式,若想支持多种而使用@JsonFormat自定义格式化的话,会有时区问题!,必须显式指定时区:
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
关于第二点这个坑,我研究了一上午想全局指定时区,但好像不太行,尝试的方法:
- 在配置文件指定时区,不行,因为配置文件其实已经无用了
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
会抛异常,自定义的DateFormat就是会抛异常,百思不得其解,但使用其自带的SimpleDateFormat,就正常的- 那我在想会不会拓展的DateFormat自己可以指定时区?(上面代码的getTimeZone 方法),尝试了也是不行的,根据断点可知使用@JsonFormat后,其序列化是不会走拓展的DateFormat,而是走自带的
StdDateFormat.java
所以该方案,如果想在不同接口返回不同的日期格式,一定要指定时区,除了这点,倒也没其他问题,但是一点都不优雅
方案4:自定义序列化、反序列化的处理器(完美方案)
@Bean
public ObjectMapper getObjectMapper() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Date.class, new MyJsonDeserializer());
simpleModule.addSerializer(Date.class, new MyJsonSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(simpleModule);
return objectMapper;
}
/**
* 自定义反序列化处理器
* 支持yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
*/
public static class MyJsonDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String source = p.getText().trim();
try {
return DateUtil.parse(source);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
/**
* 自定义序列化处理器
*/
@NoArgsConstructor
@AllArgsConstructor
public static class MyJsonSerializer extends JsonSerializer<Date> implements ContextualSerializer {
private JsonFormat jsonFormat;
/**
* 默认序列化yyyy-MM-dd HH:mm:ss
* 若存在@JsonFormat(pattern = "xxx") 则根据具体其表达式序列化
*/
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String pattern = jsonFormat == null ? DatePattern.NORM_DATETIME_PATTERN : jsonFormat.pattern();
gen.writeString(DateUtil.format(value, pattern));
}
/**
* 通过字段已知的上下文信息定制 JsonSerializer
* 若字段上存在@JsonFormat(pattern = "xxx") 则根据上面的表达式进行序列化
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
JsonFormat ann = property.getAnnotation(JsonFormat.class);
if (ann != null) {
return new MyJsonSerializer(ann);
}
return this;
}
}
此方案可完美解决文章头部提到的需求点,序列化时,通过实现ContextualSerializer
获取字段已知的上下文信息,即获取@JsonFormat中的表达式进行格式化,且不会有时区问题