Jackson 日期序列化与反序列化在SpringBoot下最优方案对比 And 坑

2,914 阅读3分钟

背景

日常Web开发中对日期格式的序列化与反序列化是必不可少,在微服务下若没有一套完善且统一的配置,会出现各种奇奇怪怪的问题,如@JsonFormat(pattern = "yyyy-MM-dd")默认的是GMT时区,而中国是GMT+8的东八区,若不声明时区会少一个小时,又比如若两服务序列化配置不一致,会导致远程调用失败等

需求点

  1. 接口入参无论是yyyy-MM-dd还是yyyy-MM-dd HH:mm:ss 均支持反序列化
  2. 反参序列化,默认为yyyy-MM-dd HH:mm:ss ,但支持某些字段以@JsonFormat(pattern = "yyyy-MM-dd")定义
  3. 不会有时区问题

方案1:无任何配置

  1. 默认返回的是时间戳,时区是系统自带的时区
  2. 需在每一个字段都加上@JsonFormat 进行配置

虽说这样做没有问题,但需要在每一个dto上面的日期字段加注解,肯定不科学

方案2:使用配置文件指定

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
  1. 指定后,序列化和反序列化都只能是一个格式
  2. 若入参是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");
    }
}
  1. 这样做后入参的反序列化可以自行拓展,比如支持yyyy-MM-ddHH:mm:ss
  2. 序列化只能一种格式,若想支持多种而使用@JsonFormat自定义格式化的话,会有时区问题!,必须显式指定时区:@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")

关于第二点这个坑,我研究了一上午想全局指定时区,但好像不太行,尝试的方法:

  1. 在配置文件指定时区,不行,因为配置文件其实已经无用了
  2. objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); 会抛异常,自定义的DateFormat就是会抛异常,百思不得其解,但使用其自带的SimpleDateFormat,就正常的
  3. 那我在想会不会拓展的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 ~