JSR310-LocalDateTime序列化 & 反序列化

Aboruo發表於2021-12-23

問題

springboot 版本:spring-boot 2.3.12

今天在開發一個redis 熱key服務端的過程中,碰到2個問題:

  1. jdk8的LocalDateTime,LocalDate,LocalTime型別的欄位在序列化,會被序列成["2021","12","22","18","56","40"]這樣的陣列;
  2. 服務接受請求LocalDateTime型別的引數時,要求引數為 "2021-12-22T18:56",中間加"T"(ISO-8601) 才能夠正常實現反序列化,yyyy-MM-dd HH:mm:ss 格式的字串反序列化會報異常,異常資訊如下:

org.springframework.http.converter.HttpMessageNotReadableException: Invalid JSON input: Cannot deserialize value of type `java.time.LocalDateTime` from String "2021-12-22 18:56:40": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2020-05-04 00:00' could not be parsed at index 10; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDateTime` from String "2021-12-22 18:56:40": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '2021-12-22 18:56:40' could not be parsed at index 10 // 省略部分異常資訊 Caused by: java.time.format.DateTimeParseException: Text '2021-12-22 18:56:40' could not be parsed at index 10 

系統預設的序列化&反序列化方式給人感覺比較反人類,給所有功能相關都會帶來困惑和額外的轉化工作量,需要使用一種更符合大家使用習慣的方式解決一下。

方案職責的定位

LocalDateTime序列化&反序列化的使用應該是應用服務的共性問題,發揮作用的層次在springmvc 的HttpMessageConverter層次,個人想法-解決方案應該放在基礎框架或腳手架層次(如果所在公司又自己的框架或腳手架),這樣所有使用框架及腳手架的應用都會因此受益。

解決過程

定位問題

spring boot 在mvc請求的處理過程中,負責json 格式序列化和反序列化的是Jackson*HttpMessageConverter,具體哪個類定位一下即可,所有的請求response在spring boot裡統一在RequestResponseBodyMethodProcessor這個類的handleReturnValue方法中進行的。

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

        mavContainer.setRequestHandled(true);
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

        //這裡即是使用各種MessageConverter對返回的物件進行序列化處理的地方
        writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
    }

 順著writeWithMessageConverters方法繼續往下debug,最終找到了Jackson的converter -> AbstractJackson2HttpMessageConverter的withInternal方法。

    @Override
    protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {

        MediaType contentType = outputMessage.getHeaders().getContentType();
        JsonEncoding encoding = getJsonEncoding(contentType);

        OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
        JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding);
        try {
            //此處忽略不相關的程式碼
            
            //此處即是將返回物件序列化的objectWriter
            ObjectWriter objectWriter = (serializationView != null ?
                    this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
            if (filters != null) {
                objectWriter = objectWriter.with(filters);
            }
            if (javaType != null && javaType.isContainerType()) {
                objectWriter = objectWriter.forType(javaType);
            }
            SerializationConfig config = objectWriter.getConfig();
            if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
                    config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
                objectWriter = objectWriter.with(this.ssePrettyPrinter);
            }
            //此處開始進行序列化
            objectWriter.writeValue(generator, value);

            writeSuffix(generator, object);
            generator.flush();
            generator.close();
        }
        catch (InvalidDefinitionException ex) {
            throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
        }
        catch (JsonProcessingException ex) {
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
        }
    }            

 objectWriter是真正執行序列化的,它是由ObjectMapper來建立的,再來看以下ObjectMapper,

 關鍵的序列化元件由_serializerFactory提供,由內部提供了很多型別物件序列化支撐。

 再繼續跟蹤,最終進入了LocalDateTime型別的序列化類->LocalDateTimeSerializer,通過serialize方法進行序列化,在包裡我們還可以看到很多JSR310的日期型別的序列化類。

再來看serialize方法,其中有一個很重要的邏輯->useTimestamp方法,在父類JSR310FormattedSerializerBase中實現,單獨摘出來

protected boolean useTimestamp(SerializerProvider provider) {
        if (_useTimestamp != null) {
            return _useTimestamp.booleanValue();
        }
        if (_shape != null) {
            if (_shape == Shape.STRING) {
                return false;
            }
            if (_shape == Shape.NUMBER_INT) {
                return true;
            }
        }
        //這裡讓人眼前一亮,意味著可以通過外接擴充套件的方式來給一個配置好的_formatter
        return (_formatter == null) && (provider != null)
                && provider.isEnabled(getTimestampsFeature());
    }

 這個方法預設返回true,預設_formatter是null,provider.isEnabled也是true,這裡我們應該已經找到解題之道了—我們可以試圖給-LocalDateTimeSerializer一個定義好的_formatter,或者給定配置讓它生成一個formatter.

