SpringBoot如何對LocalDateTime進行格式化并解析
【1】格式化后臺傳給前端的日期
首先第一點需要知道的是springboot默認依賴的json框架是jackson。
當使用@ResponseBody注解返回json格式數(shù)據(jù)時就是該框架在起作用。

SpringBoot對Date/DateTime配置
如果字段屬性是Date而非LocalDateTime時,通常我們會在application.properties里面配置如下:
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 spring.jackson.serialization.write-dates-as-timestamps=false
如下圖所示,spring.jackson開頭的配置會被JacksonProperties類獲取進行使用。
當返回json格式的時候,Jackson就會根據(jù)配置文件中日期格式化的配置對結(jié)果進行處理。

但是如果字段屬性為LocalDateTime呢?這種配置就失去了作用。
第一種方式:配置localDateTimeSerializer
這時候建議配置如下:
/**
* Created by jianggc at 2020/7/1.
*/
@Configuration
public class LocalDateTimeSerializerConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
// localDateTime 序列化器
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
// localDateTime 反序列化器
@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(pattern));
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
// return new Jackson2ObjectMapperBuilderCustomizer() {
// @Override
// public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// jacksonObjectMapperBuilder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
// jacksonObjectMapperBuilder.deserializerByType(LocalDateTime.class,localDateTimeDeserializer());
// }
// };
//這種方式同上
return builder -> {
builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
builder.deserializerByType(LocalDateTime.class,localDateTimeDeserializer());
builder.simpleDateFormat(pattern);
};
}
}
第二種方式:@JsonFormat
這種配置方式自然是全局的,如果想針對某個字段特殊處理,可以在類字段上面添加注解@JsonFormat:
@JsonFormat( pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createdDate;
@JsonFormat( pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdTime;
【2】前臺傳String格式日期給后臺
如下所示,前臺傳參2020-08-30 11:11:11,后臺使用LocalDateTime 接收。
通常會報錯類似如下:
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type [java.lang.String] to type [java.time.LocalDateTime ]
很顯然是在參數(shù)綁定的時候沒有找到合適的轉(zhuǎn)換器把String轉(zhuǎn)換為對應(yīng)的格式。
① 配置全局的日期轉(zhuǎn)換器localDateTimeConvert
@Bean
public Converter<String, LocalDateTime> localDateTimeConvert() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = null;
try {
//2020-01-01 00:00:00
switch (source.length()){
case 10:
logger.debug("傳過來的是日期格式:{}",source);
source=source+" 00:00:00";
break;
case 13:
logger.debug("傳過來的是日期 小時格式:{}",source);
source=source+":00:00";
break;
case 16:
logger.debug("傳過來的是日期 小時:分鐘格式:{}",source);
source=source+":00";
break;
}
dateTime = LocalDateTime.parse(source, df);
} catch (Exception e) {
logger.error(e.getMessage(),e);
}
return dateTime;
}
};
}
實現(xiàn)原理簡要描述
在進行參數(shù)綁定的時候,會使用WebDataBinder對象。而創(chuàng)建WebDataBinder對象時,會遍歷DefaultDataBinderFactory.initializer,使用其WebBindingInitializer initializer對WebDataBinder對象進行初始化。
初始化方法具體可見ConfigurableWebBindingInitializer.initBinder(WebDataBinder binder),源碼如下:
public void initBinder(WebDataBinder binder) {
binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
if (this.directFieldAccess) {
binder.initDirectFieldAccess();
}
//設(shè)置messageCodesResolver
if (this.messageCodesResolver != null) {
binder.setMessageCodesResolver(this.messageCodesResolver);
}
//設(shè)置bindingErrorProcessor
if (this.bindingErrorProcessor != null) {
binder.setBindingErrorProcessor(this.bindingErrorProcessor);
}
//設(shè)置validator
if (this.validator != null && binder.getTarget() != null && this.validator.supports(binder.getTarget().getClass())) {
binder.setValidator(this.validator);
}
//設(shè)置conversionService
if (this.conversionService != null) {
binder.setConversionService(this.conversionService);
}
if (this.propertyEditorRegistrars != null) {
PropertyEditorRegistrar[] var2 = this.propertyEditorRegistrars;
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
PropertyEditorRegistrar propertyEditorRegistrar = var2[var4];
propertyEditorRegistrar.registerCustomEditors(binder);
}
}
}
而conversionService中包含了許多的convert-類型格式化器。在WebDataBinder進行參數(shù)綁定的時候就會使用不同的格式化器即不同的convert進行參數(shù)類型轉(zhuǎn)換。
關(guān)于參數(shù)綁定的過程,有興趣的可以跟蹤DataBinder.doBind方法,在這個過程中會對前臺傳輸?shù)闹颠M行類型轉(zhuǎn)換為目標參數(shù)需要的類型。自定義的localDateTimeConvert也是在這里被用到的。
如下所示前臺傳String格式給后臺參數(shù)endDate,參數(shù)類型為java.time.LocalDateTime。

