LocalDateTimeJava 8开始提供的时间日期API,主要用来优化Java 8以前对于时间日期的处理操作。在使用Spring Boot的时候,往往会发现请求参数或返回结果中含有LocalDateTimeJava 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类型默认序列化结果

环境:在springboot1springboot版本为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)]

java 时序图用什么软件画 java 时间序列_java 时序图用什么软件画

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 自带的注解,而@JsonFormatjackson 的注解。
  • @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、需要配置objectMappejava8时间支持,即objectMapper.registerModule(javaTimeModule);

七、springboot2中对时间支持的变化

环境:在springboot2springboot版本为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时间输出

java 时序图用什么软件画 java 时间序列_java 时序图用什么软件画_02


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XutywE7a-1576739340526)(D:\markdown\springboot2.PNG)]2、在JacksonAutoConfiguration自动配置类修改了SerializationFeature.WRITE_DATES_AS_TIMESTAMPS值为false,如下图:

java 时序图用什么软件画 java 时间序列_序列化_03

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VDFqTHLc-1576739340527)(D:\markdown\时间圈自动配置.PNG)]

3、Date序列化实现如下,从源代码跟进去就可以看出输出UTC时间字符串。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J08Sm03B-1576739340528)(D:\markdown\Date序列化.PNG)]

java 时序图用什么软件画 java 时间序列_java 时序图用什么软件画_04

4、对于LocalDateTime类型时间,从上面分析中可以知道输出ISO格式时间字符串。

5、springboot1springboot2兼容性考虑:

我们知道Date类型在springboot1中序列化为时间圈,为了保证springboot1环境升级到springboot2环境下,时间类型数据能够正常序列化和反序列化,只需要在application.yml中增加如下配置:

spring:
  jackson:
    serialization: 
      WRITE_DATES_AS_TIMESTAMPS: true

这样springboot2环境下仍能以时间圈格式输出。