背景
日常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中的表达式进行格式化,且不会有时区问题, End ~