LocalDateTime
是Java 8
开始提供的时间日期API
,主要用来优化Java 8
以前对于时间日期的处理操作。在使用Spring Boot
的时候,往往会发现请求参数或返回结果中含有LocalDateTime
等Java 8
时间字段的时候会发生各种问题。下文就时间字段各种问题进行描述,并提供相关原理和解决方案。
举例的实体信息
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
@Data
public class Book {
private String id ;
private String name;
private Date createtime;
private LocalDateTime updatetime;
}
对应的controller请求
@GetMapping("/books")
public Book listBook() {
Book book =new Book();
book.setId("11");
book.setName("语文");
book.setCreatetime(new Date());
book.setUpdatetime(LocalDateTime.now());
return book;
}
1、Date
类型和LocalDateTime
类型默认序列化结果
环境:在springboot1
(springboot
版本为1.5.*及以下)版本环境下
一、 在浏览器上进行请求:http://localhost:8080/books
二、输出结果如下:
{"id":"11","name":"语文","createtime":1574733137204,"updatetime":{"month":"NOVEMBER","year":2019,"dayOfMonth":26,"dayOfWeek":"TUESDAY","dayOfYear":330,"monthValue":11,"hour":9,"minute":52,"nano":220000000,"second":17,"chronology":{"id":"ISO","calendarType":"iso8601"}}}
三、根据输出结果,可以得出以下结论:
Date
类型字段默序列化成时间圈。LocalDateTime
序列化时会当做一个对象,这样会将该对象所有所有字段进行输出。
主要是在Springboot1
版本中,Jackson
序列化框架没有定义LocalDateTime
是如何序列化的,那么会将LocalDateTime
当做一个对象来序列化,会采用BeanSerializerBase
来进行对象序列化,故能看到上述序列化结果。最大一个问题,构建反序列化json
字符串是比较困难的。
2、jackson-datatype-jsr310
包对提供了对Java 8
时间支持
一、引入jackson-datatype-jsr310
包依赖
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.8.11</version>
<scope>compile</scope>
</dependency>
二、再次请求:http://localhost:8080/books
三、输出结果如下:
{"id":"11","name":"语文","createtime":1574734194115,"updatetime":[2019,11,26,10,9,54,119000000]}
四、根据输出结果,可以得出以下结论:
LocalDateTime
此时会序列化成一个数组。
由于jsr310
依赖包的引入,LocalDateTime
会使用LocalDateTimeSerializer
类来进行序列化,默认时它会将LocalDateTime
序列化成数组。但是反序列化时,让客户端构造一个数组是比较困难的。
3、配置属性spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
作用
一、在springboot
配置文件中加入上述配置。
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
二、再次请求:http://localhost:8080/books
三、输出结果如下:
{"id":"11","name":"语文","createtime":"2019-11-26 02:32:56","updatetime":"2019-11-26T10:32:56.254"}
四、根据输出结果,可以得出以下结论:
- 上述配置会影响
Date
类型和LocalDateTime
类型序列化结果 spring.jackson.date-format
只对Date
类型字段起作用,但不会对LocalDateTime
类型字段起作用,但会影响LocalDateTime
序列化结果。
通过对LocalDateTime
源码可以看出,由于指定了字符串格式,则不会采用默认方式进行序列化,但是又没有指定LocalDateTime
序列化字符串格式,则采用ISO
标准格式进行序列化结果输出。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z4bHYj5G-1576739340523)(D:\markdown\LocalDateTime输出格式.PNG)]
4、输出统一字符串格式为yyyy-MM-dd HH:mm:ss
ISO
标准格式化字符串通常不符合我们使用字符串格式,人们通常喜欢使用yyyy-MM-dd HH:mm:ss
来表示时间。分两种情况:全局对象序列化配置(所有时间字段采用同一种格式)和局部对象序列化配置(这种配置方式更加灵活)。
一、全局对象序列化配置
上面提过spring.jackson.date-format
只能对Date
类型生效,要想对LocalDateTime
也生效,可以采用如下配置来指定LocalDateTime
的序列化和反序列化时候采用类对象:
@Configuration
public class JacksonConfig {
@Value("${spring.jackson.date-format:yyyy-MM-dd HH:mm:ss}")
private String pattern;
@Bean
public LocalDateTimeSerializer localDateTimeDeserializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(pattern));
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder.serializerByType(LocalDateTime.class, localDateTimeDeserializer() );
}
}
再次请求:http://localhost:8080/books,根据输出结果可以看出时间格式统一了。
{"id":"11","name":"语文","createtime":"2019-11-26 03:21:48","updatetime":"2019-11-26 11:21:48"}
二:局部对象序列化配置(通过 @JsonFormat
处理):
将时间类型通过注解来指定序列化字符串的格式
@Data
public class Book {
private String id ;
private String name;
@JsonFormat( pattern="yyyy-MM-dd HH:mm:ss")
private Date createtime;
@JsonFormat( pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatetime;
}
再次请求:http://localhost:8080/books,根据输出结果可以看出时间格式统一了。
{"id":"11","name":"语文","createtime":"2019-11-26 03:21:48","updatetime":"2019-11-26 11:21:48"}
5、@DateTimeFormat
和 @JsonFormat
注解比较
@DateTimeFormat
注解是Spring
自带的注解,而@JsonFormat
是jackson
的注解。@DateTimeFormat
主要用来处理表单请求时间参数类型,会将字符串类型时间转换成时间类型。而@JsonFormat
是处理Json
串中含有时间类型请求。
一、json
请求代码:
@PostMapping("/addBook")
public void addBookWithJson(@RequestBody Book book) {
System.out.println(book);
}
从代码可以看出,接收json
格式字符串请求参数。对于时间字段类型,需要@JsonFormat
来指定请求时间类型字符串格式。故在Book
实体上createtime
字段和updatetime
字段都要通过 @JsonFormat( pattern="yyyy-MM-dd HH:mm:ss")
注解来指明时间字段字符串格式。
请求路径为:http://localhost:8080/addBook,
对应输入json
字符串信息为:
{"id":"111", "createtime":"2013-10-11 12:20:23", "updatetime":"2013-10-11 12:20:20"}
二、表单请求代码
@PostMapping("/addBook1")
public void addBookWithForm(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) {
System.out.println(localDateTime);
}
请求url:http://localhost:8080/addBook1?localDateTime=2013-10-11 12:20:20,由于localDateTime
变量是请求参数,且为时间字段类型,故接收端需要通过@DateTimeFormat
注解完成将时间字段字符串转换成时间类型。
6、在redis
中时间字段序列化
在redis
中, 我们通常使用redisTemplate
来进行对象的存储和读取,其内部默认采用jdk
序列化机制来进行对象的序列化和反序列化。由于该序列化机制无论从数据可读性和效率性来说,性能都比较底下,大家通常会采用可读性和性能都不错json
来进行对象序列化和反序列化。为了避免每个对象都生成一个redisTemplate
对象,一般采用通用GenericJackson2JsonRedisSerializer
类对对象序列化和反序列化,由于GenericJackson2JsonRedisSerializer
序列化对象时候会在序列化结果中加入一个@class
属性,这样反序列化时可以通过@class
属性知道要反序列化对象的类型,从而能正常反序列化,从而避免为每个类型的对象定义一个resttemplate
对象。
看下面一段代码:
public static void main(String[] args) {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer
=new GenericJackson2JsonRedisSerializer();
Student s =new Student();
s.setName("张三");
s.setCreateTime(new Date());
s.setUpdateTime(LocalDateTime.now());
String str = new String(genericJackson2JsonRedisSerializer.serialize(s));
System.out.println(str);
}
输出结果如下:
{"@class":"com.hikvision.building.cloud.time.entity.Student","name":"张三","createTime":["java.util.Date",1575006137406],"updateTime":{"dayOfMonth":29,"dayOfWeek":"FRIDAY","dayOfYear":333,"month":"NOVEMBER","monthValue":11,"year":2019,"hour":13,"minute":42,"nano":511000000,"second":17,"chronology":{"@class":"java.time.chrono.IsoChronology","id":"ISO","calendarType":"iso8601"}}}
上面说过,jackson
默认序列化是不支持java8
中定义日期时间类等型,所以会按照对象方式输出。而我们希望时间按照以下格式字符串yyyy-MM-dd HH:mm:ss
输出,则按照以下步骤来。
一、引入jackson-datatype-jsr310
包依赖
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.8.11</version>
<scope>compile</scope>
</dependency>
二、定义ObjectMapper
对象,添加对Java8
时间支持:
private static ObjectMapper objectMapper = new ObjectMapper();
static {
// 排除值为空属性
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 转换成对象时,没有属性的处理,忽略掉
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
JavaTimeModule javaTimeModule = new JavaTimeModule();
//对Jdk8时间支持
objectMapper.registerModule(javaTimeModule);
}
三、重写GenericJackson2JsonRedisSerializer
类,提供对ObjectMapper
设置,默认GenericJackson2JsonRedisSerializer
类中定义ObjectMapper
是固定,不支持定制过的。
public class GenericJackson2JsonRedisSerializerCustomizer implements RedisSerializer<Object> {
private final ObjectMapper mapper;
public GenericJackson2JsonRedisSerializerCustomizer() {
this((String)null);
}
public GenericJackson2JsonRedisSerializerCustomizer(String classPropertyTypeName) {
this(new ObjectMapper(),classPropertyTypeName);
}
public GenericJackson2JsonRedisSerializerCustomizer(ObjectMapper mapper,
String classPropertyTypeName) {
Assert.notNull(mapper, "ObjectMapper must not be null!");
this.mapper = mapper;
this.mapper.registerModule((new SimpleModule()).addSerializer(new GenericJackson2JsonRedisSerializerCustomizer.NullValueSerializer(classPropertyTypeName)));
if (StringUtils.hasText(classPropertyTypeName)) {
this.mapper.enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.NON_FINAL, classPropertyTypeName);
} else {
this.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
}
}
public GenericJackson2JsonRedisSerializerCustomizer(ObjectMapper mapper) {
this(mapper,(String)null);
}
}
四、定义RedisTemplate
对象采用自定义的GenericJackson2JsonRedisSerializerCustomizer
来序列化。
@Bean
public RedisTemplate redisTemplate() {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory());
StringRedisSerializer redisKeySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializerCustomizer jackson2JsonRedisSerializer =
new GenericJackson2JsonRedisSerializerCustomizer(objectMapper);
redisTemplate.setKeySerializer(redisKeySerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(redisKeySerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
五、定义时间输出格式,由于采用json
序列化和反序列化,故采用 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
来指定输出字符串的格式。
@Data
public class Book {
private String id ;
private String name;
private Date createtime;
@JsonFormat( pattern="yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatetime;
}
注意事项:
1、ObjectMapper
定义和JsonUtil
工具类中定义ObjectMapper
不是同一个对象,一个需要序列化是输出带有@class
属性,一个不需要,不过带不带也没什么大的影响。
2、需要配置objectMappe
对java8
时间支持,即objectMapper.registerModule(javaTimeModule)
;
七、springboot2中对时间支持的变化
环境:在springboot2
(springboot
版本为2.0.*及以上)版本环境下,就上面1的例子
一、 在浏览器上进行请求:http://localhost:8080/books
二、输出结果如下:
{"id":"11","name":"语文","createTime":"2019-12-17T12:53:51.302+0000","updateTime":"2019-12-17T20:53:51.313"}
三、根据输出结果,可以得出以下结论:
Date
类型不在默认序列化成时间圈,而是序列化成字符串,且按照UTC
时间进行输出。LocalDateTime
类型能够正常序列化成字符串。
四、原因分析
1、引入了jsr310
包的依赖,这样就支持java8
时间输出
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XutywE7a-1576739340526)(D:\markdown\springboot2.PNG)]2、在JacksonAutoConfiguration
自动配置类修改了SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
值为false
,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VDFqTHLc-1576739340527)(D:\markdown\时间圈自动配置.PNG)]
3、Date
序列化实现如下,从源代码跟进去就可以看出输出UTC
时间字符串。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J08Sm03B-1576739340528)(D:\markdown\Date序列化.PNG)]
4、对于LocalDateTime
类型时间,从上面分析中可以知道输出ISO
格式时间字符串。
5、springboot1
和springboot2
兼容性考虑:
我们知道Date
类型在springboot1
中序列化为时间圈,为了保证springboot1
环境升级到springboot2
环境下,时间类型数据能够正常序列化和反序列化,只需要在application.yml
中增加如下配置:
spring:
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: true
这样springboot2
环境下仍能以时间圈格式输出。