SpringBoot日期类OffsetDateTime序列化问题分析解决

导读:本篇文章讲解 SpringBoot日期类OffsetDateTime序列化问题分析解决,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

问题背景

在使用Redis时,常用的值序列化器为GenericJackson2JsonRedisSerializer,但是该序列化器默认不支持Java8的日期相关类(java.time.*);
测试代码如下:

public class Demo {

    public static void main(String[] args) {
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

        OffsetDateTime now = OffsetDateTime.now();
        System.out.println(now);
        byte[] serialize = serializer.serialize(now);
        OffsetDateTime deserialize = serializer.deserialize(serialize, OffsetDateTime.class);
        System.out.println(deserialize);
    }
}

代码运行过程中,会发生序列化错误,错误简要信息如下:

Exception in thread "main" org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.OffsetDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling;

问题分析

根据提示信息,加入jackson-datatype-jsr310依赖,如下:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

重新运行后,报错依然;
除了需要引入jackson-datatype-jsr310依赖外,还需要增加JavaTimeModule,代码如下:

public class Demo {

    public static void main(String[] args) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        OffsetDateTime now = OffsetDateTime.now();
        System.out.println(now);
        byte[] serialize = serializer.serialize(now);
        OffsetDateTime deserialize = serializer.deserialize(serialize, OffsetDateTime.class);
        System.out.println(deserialize);
    }
}

重新运行后,序列化报错问题解决了,但是发现,两次打印信息不一致,如下:

2022-04-20T20:57:09.910+08:00
2022-04-20T12:57:09.910Z

可以看出,经过序列化和反序列化后,时区信息丢失;
那么,此处就需要分析两个问题:

  1. 日期类如何序列化?
  2. 时区信息如何丢失?

日期类序列化

代码中,使用JavaTimeModule后,序列化器就支持日期类的序列化,那么直接分析JavaTimeModule做了啥,核心代码如下:

public JavaTimeModule()
{
    super(PackageVersion.VERSION);

    // First deserializers

    // // Instant variants:
    addDeserializer(Instant.class, InstantDeserializer.INSTANT);
    addDeserializer(OffsetDateTime.class, InstantDeserializer.OFFSET_DATE_TIME);
    addDeserializer(ZonedDateTime.class, InstantDeserializer.ZONED_DATE_TIME);

    // // Other deserializers
    addDeserializer(Duration.class, DurationDeserializer.INSTANCE);
    addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE);
    addDeserializer(LocalDate.class, LocalDateDeserializer.INSTANCE);
    addDeserializer(LocalTime.class, LocalTimeDeserializer.INSTANCE);
    addDeserializer(MonthDay.class, MonthDayDeserializer.INSTANCE);
    addDeserializer(OffsetTime.class, OffsetTimeDeserializer.INSTANCE);
    addDeserializer(Period.class, JSR310StringParsableDeserializer.PERIOD);
    addDeserializer(Year.class, YearDeserializer.INSTANCE);
    addDeserializer(YearMonth.class, YearMonthDeserializer.INSTANCE);
    addDeserializer(ZoneId.class, JSR310StringParsableDeserializer.ZONE_ID);
    addDeserializer(ZoneOffset.class, JSR310StringParsableDeserializer.ZONE_OFFSET);

    // then serializers:
    addSerializer(Duration.class, DurationSerializer.INSTANCE);
    addSerializer(Instant.class, InstantSerializer.INSTANCE);
    addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE);
    addSerializer(LocalDate.class, LocalDateSerializer.INSTANCE);
    addSerializer(LocalTime.class, LocalTimeSerializer.INSTANCE);
    addSerializer(MonthDay.class, MonthDaySerializer.INSTANCE);
    addSerializer(OffsetDateTime.class, OffsetDateTimeSerializer.INSTANCE);
    addSerializer(OffsetTime.class, OffsetTimeSerializer.INSTANCE);
    addSerializer(Period.class, new ToStringSerializer(Period.class));
    addSerializer(Year.class, YearSerializer.INSTANCE);
    addSerializer(YearMonth.class, YearMonthSerializer.INSTANCE);

    addSerializer(ZonedDateTime.class, ZonedDateTimeSerializer.INSTANCE);
    
    addSerializer(ZoneId.class, new ZoneIdSerializer());
    addSerializer(ZoneOffset.class, new ToStringSerializer(ZoneOffset.class));

    // key serializers
    addKeySerializer(ZonedDateTime.class, ZonedDateTimeKeySerializer.INSTANCE);

    // key deserializers
    addKeyDeserializer(Duration.class, DurationKeyDeserializer.INSTANCE);
    addKeyDeserializer(Instant.class, InstantKeyDeserializer.INSTANCE);
    addKeyDeserializer(LocalDateTime.class, LocalDateTimeKeyDeserializer.INSTANCE);
    addKeyDeserializer(LocalDate.class, LocalDateKeyDeserializer.INSTANCE);
    addKeyDeserializer(LocalTime.class, LocalTimeKeyDeserializer.INSTANCE);
    addKeyDeserializer(MonthDay.class, MonthDayKeyDeserializer.INSTANCE);
    addKeyDeserializer(OffsetDateTime.class, OffsetDateTimeKeyDeserializer.INSTANCE);
    addKeyDeserializer(OffsetTime.class, OffsetTimeKeyDeserializer.INSTANCE);
    addKeyDeserializer(Period.class, PeriodKeyDeserializer.INSTANCE);
    addKeyDeserializer(Year.class, YearKeyDeserializer.INSTANCE);
    addKeyDeserializer(YearMonth.class, YearMonthKeyDeserializer.INSTANCE);
    addKeyDeserializer(ZonedDateTime.class, ZonedDateTimeKeyDeserializer.INSTANCE);
    addKeyDeserializer(ZoneId.class, ZoneIdKeyDeserializer.INSTANCE);
    addKeyDeserializer(ZoneOffset.class, ZoneOffsetKeyDeserializer.INSTANCE);
}

