SpringBoot中使用LocalDateTime踩坑记录

@[toc]

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。

本项目基于Java21和SpringBoot3开发,序列化工具使用的是默认的Jackson,使用Spring Data Redis操作Redis缓存。

在定义实体类过程中,日期时间类型的属性我使用了java.time包下的LocalDateLocalDateTime类,而没有使用java.util包下的Date类。

但在使用过程中遇到了一些问题,于是在此记录下来与诸位分享。

一、为什么推荐使用java.time包的LocalDateTime而不是java.util的Date?

LocalDateTime和Date是Java中表示日期和时间的两种不同的类,它们有一些区别和特点。

  • 类型:LocalDateTime是Java 8引入的新类型,属于Java 8日期时间API(java.time包)。而Date是旧版Java日期时间API(java.util包)中的类。
  • 不可变性:LocalDateTime是不可变的类型,一旦创建后,其值是不可变的,对该类对象的加减等计算操作不会修改原对象,而是会返回一个新的LocalDateTime对象。而Date是可变的类型,可以通过方法修改其值。
  • 线程安全性:LocalDateTime是线程安全的,多个线程可以同时访问和操作不同的LocalDateTime实例。而Date是非线程安全的,如果多个线程同时访问和修改同一个Date实例,可能会导致不可预期的结果。
  • 时间精度:LocalDateTime提供了纳秒级别的时间精度,可以表示更加精确的时间。而Date只能表示毫秒级别的时间精度。
  • 时区处理:LocalDateTime默认不包含时区信息,表示的是本地日期和时间。而Date则包含时区信息,它的实际值会受到系统默认时区的影响。

由于LocalDateTime是Java 8及以上版本的新类型,并提供了更多的功能和灵活性,推荐在新的项目中使用LocalDateTime来处理日期和时间。

对于旧版Java项目,仍然需要使用Date类,但在多线程环境下需要注意其线程安全性。

如果需要在LocalDateTime和Date之间进行转换,可以使用相应的方法进行转换,例如通过LocalDateTime的atZone()方法和Date的toInstant()方法进行转换。

二、使用LocalDateTime和LocalDate时遇到了哪些坑?

2.1 Redis序列化报错

2.1.1 问题现象

在使用RedisTemplate向Redis中插入数据时,遇到了如下报错:

kotlin 复制代码
2024-01-11T21:33:25.233+08:00 ERROR 13212 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.ArrayList[0]->com.fast.alden.data.model.SysApiResource["createdTime"])
	at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.serialize(Jackson2JsonRedisSerializer.java:157) ~[spring-data-redis-3.2.0.jar:3.2.0]
	at org.springframework.data.redis.core.AbstractOperations.rawValue(AbstractOperations.java:128) ~[spring-data-redis-3.2.0.jar:3.2.0]
	at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:236) ~[spring-data-redis-3.2.0.jar:3.2.0]

2.1.2 问题分析

在使用Redis缓存含有LocalDateTime类型变量的实体类时会产生序列化问题,因为Jackson库在默认情况下不支持Java8的LocalDateTime类型的序列化和反序列化。

错误堆栈中也给出了解决方案,添加 com.fasterxml.jackson.datatype:jackson-datatype-jsr310依赖,但光添加依赖是不够的,还我们需要自定义序列化和反序列化的行为。

2.1.3 解决方案

  1. 添加maven依赖
xml 复制代码
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>2.13.0</version>
</dependency>
  1. 修改RedisSerializer Bean配置

在定义RedisSerializer Bean的代码中自定义ObjectMapper对象处理时间属性时的序列化和反序列化行为,LocalDateLocalDateTimeLocalTime的序列化和反序列化都要自定义,还要禁用将日期序列化为时间戳。

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisSerializer<Object> redisSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 必须设置,否则无法将JSON转化为对象,会转化成Map类型
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        // 自定义ObjectMapper的时间处理模块
        JavaTimeModule javaTimeModule = new JavaTimeModule();

        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

        objectMapper.registerModule(javaTimeModule);
        
        // 禁用将日期序列化为时间戳的行为
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        
        //创建JSON序列化器
        return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    }
}

2.2 LocalDateTime和LocalDate类型的属性返回给前端的值格式不正确

2.2.1 问题现象

在application.yml中设置了全局的日期类型的序列化和反序列化格式,在对应字段上也并没有使用@JsonFormat进行特殊设置,但是LocalDateTime类型的属性返回给前端时并没有生效,返回的仍是LocalDateTime默认的ISO标准时间格式的字符串。

yaml 复制代码
spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: always
  mvc:
    format:
      date-time: yyyy-MM-dd HH:mm:ss
      date: dd/MM/yyyy

2.2.2 解决方案

自定义Jackson配置,代码如下:

java 复制代码
@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder ->
                builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
                        // long类型转string, 前端处理Long类型,数值过大会丢失精度
                        .serializerByType(Long.class, ToStringSerializer.instance)
                        .serializerByType(Long.TYPE, ToStringSerializer.instance)
                        .serializationInclusion(JsonInclude.Include.NON_NULL)
                        //指定反序列化类型,也可以使用@JsonFormat(pattern = "yyyy-MM-dd")替代。主要是mvc接收日期时使用
                        .deserializerByType(LocalTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")))
                        .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
                        .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                        // 日期序列化,主要返回数据时使用
                        .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")))
                        .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
                        .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

三、总结

在使用java.time API的过程中,除了会遇到前文所说的序列化问题之外,可能还会遇到以下问题:

  • 时区问题:LocalDateTime不包含时区信息,这可能导致在不同时区的用户之间出现不一致性。为了避免这个问题,您应该考虑使用ZonedDateTime或OffsetDateTime,并确保在处理日期和时间时考虑时区。
  • 数据库交互:当与数据库交互时,要确保数据库列的数据类型与正在使用的Java日期类型相匹配。例如,如果使用的是PostgreSQL,则可能需要使用timestamp without time zone列类型来存储日期和时间。
  • 默认值和验证:在某些情况下,可能希望为日期或时间字段设置默认值或进行验证。使用Spring的验证注解(如@NotNull或@Size)可以帮助我们确保输入的有效性。
  • 跨时区处理:由于LocalDateTime不包含时区信息,当与全球用户互动时,需要特别注意时区转换。考虑使用像Joda-Time这样的库来帮助我们处理复杂的时区转换。
  • 处理过去和未来的日期:在处理历史事件或计划未来的活动时,请确保我们的应用程序能够正确地处理这些日期。考虑使用像Period或Duration这样的类来计算日期之间的差异。

我也会及时的更新后续实践中所遇到的问题,希望与诸位看官一起进步。

如有错误,还望批评指正。

相关推荐
hashiqimiya8 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见8 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
无关86889 小时前
SpringBootApplication注解大解密
spring boot
追梦者12310 小时前
springboot整合minio
java·spring boot·后端
帅气的你11 小时前
Spring Boot 集成 AOP 实现日志记录与接口权限校验
java·spring boot
计算机毕设VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机毕设VX:Fegn089512 小时前
计算机毕业设计|基于springboot + vue博物馆展览与服务一体化系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
帅气的你12 小时前
Spring Boot 1.x 接口性能优化:从 3 秒到 200 毫秒的实战调优之路
java·spring boot
yangminlei12 小时前
Spring Boot/Spring MVC核心注解深度解析
spring boot
goodlook012312 小时前
监控平台搭建-日志-springboot直接推送loki篇(九)
java·spring boot·后端·grafana