Java 8日期/时间( Date/Time)API是开发人员最受追捧的变化之一,其简洁、清晰以及线程安全等特性使得其推出后就备受Java开发者欢迎。
Java 8日期/时间API是JSR-310的实现,它的实现目标是克服旧的日期时间实现中所有的缺陷。但是Spring中的很多组件标准(如JPA2.1)的制定时间更早,因此未兼容到Java 8的新时间类型(LocalDate, LocalTime, LocalDateTime),所以直接使用这些类型作为时间属性时,会出现转换失败等异常。
但Spring各组件在随后的更新中均添加了对Java 8新时间类型的支持,但是需要进行手动配置。使用这些相关的支持,可以让我们更愉快地使用LocalDate等类型作为我们对象默认的时间属性了。
如何将LocalDate和LocalDateTime通过JPA持久化
当我们在使用JPA进行持久层操作时,如果实体(entity)的某个属性为Java 8的新时间类型时,它将无法正确地被持久化到数据库。JPA会将其映射为一个BLOB字段而不是DATE or TIMESTAMP,而这并不是我们所希望的。
使用AttributeConverter支持新时间类型的映射转换
在JPA标准中,DATE字段使用java.sql.Date进行映射,而TIMESTAMP字段则是java.sql.Timestamp。 java.util.Date则可以通过@Temporal注解指定映射的时间类型。因此,如果要让JPA兼容新的时间类型,则需要将新时间类型转换为JPA支持的时间类型。
使用Spring官方提供的Converter
在Sping Data的公共模块中,很早便加入了JSR-310的支持,如果要使用该Converter,只需要在你的应用启动类中添加以下配置:
@EntityScan(
basePackageClasses = { Application.class, Jsr310JpaConverters.class}
)
@SpringBootApplication
class Application { … }
该配置将保证你的应用目录以及JSR-310的Converter将被扫描到,并且支持使用新的时间类型的entity被持久化。但需要注意的是,该Converter只是将新时间类型转换为了传统的Date,而且仅支持未带时区信息的时间类型。
使用自定义的Converter
由于Spring-Data-JPA默认使用的Hibernate对JPA2.1标准进行的实现。而在JPA2.1中,增加了Attribute Converter功能,因此,可以通过自定义Attribute Converter来实现新时间类型的转换。
例如:
@Converter(autoApply = true)
public class LocalDateAttributeConverter implements AttributeConverter<LocalDate, Date> {
@Override
public Date convertToDatabaseColumn(LocalDate locDate) {
return (locDate == null ? null : Date.valueOf(locDate));
}
@Override
public LocalDate convertToEntityAttribute(Date sqlDate) {
return (sqlDate == null ? null : sqlDate.toLocalDate());
}
}
首先需要实现AttributeConverter<LocalDate, Date>接口,包括它的两个转换方法:convertToDatabaseColumn 和 convertToEntityAttribute,它们两个定义了从实体类型(LocalDate)转换成数据库字段类型(Date)以及与之相反方法。这里的写法非常简单,因为java.sql.Date已经有现成的通过LocalDate进行转换的方法。
然后需要将该属性转换器添加 @Converter 注解并设置autoApply属性为true,该设置将保证该转换器会自动应用到LocalDate类型的属性上。
最后确认该Converter可以被bean扫描器扫描到,这样我就完成了一个自定义的属性转换器。同理,我们可以创建一个LocalDateTime的转换器:
@Converter(autoApply = true)
public class LocalDateTimeAttributeConverter implements AttributeConverter<LocalDateTime, Timestamp> {
@Override
public Timestamp convertToDatabaseColumn(LocalDateTime locDateTime) {
return (locDateTime == null ? null : Timestamp.valueOf(locDateTime));
}
@Override
public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) {
return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime());
}
}
其它方案
Hibernate5同样提供了对Java 8新时间类型的支持,可以通过pom文件中增加以下配置来添加依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-java8</artifactId>
<version>5.1.0.Final</version>
</dependency>
附:hibernate5 Java 8支持包映射关系对应表
Java type JDBC type java.time.Duration BIGINT java.time.Instant TIMESTAMP java.time.LocalDateTime TIMESTAMP java.time.LocalDate DATE java.time.LocalTime TIME java.time.OffsetDateTime TIMESTAMP java.time.OffsetTime TIME java.time.ZonedDateTime TIMESTAMP
SpringMVC如何将request参数自动封装为LocalDate和LocalDateTime
在使用SpringMVC时,java.util.Date类型字段可以使用@DateTimeFormat注解将application/x-www-from-urlencoded类型的请求中的字符串进行自动转换。而Java 8中新的时间类型该如何支持呢?
在application/x-www-from-urlencoded(键值对)请求中自动转换新时间类型
从Spring4.0开始,Spring的context模块包中增加了Jsr310DateTimeFormatAnnotationFormatterFactory工厂类。该类是对@DateTimeFormat注解的JSR310标准扩展支持。因此,在Spring4.0之后,可以直接使用@DateTimeFormat注解标注LocalDate等新时间类型字段,从而实现时间格式字符串到新时间类型的自动转换。
如:
public class SimpleRequest {
private Integer id;
@DateTimeFormat(iso = ISO.DATE)
private LocalDate startDate;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
}
在JSON请求中支持新时间类型的转换
如果请求的格式为application/json,则@DateTimeFormat注解将不再生效,取而代之的是Spring默认使用Jackson作为json的序列化工具,因此需要增加Jackson对新时间类型的反序列化器(Deserializer)来支持新时间类型的转换。
而Jackson官方已经提供了对JSR310标准的支持包,只需在pom文件中添加以下配置引入依赖(版本自选):
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.3</version>
</dependency>
然后在需要反序列化(序列化)的对象字段上添加@JsonDeserialize(using = LocalDateDeserializer.class)(@JsonSerialize(using = LocalDateSerializer.class))注解,Jackson便会使用该反序列化器将json字段反序列化成LocalDate类型。
如:
public class SimpleRequest {
private Integer id;
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate startDate;
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime startTime;
}
在Spring Boot 2.0之后,将默认依赖
spring-boot-starter-json
包,该依赖包括了jackson-datatype-jsr310
在内的3种json实用工具包,因此不需要再手动添加依赖
结语
Java 8的日期/时间API非常易于使用,理解了其设计语法会让相似方法也变得非常好找。以上的这些说明就是为了方便更快地在实际应用中将旧的时间类切换为新的时间类型,虽然会有些时间上的消耗,但我相信在你理解了新Api带来的方便与好处后,就会知道这是值得的。