整理一下呼叫鏈路

 整理一下思路:LocalDateTimeSerializer->...->DefaultSerializerProvider->Prefetch->ObjectWritter->ObjectMapper

看一下不起眼的Prefetch的關鍵方法-_serializerProvider

protected DefaultSerializerProvider _serializerProvider() {
    return _serializerProvider.createInstance(_config, _serializerFactory);
}

通過此方法將ObjectWritter中的_serializerFactory來建立一個預設的DefaultSerializerProvider。

好了,回過頭來溯源已經清楚了,其實還是ObjectMapper來傳入進去的。那麼我們就研究在ObjectMapper建立時如何做一下擴充套件,將自己的擴充套件解決方案融入到ObjectMapper中就可以了。

解決方案

AbstractJackson2HttpMessageConverter的實現類MappingJackson2HttpMessageConverter,找到這個bean的建立位置

 

 再看一下ObjectMapper的建立位置

 

 如圖所示,MappingJackson2HtttMessageConverter的ObjectMapper在JacksonAutoConfiguration中建立的,並且通過Jackson2ObjectMapperBuilder 來建立的,這個類很重要,我們看一下這個build方法。

public <T extends ObjectMapper> T build() {
   ObjectMapper mapper;
   if (this.createXmlMapper) {
      mapper = (this.defaultUseWrapper != null ?
            new XmlObjectMapperInitializer().create(this.defaultUseWrapper, this.factory) :
            new XmlObjectMapperInitializer().create(this.factory));
   }
   else {
      mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper());
   }
   //這裡的configure就是配置mapper的,做包括序列在內的各種配置的
   configure(mapper);
   return (T) mapper;
}

configure方法:

public void configure(ObjectMapper objectMapper) {
        Assert.notNull(objectMapper, "ObjectMapper must not be null");

        MultiValueMap<Object, Module> modulesToRegister = new LinkedMultiValueMap<>();
        if (this.findModulesViaServiceLoader) {
            ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> registerModule(module, modulesToRegister));
        }
        else if (this.findWellKnownModules) {
            registerWellKnownModulesIfAvailable(modulesToRegister);
        }

        if (this.modules != null) {
            this.modules.forEach(module -> registerModule(module, modulesToRegister));
        }
        if (this.moduleClasses != null) {
            for (Class<? extends Module> moduleClass : this.moduleClasses) {
                registerModule(BeanUtils.instantiateClass(moduleClass), modulesToRegister);
            }
        }
        List<Module> modules = new ArrayList<>();
        for (List<Module> nestedModules : modulesToRegister.values()) {
            modules.addAll(nestedModules);
        }
        objectMapper.registerModules(modules);
               //此處省去不關注的程式碼
}

這個Module是個抽象類,有這麼多實現,JavaTimeModule就是時間相關的模組

  看一下JavaTimeModule說明

Class that registers capability of serializing java.time objects with the Jackson core.

ObjectMapper mapper = new ObjectMapper();

mapper.registerModule(new JavaTimeModule());

 

Note that as of 2.x, if auto-registering modules, this package will register legacy version, JSR310Module, and NOT this module. 3.x will change the default. Legacy version has the same functionality, but slightly different default configuration: see JSR310Module for details.

再看一下JavaTimeModule的核心程式碼,它的構造方法里加入了這麼多的序列化及反序列化類,不過是都是預設的,我們需要使用JavaTimeModule建立一個我們需要的類。

public final class JavaTimeModule extends SimpleModule
{
    private static final long serialVersionUID = 1L;

    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);

 

回過頭來繼續看Jackson2ObjectMapperBuilder的建立,在JacksonAutoConfiguration中建立

@Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
    static class JacksonObjectMapperBuilderConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
                List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
            //這裡有Jackson2ObjectMapperBuilderCustomizer,意味著我們可以通過自定義它的Cutomizer,
            //來做一下個性化擴充套件
            Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
            builder.applicationContext(applicationContext);
            customize(builder, customizers);
            return builder;
        }

        private void customize(Jackson2ObjectMapperBuilder builder,
                List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
            for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
                customizer.customize(builder);
            }
        }

    }

熟悉spring 設計風格的同學應該知道,這裡的Customizer 類似於RestTemplateBuilder中的customizer,是方便我們做擴充套件用的,這就是spring 牛逼之處,再次深深膜拜一下,瞭解到這裡幾乎可以確定,我們擴充套件一個Jackson2ObjectMapperBuilderCustomizer的實現,就可以達成我們的目的了。

程式碼實現

給出程式碼設計

import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.CollectionUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;

/**
 * @author zhaoxinbo
 * @name: ApplicationWebConfig
 * @description: web配置類
 * @date 2021/12/2120:40
 */
@Configuration
public class ApplicationWebConfig {
    
    private static final String STANDARD_LOCAL_DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss SSS";