好家伙,简单直接地引入了Java8的日期相关类(java.time.*)的序列化器、反序列化器;其中为OffsetDateTime引入了OffsetDateTimeSerializerOffsetDateTimeKeyDeserializer
解析来就分析serializer.serialize(now)如何找到OffsetDateTimeSerializer的?

查找序列化器

GenericJackson2JsonRedisSerializer.serialize代码如下:

public byte[] serialize(@Nullable Object source) throws SerializationException {

    if (source == null) {
        return SerializationUtils.EMPTY_ARRAY;
    }

    try {
        // 使用内部的ObjectMapper实例完成序列化
        return mapper.writeValueAsBytes(source);
    } catch (JsonProcessingException e) {
        throw new SerializationException("Could not write JSON: " + e.getMessage(), e);
    }
}

继续跟踪,会进入SerializerProvider.findTypedValueSerializer方法,代码如下:

public JsonSerializer<Object> findTypedValueSerializer(Class<?> valueType,
        boolean cache, BeanProperty property)
    throws JsonMappingException
{
    // 先从已知类型中查找
    JsonSerializer<Object> ser = _knownSerializers.typedValueSerializer(valueType);
    if (ser != null) {
        return ser;
    }
    // 再从序列化器缓存中查找
    ser = _serializerCache.typedValueSerializer(valueType);
    if (ser != null) {
        return ser;
    }

    // 还找不到,那么就根据valueType进行创建了
    ser = findValueSerializer(valueType, property);
    TypeSerializer typeSer = _serializerFactory.createTypeSerializer(_config,
            _config.constructType(valueType));
    if (typeSer != null) {
        typeSer = typeSer.forProperty(property);
        ser = new TypeWrappedSerializer(typeSer, ser);
    }
    if (cache) {
        _serializerCache.addTypedSerializer(valueType, ser);
    }
    return ser;
}

继续跟踪,来到创建序列化器的方法BeanSerializerFactory.createSerializer,代码如下:

public JsonSerializer<Object> createSerializer(SerializerProvider prov,
        JavaType origType)
    throws JsonMappingException
{
    // 获取序列化配置信息
    final SerializationConfig config = prov.getConfig();
    BeanDescription beanDesc = config.introspect(origType);
    // 尝试根据类上的注解找到序列化器
    JsonSerializer<?> ser = findSerializerFromAnnotation(prov, beanDesc.getClassInfo());
    if (ser != null) {
        // 找到则优先使用
        return (JsonSerializer<Object>) ser;
    }
    boolean staticTyping;
    // Next: we may have annotations that further indicate actual type to use (a super type)
    // 如果存在AnnotationIntrospector,则使用其找到相关注解标注的序列化类型
    // 默认为JacksonAnnotationIntrospector,注解为JsonSerialize
    final AnnotationIntrospector intr = config.getAnnotationIntrospector();
    JavaType type;

    if (intr == null) {
        type = origType;
    } else {
        try {
            type = intr.refineSerializationType(config, beanDesc.getClassInfo(), origType);
        } catch (JsonMappingException e) {
            return prov.reportBadTypeDefinition(beanDesc, e.getMessage());
        }
    }
    if (type == origType) {
        staticTyping = false;
    } else {
        staticTyping = true;
        if (!type.hasRawClass(origType.getRawClass())) {
            beanDesc = config.introspect(type);
        }
    }
    // 是否存在Convertor
    Converter<Object,Object> conv = beanDesc.findSerializationConverter();
    if (conv == null) {
        // 不存在,则直接创建并返回
        return (JsonSerializer<Object>) _createSerializer2(prov, type, beanDesc, staticTyping);
    }
    JavaType delegateType = conv.getOutputType(prov.getTypeFactory());
    
    if (!delegateType.hasRawClass(type.getRawClass())) {
        beanDesc = config.introspect(delegateType);
        ser = findSerializerFromAnnotation(prov, beanDesc.getClassInfo());
    }
    if (ser == null && !delegateType.isJavaLangObject()) {
        ser = _createSerializer2(prov, delegateType, beanDesc, true);
    }
    return new StdDelegatingSerializer(conv, delegateType, ser);
}

最终来到SimpleSerializers.findSerializer方法,代码如下:

public JsonSerializer<?> findSerializer(SerializationConfig config,
        JavaType type, BeanDescription beanDesc)
{
    Class<?> cls = type.getRawClass();
    ClassKey key = new ClassKey(cls);
    JsonSerializer<?> ser = null;
    
    // 是否是接口
    if (cls.isInterface()) {
        if (_interfaceMappings != null) {
            ser = _interfaceMappings.get(key);
            if (ser != null) {
                return ser;
            }
        }
    } else {
        if (_classMappings != null) {
            // 从_classMappings中查找对应的序列化器
            ser = _classMappings.get(key);
            if (ser != null) {
                return ser;
            }

            // 代码省略
            ...
        }
    }
    // 代码省略
    ...
    return null;
}

在该方法中,根据java.time.OffsetDateTime找到OffsetDateTimeSerializer,那么这个核心的_classMappings是如何赋值的呢?
其赋值流程如下:

  1. JavaTimeModule调用父类SimpleModuleaddSerializer方法增加了OffsetDateTime的序列化器 OffsetDateTimeSerializer.INSTANCE
  2. addSerializer使用内部SimpleSerializers实例缓存了类型和序列化器的映射关系;
  3. 这样在SimpleSerializers中,就可以根据类型找到对应的序列化器了;

接下来就是序列化器序列化的实现了;

序列化

序列化的入口为DefaultSerializerProvider._serialize,此时序列化器为OffsetDateTimeSerializer.INSTANCEOffsetDateTime类序列化真正的实现在InstantSerializerBase.serialize方法,代码如下:

public void serialize(T value, JsonGenerator generator, SerializerProvider provider) throws IOException
{
    if (useTimestamp(provider)) {
        if (useNanoseconds(provider)) {
            // 默认使用nanosecond timestamps
            generator.writeNumber(DecimalUtils.toBigDecimal(
                    getEpochSeconds.applyAsLong(value), getNanoseconds.applyAsInt(value)
            ));
            return;
        }
        generator.writeNumber(getEpochMillis.applyAsLong(value));
        return;
    }

    generator.writeString(formatValue(value, provider));
}

getEpochSecondsgetNanoseconds的赋值见OffsetDateTimeSerializer.INSTANCE,代码如下:

public static final OffsetDateTimeSerializer INSTANCE = new OffsetDateTimeSerializer();

protected OffsetDateTimeSerializer() {
    super(OffsetDateTime.class, dt -> dt.toInstant().toEpochMilli(),
            OffsetDateTime::toEpochSecond, OffsetDateTime::getNano,
            DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}


// InstantSerializerBase.java
protected InstantSerializerBase(Class<T> supportedType, ToLongFunction<T> getEpochMillis,
        ToLongFunction<T> getEpochSeconds, ToIntFunction<T> getNanoseconds,
        DateTimeFormatter formatter)
{
    super(supportedType, null);
    defaultFormat = formatter;
    this.getEpochMillis = getEpochMillis;
    this.getEpochSeconds = getEpochSeconds;
    this.getNanoseconds = getNanoseconds;
}

至此,OffsetDateTime的序列化流程就分析结束了。

时区信息丢失

时区信息丢失是发生在OffsetDateTime的反序列化过程中,简单分析其反序列化过程;
反序列化的入口为GenericJackson2JsonRedisSerializer.deserialize,代码如下:

public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {

    Assert.notNull(type,
            "Deserialization type must not be null! Please provide Object.class to make use of Jackson2 default typing.");

    if (SerializationUtils.isEmpty(source)) {
        return null;
    }

    try {
        // 使用内部的ObjectMapper实例完成反序列化
        return mapper.readValue(source, type);
    } catch (Exception ex) {
        throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
    }
}

继续跟踪来到ObjectMapper._readMapAndClose方法,代码如下:

protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
    throws IOException
{
    try (JsonParser p = p0) {
        final Object result;
        final DeserializationConfig cfg = getDeserializationConfig();
        final DefaultDeserializationContext ctxt = createDeserializationContext(p, cfg);
        JsonToken t = _initForReading(p, valueType);
        if (t == JsonToken.VALUE_NULL) {
            result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
        } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
            result = null;
        } else {
            // 找到反序列化器并进行反序列化
            result = ctxt.readRootValue(p, valueType,
                    _findRootDeserializer(ctxt, valueType), null);
            ctxt.checkUnresolvedObjectId();
        }
        if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
            _verifyNoTrailingTokens(p, ctxt, valueType);
        }
        return result;
    }
}

和序列化过程类似,反序列化也分为两步:

  1. 查找反序列化器;
  2. 反序列化;

查找反序列化器

和序列化器在SimpleSerializers.findSerializer方法中找到类似,反序列化器是在SimpleDeserializers.findBeanDeserializer方法中找到,代码如下:

public JsonDeserializer<?> findBeanDeserializer(JavaType type,
        DeserializationConfig config, BeanDescription beanDesc)
    throws JsonMappingException
{
    return _find(type);
}

private final JsonDeserializer<?> _find(JavaType type) {
    if (_classMappings == null) {
        return null;
    }
    // 从_classMappings中找到
    return _classMappings.get(new ClassKey(type.getRawClass()));
}

_classMappings赋值流程如下:

  1. JavaTimeModule调用父类SimpleModuleaddDeserializer方法增加了OffsetDateTime的序列化器 InstantDeserializer.OFFSET_DATE_TIME
  2. addDeserializer使用内部SimpleDeserializers实例缓存了类型和序列化器的映射关系;
  3. 这样在SimpleDeserializers中,就可以根据类型找到对应的序列化器了;

接下来就是反序列化器反序列化的实现了;

反序列化

序列化的入口为DefaultDeserializationContext.readRootValue,此时反序列化器为InstantDeserializer.OFFSET_DATE_TIMEOffsetDateTime类反序列化真正的实现在InstantDeserializer.deserialize方法,代码如下:

public T deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
    switch (parser.currentTokenId())
    {
        case JsonTokenId.ID_NUMBER_FLOAT:
            return _fromDecimal(context, parser.getDecimalValue());
        case JsonTokenId.ID_NUMBER_INT:
            return _fromLong(context, parser.getLongValue());
        case JsonTokenId.ID_STRING:
            return _fromString(parser, context, parser.getText());
        case JsonTokenId.ID_START_OBJECT:
            return _fromString(parser, context,
                    context.extractScalarFromObject(parser, this, handledType()));
        case JsonTokenId.ID_EMBEDDED_OBJECT:
            return (T) parser.getEmbeddedObject();

        case JsonTokenId.ID_START_ARRAY:
            return _deserializeFromArray(parser, context);
    }
    return _handleUnexpectedToken(context, parser, JsonToken.VALUE_STRING,
            JsonToken.VALUE_NUMBER_INT, JsonToken.VALUE_NUMBER_FLOAT);
}

protected T _fromDecimal(DeserializationContext context, BigDecimal value)
{
    FromDecimalArguments args =
        DecimalUtils.extractSecondsAndNanos(value, (s, ns) -> new FromDecimalArguments(s, ns, getZone(context)));
    return fromNanoseconds.apply(args);
}

fromNanoseconds的赋值见InstantDeserializer.OFFSET_DATE_TIME,代码如下:

public static final InstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME = new InstantDeserializer<>(
        OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME,
        OffsetDateTime::from,
        a -> OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId),
        a -> OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId),
        (d, z) -> (d.isEqual(OffsetDateTime.MIN) || d.isEqual(OffsetDateTime.MAX) ? d : d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime()))),
        true
);


// InstantDeserializer.java
protected InstantDeserializer(Class<T> supportedType,
        DateTimeFormatter formatter,
        Function<TemporalAccessor, T> parsedToValue,
        Function<FromIntegerArguments, T> fromMilliseconds,
        Function<FromDecimalArguments, T> fromNanoseconds,
        BiFunction<T, ZoneId, T> adjust,
        boolean replaceZeroOffsetAsZ)
{
    super(supportedType, formatter);
    this.parsedToValue = parsedToValue;
    this.fromMilliseconds = fromMilliseconds;
    this.fromNanoseconds = fromNanoseconds;
    this.adjust = adjust == null ? ((d, z) -> d) : adjust;
    this.replaceZeroOffsetAsZ = replaceZeroOffsetAsZ;
    _adjustToContextTZOverride = null;
}

时区设置

反序列化流程中,时区相关的处理就是getZone(context),该方法底层实现为BaseSettings.getTimeZone方法,代码如下:

public TimeZone getTimeZone() {
    TimeZone tz = _timeZone;
    // 默认情况下_timeZone为空
    return (tz == null) ? DEFAULT_TIMEZONE : tz;
}

private static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC");

_timeZone的赋值流程如下:

  1. ObjectMapper声明了常量DEFAULT_BASE,其_timeZone属性为空;
  2. ObjectMapper构造函数中,默认使用DEFAULT_BASE构建了SerializationConfigDeserializationConfig
  3. getZone(context)通过DeserializationContext内部的DeserializationConfig实例获取到时区信息,那么就会得到DEFAULT_TIMEZONE;从而丢失时区信息;

至此,OffsetDateTime的序列化流程就分析结束了。

问题解决

知道时区丢失的根源所在,那么问题自然引刃而解;
既然ObjectMapper默认没有设置时区信息,那么给ObjectMapper设置下当前时区信息便可,代码如下:

public ObjectMapper setTimeZone(TimeZone tz) {
    _deserializationConfig = _deserializationConfig.with(tz);
    _serializationConfig = _serializationConfig.with(tz);
    return this;
}

当通过ObjectMapper.setTimeZone设置时区信息时,会自动更新SerializationConfigDeserializationConfig的时区信息,这样在反序列化时就不会丢失时区信息了;

最终代码如下:

public class Demo {

    public static void main(String[] args) {
        ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new JavaTimeModule()).setTimeZone(TimeZone.getDefault());
        objectMapper.activateDefaultTyping(
        objectMapper.getPolymorphicTypeValidator(), DefaultTyping.NON_FINAL, As.PROPERTY);
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        OffsetDateTime now = OffsetDateTime.now();
        System.out.println(now);
        byte[] serialize = serializer.serialize(now);
        OffsetDateTime deserialize = serializer.deserialize(serialize, OffsetDateTime.class);
        System.out.println(deserialize);
    }
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/4764.html

(0)
小半的头像小半

相关推荐

极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!