作者:杨绿寒轻 链接:https://juejin.cn/post/6844904177479450632 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着Date
和SimpleDateFormat
进行时间操作。SimpleDateFormat
不是线程安全的,而Date
处理时间很麻烦,所以Java8提供了LocalDateTime
、LocalDate
和LocalTime
等全新的时间操作API。无论是Date
还是LocalDate
,在开发Spring Boot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat
注解来接收前端传值与日期字段绑定,加上@JsonFormat
注解来让返回前端的日期字段格式化成我们想要的时间格式。时间和日期类型在开发中使用的频率是非常高的,如果每个字段都加上这两个注解的话是非常繁琐的,有没有一种全局设置的处理方式呢?今天就来向大家介绍一下。
注:本文基于Springboot2.3.0版本。
根据不同的请求方式需要做不同的配置,下文中分为了JSON方式传参和GET请求及POST表单方式传参两种情况。
为什么需要LocalDate、LocalTime、LocalDateTime
主要有三个原因:
Date
打印出的日期可读性差SimpleDateFormat
线程不安全- Date对时间处理比较麻烦
下面详细进行说明: 1、Date如果不格式化,打印出的日期可读性差
Tue Sep 10 09:34:04 CST 2019
**2、**使用SimpleDateFormat
对时间进行格式化,但SimpleDateFormat
是线程不安全的 SimpleDateFormat
的format
方法最终调用代码:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar
是共享变量,并且这个共享变量没有做线程安全控制。当多个线程同时使用相同的SimpleDateFormat对象【如用static
修饰的SimpleDateFormat
】调用format
方法时,多个线程会同时调用calendar.setTime
方法,可能一个线程刚设置好time值另外的一个线程马上把设置的time值给修改了导致返回的格式化时间可能是错误的。在多并发情况下使用SimpleDateFormat
需格外注意 SimpleDateFormat
除了format
是线程不安全以外,parse
方法也是线程不安全的。parse
方法实际调用alb.establish(calendar).getTime()
方法来解析,alb.establish(calendar)
方法里主要完成了
- 重置日期对象cal的属性值
- 使用calb中中属性设置cal
- 返回设置好的cal对象
但是这三步不是原子操作。
那么多线程并发条件下使用**SimpleDateFormat**
如何保证线程安全:
- 避免线程之间共享一个
SimpleDateFormat
对象,每个线程使用时都创建一次SimpleDateFormat
对象 => 创建和销毁对象的开销大 - 对使用
format
和parse
方法的地方进行加锁 => 线程阻塞性能差 - 使用
ThreadLocal
保证每个线程最多只创建一次SimpleDateFormat
对象 => 较好的方法
JSON方式传参
这种情况指的是类型POST,Content-Type 是application/json 方式的请求。对于这类请求,controller中需要加上@RequestBody
注解来标注到我们用来接收请求参数的局部变量上,代码如下:
@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDateLearningApplication.class, args);
}
/**
* DateTime格式化字符串
*/
private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* Date格式化字符串
*/
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
/**
* Time格式化字符串
*/
private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";
public static class DateEntity {
private LocalDate date;
private LocalDateTime dateTime;
private Date originalDate;
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public LocalDateTime getDateTime() {
return dateTime;
}
public void setDateTime(LocalDateTime dateTime) {
this.dateTime = dateTime;
}
public Date getOriginalDate() {
return originalDate;
}
public void setOriginalDate(Date originalDate) {
this.originalDate = originalDate;
}
}
@RequestMapping("/date")
public DateEntity getDate(@RequestBody DateEntity dateEntity) {
return dateEntity;
}
}
复制代码
假设默认的接收和返回值的格式都是yyyy-MM-dd HH:mm:ss
,可以有以下几个方案。
配置application.yml 文件
在application.yml文件中配置上如下内容:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
小结:
- 支持Content-Type 是application/json的POST请求,请求参数字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss
如果请求参数是其他格式,如yyyy-MM-dd
字符串则报400 Bad Request异常。 - 不支持LocalDate等Java8日期API。
增加Jackson配置
/**
* Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder
.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
.serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
.deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
;
}
复制代码
小结:
- 支持Content-Type 是application/json的POST请求,请求参数字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss
如果请求参数是其他格式,如yyyy-MM-dd
字符串则报400 Bad Request异常。 - 支持LocalDate等Java8日期API。
PS:上面的方式是通过配置一个
Jackson2ObjectMapperBuilderCustomizer
Bean完成的,除了这种,也可以通过自定义一个MappingJackson2HttpMessageConverter
来实现。
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// 指定时区
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
// 日期类型字符串处理
objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));
// Java8日期日期处理
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
objectMapper.registerModule(javaTimeModule);
converter.setObjectMapper(objectMapper);
return converter;
}
复制代码
以上几种方式都可以实现JSON传参时的全局化配置,更推荐后两种代码中增加配置bean的方式,可以同时支持Date
和LocalDate
。
GET请求及POST表单方式传参
这种方式和上面的JSON方式,在Spring Boot处理的方式是完全不同的。上一种JSON方式传参是在HttpMessgeConverter
中通过jackson的ObjectMapper
将http请求体转换成我们写在controller中的参数对象的,而这种方式用的是Converter
接口(spring-core中定义的用于将源类型(一般是String
)转成目标类型的接口),两者是有本质区别的。
自定义参数转换器(Converter)
自定义一个参数转换器,实现上面提到的org.springframework.core.convert.converter.Converter
接口,在配置类里配置上以下几个bean,示例如下:
@Bean
public Converter<String, Date> dateConverter() {
return new Converter<>() {
@Override
public Date convert(String source) {
SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
try {
return formatter.parse(source);
} catch (Exception e) {
throw new RuntimeException(String.format("Error parsing %s to Date", source));
}
}
};
}
@Bean
public Converter<String, LocalDate> localDateConverter() {
return new Converter<>() {
@Override
public LocalDate convert(String source) {
return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
}
};
}
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<>() {
@Override
public LocalDateTime convert(String source) {
return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
}
};
}
复制代码
同时把controller接口增加一些参数,可以发现在接口里单独用变量接收也是可以正常转换的。
@RequestMapping("/date")
public DateEntity getDate(
LocalDate date,
LocalDateTime dateTime,
Date originalDate,
DateEntity dateEntity) {
System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate);
return dateEntity;
}
复制代码
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
使用@DateTimeFormat
注解
和前面提到的一样,GET请求及POST表单方式也是可以用@DateTimeFormat
来处理的,单独在controller接口参数或者实体类属性中都可以使用,比如@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate
。注意,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即**@DateTimeFormat**
注解不生效,两种方式是不兼容的。
那么假如我们使用了自定义参数转换器,但是还是想兼容用yyyy-MM-dd
形式接受呢?我们可以把前面的dateConverter
改成用正则匹配方式,这样也不失为一种不错的解决方案,示例如下。
/**
* 日期正则表达式
*/
private static final String DATE_REGEX = "[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
/**
* 时间正则表达式
*/
private static final String TIME_REGEX = "(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d";
/**
* 日期和时间正则表达式
*/
private static final String DATE_TIME_REGEX = DATE_REGEX + "\s" + TIME_REGEX;
/**
* 13位时间戳正则表达式
*/
private static final String TIME_STAMP_REGEX = "1\d{12}";
/**
* 年和月正则表达式
*/
private static final String YEAR_MONTH_REGEX = "[1-9]\d{3}-(0[1-9]|1[0-2])";
/**
* 年和月格式
*/
private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
@Bean
public Converter<String, Date> dateConverter() {
return new Converter<String, Date>() {
@SuppressWarnings("NullableProblems")
@Override
public Date convert(String source) {
if (StrUtil.isEmpty(source)) {
return null;
}
if (source.matches(TIME_STAMP_REGEX)) {
return new Date(Long.parseLong(source));
}
DateFormat format;
if (source.matches(DATE_TIME_REGEX)) {
format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
} else if (source.matches(DATE_REGEX)) {
format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
} else if (source.matches(YEAR_MONTH_REGEX)) {
format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
} else {
throw new IllegalArgumentException();
}
try {
return format.parse(source);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
};
}
复制代码
小结:
- GET请求及POST表单方式请求,但是需要在每个使用的地方加上
@DateTimeFormat
注解。 - 与自定义参数转化器(Converter)不兼容。
- 支持LocalDate等Java8日期API。
使用@ControllerAdvice
配合@initBinder
/*
* 在类上加上@ControllerAdvice
*/
@ControllerAdvice
@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {
...
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
}
});
binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
}
});
binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
}
});
binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
try {
setValue(formatter.parse(text));
} catch (Exception e) {
throw new RuntimeException(String.format("Error parsing %s to Date", text));
}
}
});
}
...
}
复制代码
在实际应用中,我们可以把上面代码放到父类中,所有接口继承这个父类,达到全局处理的效果。原理就是与AOP类似,在参数进入handler之前进行转换时使用我们定义的PropertyEditorSupport
来处理。
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
局部差异化处理
假设按照前面的全局日期格式设置的是:yyyy-MM-dd HH:mm:ss
,但是某个Date
类型的字段需要特殊处理成yyyy/MM/dd
格式来接收或者返回,有以下方案可以选择。
使用@DateTimeFormat
和@JsonFormat
注解
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date originalDate;
如上所示,可以在字段上增加@DateTimeFormat
和@JsonFormat
注解,可以分别单独指定该字段的接收和返回的日期格式。
PS:
@JsonFormat
和@DateTimeFormat
注解都不是Spring Boot提供的,在Spring应用中也可以使用。
再次提醒,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即**@DateTimeFormat**
注解不生效。
自定义序列化器和反序列化器
/**
* {@link Date} 序列化器
*/
public class DateJsonSerializer extends JsonSerializer<Date> {
@Override
public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws
IOException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
jsonGenerator.writeString(dateFormat.format(date));
}
}
/**
* {@link Date} 反序列化器
*/
public class DateJsonDeserializer extends JsonDeserializer<Date> {
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
try {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return dateFormat.parse(jsonParser.getText());
} catch (ParseException e) {
throw new IOException(e);
}
}
}
/**
* 使用方式
*/
@JsonSerialize(using = DateJsonSerializer.class)
@JsonDeserialize(using = DateJsonDeserializer.class)
private Date originalDate;
复制代码
如上所示,可以在字段上使用@JsonSerialize
和@JsonDeserialize
注解来指定在序列化和反序列化时使用我们自定义的序列化器和反序列化器。
最后再来个兼容JSON方式和GET请求及POST表单方式的完整的配置吧。
@Configuration
public class GlobalDateTimeConfig {
/**
* 日期正则表达式
*/
private static final String DATE_REGEX = "[1-9]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
/**
* 时间正则表达式
*/
private static final String TIME_REGEX = "(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d";
/**
* 日期和时间正则表达式
*/
private static final String DATE_TIME_REGEX = DATE_REGEX + "\s" + TIME_REGEX;
/**
* 13位时间戳正则表达式
*/
private static final String TIME_STAMP_REGEX = "1\d{12}";
/**
* 年和月正则表达式
*/
private static final String YEAR_MONTH_REGEX = "[1-9]\d{3}-(0[1-9]|1[0-2])";
/**
* 年和月格式
*/
private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
/**
* DateTime格式化字符串
*/
private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* Date格式化字符串
*/
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
/**
* Time格式化字符串
*/
private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
/**
* LocalDate转换器,用于转换RequestParam和PathVariable参数
*/
@Bean
public Converter<String, LocalDate> localDateConverter() {
return new Converter<String, LocalDate>() {
@SuppressWarnings("NullableProblems")
@Override
public LocalDate convert(String source) {
if (StringUtils.isEmpty(source)) {
return null;
}
return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
}
};
}
/**
* LocalDateTime转换器,用于转换RequestParam和PathVariable参数
*/
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@SuppressWarnings("NullableProblems")
@Override
public LocalDateTime convert(String source) {
if (StringUtils.isEmpty(source)) {
return null;
}
return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
}
};
}
/**
* LocalDate转换器,用于转换RequestParam和PathVariable参数
*/
@Bean
public Converter<String, LocalTime> localTimeConverter() {
return new Converter<String, LocalTime>() {
@SuppressWarnings("NullableProblems")
@Override
public LocalTime convert(String source) {
if (StringUtils.isEmpty(source)) {
return null;
}
return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
}
};
}
/**
* Date转换器,用于转换RequestParam和PathVariable参数
*/
@Bean
public Converter<String, Date> dateConverter() {
return new Converter<String, Date>() {
@SuppressWarnings("NullableProblems")
@Override
public Date convert(String source) {
if (StringUtils.isEmpty(source)) {
return null;
}
if (source.matches(TIME_STAMP_REGEX)) {
return new Date(Long.parseLong(source));
}
DateFormat format;
if (source.matches(DATE_TIME_REGEX)) {
format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
} else if (source.matches(DATE_REGEX)) {
format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
} else if (source.matches(YEAR_MONTH_REGEX)) {
format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
} else {
throw new IllegalArgumentException();
}
try {
return format.parse(source);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
};
}
/**
* Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.serializerByType(Long.class, ToStringSerializer.instance)
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
}
}