    private static final String STANDARD_LOCAL_DATE_FORMAT = "yyyy-MM-dd";

    private static final String STANDARD_LOCAL_TIME_FORMAT = "HH:mm:ss SSS";

    @Bean
    public LocalJackson2ObjectMapperBuilderCustomizer localJackson2ObjectMapperBuilderCustomizer(ApplicationContext applicationContext,
                                                                                                 JacksonProperties jacksonProperties) {
        return new LocalJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
    }

    /**
     * 建立應用自己的JavaTimeModule
     * <p>
     *     1. Jackson預設不會建立spring 容器管理的JavaTimeModule,我們可以建立這樣一個例項去覆蓋系統預設的;
     *     2. 在此我們可以自定義JSR310日期的序列化&反序列化物件
     * </p>
     * @return
     */
    @Bean
    public JavaTimeModule javaTimeModule() {
        JavaTimeModule module = new JavaTimeModule();
        /** serializers */
        LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_DATETIME_FORMAT));
        LocalDateSerializer localDateSerializer = new LocalDateSerializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_DATE_FORMAT));
        LocalTimeSerializer localTimeSerializer = new LocalTimeSerializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_TIME_FORMAT));
        module.addSerializer(LocalDateTime.class, localDateTimeSerializer);
        module.addSerializer(LocalDate.class, localDateSerializer);
        module.addSerializer(LocalTime.class, localTimeSerializer);

        /** deserializers */
        LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_DATETIME_FORMAT));
        LocalDateDeserializer localDateDeserializer = new LocalDateDeserializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_DATE_FORMAT));
        LocalTimeDeserializer localTimeDeserializer = new LocalTimeDeserializer(DateTimeFormatter.ofPattern(STANDARD_LOCAL_TIME_FORMAT));
        module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
        module.addDeserializer(LocalDate.class, localDateDeserializer);
        module.addDeserializer(LocalTime.class, localTimeDeserializer);
        return module;
    }
    
    /**
     * <p>
     *     1. 自定義Jackson2ObjectMapperBuilderCustomizer;
     *     2. 將自定義建立JavaTimeModule配置在 Jackson2ObjectMapperBuilder 中
     *     3. JacksonAutoConfiguration 中在建立ObjectMapper時就會把我們自己的JavaTimeModule初始化為對應的Serializer了
     * </p>
     */
    static final class LocalJackson2ObjectMapperBuilderCustomizer
            implements Jackson2ObjectMapperBuilderCustomizer, Ordered {

        private final ApplicationContext applicationContext;

        private final JacksonProperties jacksonProperties;

        LocalJackson2ObjectMapperBuilderCustomizer(ApplicationContext applicationContext,
                                                      JacksonProperties jacksonProperties) {
            this.applicationContext = applicationContext;
            this.jacksonProperties = jacksonProperties;
        }

        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE - 100;
        }

        @Override
        public void customize(Jackson2ObjectMapperBuilder builder) {

            if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
                builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
            }
            if (this.jacksonProperties.getTimeZone() != null) {
                builder.timeZone(this.jacksonProperties.getTimeZone());
            }
            configureModules(builder);
        }

        /**
         * 將自定義建立JavaTimeModule配置在 Jackson2ObjectMapperBuilder
         * @param builder
         */
        private void configureModules(Jackson2ObjectMapperBuilder builder) {
            Collection<Module> modules = getBeans(this.applicationContext, Module.class);
            if(CollectionUtils.isEmpty(modules)) {
                return;
            }
            builder.modulesToInstall(modules.toArray(new Module[0]));
        }

        private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory, Class<T> type) {
            return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type).values();
        }

    }
}

 

效果展示

反序列化

發起請求

url:/worker/test/queryByTime

method: post

請求引數:

{
    "updateTime": "2021-12-22 17:12:47 599",
    "appName": "app-worker"
}
結果展示

 

  

序列化

發起請求

url:127.0.0.1:9000/worker/node/query?appName=app-worker

method: get

結果展示

{
    "code": "00000",
    "data": [
        {
            "id": 507,
            "appName": "app-worker",
            "ip": "10.255.22.204",
            "port": 11111,
            "createTime": "2021-12-22 17:12:47 000",
            "updateTime": "2021-12-23 11:10:34 000"
        },
        {
            "id": 511,
            "appName": "app-worker",
            "ip": "172.20.99.148",
            "port": 11111,
            "createTime": "2021-12-23 11:03:26 000",
            "updateTime": "2021-12-23 11:10:35 000"
        }
    ]
}

其他實現方案

在JSR310屬性上使用@Jsonformat註解,在看程式碼過程中看到這種解決方式,不過所有JSR310屬性都要加,不滿足要求,這種方式我並未做驗證,有空閒時間了可以做一下驗證。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

相關文章