找到我們自定義的converter


調(diào)用convert進行類型轉(zhuǎn)換:

可以看到轉(zhuǎn)換后的結(jié)果為:

② 配置日期格式化器
/**
* yyyy-MM-dd HH:mm:ss String-localDateTime
* @return
*/
@Bean
public Formatter<LocalDateTime> localDateTimeFormatter() {
return new Formatter<LocalDateTime>() {
@Override
public LocalDateTime parse(String text, Locale locale) throws ParseException {
return LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
@Override
public String print(LocalDateTime localDateTime, Locale locale) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return formatter.format(localDateTime);
}
};
}
自定義的格式化器會在SpringBoot啟動時自動化配置過程中被加入,具體可以參考如下代碼。
WebMvcAutoConfiguration.mvcConversionService:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
【3】convert是什么時候添加到ConversionService中的?
① SpringBoot啟動的時候運行run方法
其會走到SpringApplication.configureEnvironment方法處:
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
//從這里跟蹤
ConversionService conversionService = ApplicationConversionService.getSharedInstance();
environment.setConversionService((ConfigurableConversionService)conversionService);
}
this.configurePropertySources(environment, args);
this.configureProfiles(environment, args);
}
② 嘗試獲取ConversionService
ApplicationConversionService.getSharedInstance如下所示,這里可以看到其使用了設(shè)計模式中的懶漢式之雙重校驗鎖來獲取單例。
public static ConversionService getSharedInstance() {
ApplicationConversionService sharedInstance = sharedInstance;
if (sharedInstance == null) {
Class var1 = ApplicationConversionService.class;
synchronized(ApplicationConversionService.class) {
sharedInstance = sharedInstance;
if (sharedInstance == null) {
sharedInstance = new ApplicationConversionService();
sharedInstance = sharedInstance;
}
}
}
return sharedInstance;
}
③ 獲取ApplicationConversionService
繼續(xù)對象創(chuàng)建過程會發(fā)現(xiàn)其走到了configure處:
public ApplicationConversionService(StringValueResolver embeddedValueResolver) {
if (embeddedValueResolver != null) {
this.setEmbeddedValueResolver(embeddedValueResolver);
}
//我們從這里繼續(xù)跟進
configure(this);
}
這里我們順帶看一下ApplicationConversionService的類繼承示意圖(其不只是可以作為ConversionService還可以作為ConverterRegistry與FormatterRegistry):

④ ApplicationConversionService.configure
創(chuàng)建ApplicationConversionService時會對其進行配置,這里很重要。其會注入默認的Converter和Formatter:
public static void configure(FormatterRegistry registry) {
DefaultConversionService.addDefaultConverters(registry);
DefaultFormattingConversionService.addDefaultFormatters(registry);
addApplicationFormatters(registry);
addApplicationConverters(registry);
}
⑤ DefaultConversionService.addDefaultConverters
該方法執(zhí)行完,會添加52個類型轉(zhuǎn)換器:
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
addScalarConverters(converterRegistry);如下所示:
private static void addScalarConverters(ConverterRegistry converterRegistry) {
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry));
converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharsetConverter());
converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCurrencyConverter());
converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
}
這里會添加23個類型轉(zhuǎn)換器:

添加集合處理的類型轉(zhuǎn)換器(這里會添加17個類型轉(zhuǎn)換器):
public static void addCollectionConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
converterRegistry.addConverter(new MapToMapConverter(conversionService));
converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
converterRegistry.addConverter(new StringToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
converterRegistry.addConverter(new StringToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
converterRegistry.addConverter(new StreamConverter(conversionService));
}
⑥ addDefaultFormatters添加格式化器
/**
* Add formatters appropriate for most environments: including number formatters,
* JSR-354 Money & Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters,
* depending on the presence of the corresponding API on the classpath.
* @param formatterRegistry the service to register default formatters with
*/
public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
// Default handling of number values
formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Default handling of monetary values
if (jsr354Present) {
formatterRegistry.addFormatter(new CurrencyUnitFormatter());
formatterRegistry.addFormatter(new MonetaryAmountFormatter());
formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
}
// Default handling of date-time values
// just handling JSR-310 specific date and time types
new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
if (jodaTimePresent) {
// handles Joda-specific types as well as Date, Calendar, Long
new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry);
}
else {
// regular DateFormat-based Date, Calendar, Long converters
new DateFormatterRegistrar().registerFormatters(formatterRegistry);
}
}
DateTimeFormatterRegistrar.registerFormatters
@Override
public void registerFormatters(FormatterRegistry registry) {
DateTimeConverters.registerConverters(registry);
DateTimeFormatter df = getFormatter(Type.DATE);
DateTimeFormatter tf = getFormatter(Type.TIME);
DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);
// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
registry.addFormatterForFieldType(LocalDate.class,
new TemporalAccessorPrinter(
df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
new TemporalAccessorParser(LocalDate.class, df));
registry.addFormatterForFieldType(LocalTime.class,
new TemporalAccessorPrinter(
tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf),
new TemporalAccessorParser(LocalTime.class, tf));
registry.addFormatterForFieldType(LocalDateTime.class,
new TemporalAccessorPrinter(
dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf),
new TemporalAccessorParser(LocalDateTime.class, dtf));
registry.addFormatterForFieldType(ZonedDateTime.class,
new TemporalAccessorPrinter(dtf),
new TemporalAccessorParser(ZonedDateTime.class, dtf));
registry.addFormatterForFieldType(OffsetDateTime.class,
new TemporalAccessorPrinter(dtf),
new TemporalAccessorParser(OffsetDateTime.class, dtf));
registry.addFormatterForFieldType(OffsetTime.class,
new TemporalAccessorPrinter(tf),
new TemporalAccessorParser(OffsetTime.class, tf));
registry.addFormatterForFieldType(Instant.class, new InstantFormatter());
registry.addFormatterForFieldType(Period.class, new PeriodFormatter());
registry.addFormatterForFieldType(Duration.class, new DurationFormatter());
registry.addFormatterForFieldType(Year.class, new YearFormatter());
registry.addFormatterForFieldType(Month.class, new MonthFormatter());
registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter());
registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());
registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
}
DateTimeConverters.registerConverters
public static void registerConverters(ConverterRegistry registry) {
DateFormatterRegistrar.addDateConverters(registry);
registry.addConverter(new LocalDateTimeToLocalDateConverter());
registry.addConverter(new LocalDateTimeToLocalTimeConverter());
registry.addConverter(new ZonedDateTimeToLocalDateConverter());
registry.addConverter(new ZonedDateTimeToLocalTimeConverter());
registry.addConverter(new ZonedDateTimeToLocalDateTimeConverter());
registry.addConverter(new ZonedDateTimeToOffsetDateTimeConverter());
registry.addConverter(new ZonedDateTimeToInstantConverter());
registry.addConverter(new OffsetDateTimeToLocalDateConverter());
registry.addConverter(new OffsetDateTimeToLocalTimeConverter());
registry.addConverter(new OffsetDateTimeToLocalDateTimeConverter());
registry.addConverter(new OffsetDateTimeToZonedDateTimeConverter());
registry.addConverter(new OffsetDateTimeToInstantConverter());
registry.addConverter(new CalendarToZonedDateTimeConverter());
registry.addConverter(new CalendarToOffsetDateTimeConverter());
registry.addConverter(new CalendarToLocalDateConverter());
registry.addConverter(new CalendarToLocalTimeConverter());
registry.addConverter(new CalendarToLocalDateTimeConverter());
registry.addConverter(new CalendarToInstantConverter());
registry.addConverter(new LongToInstantConverter());
registry.addConverter(new InstantToLongConverter());
}
DateFormatterRegistrar.addDateConverters
public static void addDateConverters(ConverterRegistry converterRegistry) {
converterRegistry.addConverter(new DateToLongConverter());
converterRegistry.addConverter(new DateToCalendarConverter());
converterRegistry.addConverter(new CalendarToDateConverter());
converterRegistry.addConverter(new CalendarToLongConverter());
converterRegistry.addConverter(new LongToDateConverter());
converterRegistry.addConverter(new LongToCalendarConverter());
}
⑦ addApplicationFormatters(registry)
添加全局格式化器:
public static void addApplicationFormatters(FormatterRegistry registry) {
registry.addFormatter(new CharArrayFormatter());
registry.addFormatter(new InetAddressFormatter());
registry.addFormatter(new IsoOffsetFormatter());
}
⑧ addApplicationConverters(registry)
添加全局類型轉(zhuǎn)換器:
public static void addApplicationConverters(ConverterRegistry registry) {
addDelimitedStringConverters(registry);
registry.addConverter(new StringToDurationConverter());
registry.addConverter(new DurationToStringConverter());
registry.addConverter(new NumberToDurationConverter());
registry.addConverter(new DurationToNumberConverter());
registry.addConverter(new StringToDataSizeConverter());
registry.addConverter(new NumberToDataSizeConverter());
registry.addConverter(new StringToFileConverter());
registry.addConverterFactory(new LenientStringToEnumConverterFactory());
registry.addConverterFactory(new LenientBooleanToEnumConverterFactory());
}
public static void addDelimitedStringConverters(ConverterRegistry registry) {
ConversionService service = (ConversionService)registry;
registry.addConverter(new ArrayToDelimitedStringConverter(service));
registry.addConverter(new CollectionToDelimitedStringConverter(service));
registry.addConverter(new DelimitedStringToArrayConverter(service));
registry.addConverter(new DelimitedStringToCollectionConverter(service));
}
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Cloud 服務(wù)網(wǎng)關(guān)Zuul的實現(xiàn)
這篇文章主要介紹了Spring Cloud 服務(wù)網(wǎng)關(guān)Zuul的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07
SpringBoot使用Feign進行服務(wù)間通信的實現(xiàn)示例代碼
Feign是一個開源的Java HTTP客戶端,可以幫助我們在SpringBoot應(yīng)用中快速構(gòu)建和使用HTTP客戶端,方便實現(xiàn)服務(wù)間的通信,本文就來介紹一下SpringBoot使用Feign進行服務(wù)間通信的實現(xiàn)示例代碼,感興趣的可以了解一下2024-01-01
Java實現(xiàn)Android拼圖游戲設(shè)計過程解析
這篇文章主要介紹了Java實現(xiàn)Android拼圖游戲設(shè)計過程解析,下面文章要接受的這是一款基于 Java 開發(fā)的移動端安卓小游戲,可以作為大家在學習期間的一個小練習,接下來和小編一起進入文章學習具體內(nèi)容吧2022-02-02
SpringBoot實現(xiàn)OneDrive文件上傳的詳細步驟
這篇文章主要介紹了SpringBoot實現(xiàn)OneDrive文件上傳的詳細步驟,文中通過代碼示例和圖文講解的非常詳細,對大家實現(xiàn)OneDrive文件上傳有一定的幫助,需要的朋友可以參考下2024-02-02
可視化Swing中JTable控件綁定SQL數(shù)據(jù)源的兩種方法深入解析
以下是對可視化Swing中JTable控件綁定SQL數(shù)據(jù)源的兩種方法進行了詳細的分析介紹,需要的朋友可以過來參考一下2013-07-07
Java數(shù)據(jù)結(jié)構(gòu)中堆的向下和向上調(diào)整解析
堆是一顆完全二叉樹,在這棵樹中,所有父節(jié)點都滿足大于等于其子節(jié)點的堆叫大根堆,所有父節(jié)點都滿足小于等于其子節(jié)點的堆叫小根堆。堆雖然是一顆樹,但是通常存放在一個數(shù)組中,父節(jié)點和孩子節(jié)點的父子關(guān)系通過數(shù)組下標來確定2021-11-11
如何解決SpringBoot啟動時無法加載配置文件或環(huán)境變量問題
文章主要介紹了在Spring Boot項目中遇到配置文件加載失敗和資源目錄圖標異常的問題,并提供了詳細的解決步驟,解決方法包括在pom.xml文件中添加特定配置,確保資源目錄順序正確,以及注意節(jié)點的正確使用,通過這些步驟,可以有效解決資源加載問題,提高開發(fā)效率2024-12-12

