在与外部rest服务交互时,我们经常会遇到日期时间字段以epoch毫秒(自1970年1月1日00:00:00 utc以来的毫秒数)的形式传输。例如,一个json响应可能包含如下结构:
{
"name": "anything",
"creation_date": 1666190973000,
"created_by": "anyone"
}而我们的Java应用中,通常希望将creation_date这样的字段直接映射到Java 8的LocalDateTime或LocalDate类型:
public class MyLocalApplicationClass {
private String name;
private LocalDateTime creationDate; // 期望的目标类型
private String createdBy;
// ... getters, setters ...
}然而,直接尝试将Epoch毫秒时间戳反序列化为LocalDateTime或LocalDate时,Jackson默认行为可能导致以下错误:
为了解决这些问题,我们需要采取特定的策略来指导Jackson正确地进行类型转换。
以下是几种处理Jackson反序列化Epoch毫秒时间戳到Java 8日期时间类型的有效方法。
这种方法的核心思想是让Jackson将时间戳字段作为原始的long类型传递给数据类的构造函数,然后在构造函数内部手动将其转换为LocalDateTime。
实现步骤:
示例代码:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class MyLocalApplicationClass {
private String name;
private LocalDateTime creationDate;
private String createdBy;
// 全参数构造函数,用于Jackson反序列化
public MyLocalApplicationClass(@JsonProperty("name") String name,
@JsonProperty("creation_date") long creationDate,
@JsonProperty("created_by") String createdBy) {
this.name = name;
this.createdBy = createdBy;
// 将Epoch毫秒时间戳转换为UTC时区的LocalDateTime
this.creationDate = Instant
.ofEpochMilli(creationDate)
.atZone(ZoneOffset.UTC) // 假设时间戳是UTC时间
.toLocalDateTime();
}
// ... getters and other methods ...
public String getName() { return name; }
public LocalDateTime getCreationDate() { return creationDate; }
public String getCreatedBy() { return createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creationDate=" + creationDate +
", createdBy='" + createdBy + '\'' +
'}';
}
}优点:
缺点:
对于更通用的场景,我们可以配置Jackson的ObjectMapper来自动处理Epoch毫秒时间戳。这种方法通常涉及将目标字段类型更改为Instant,并配置Jackson的行为。
核心思想:
实现步骤(Spring Boot环境):
A. 配置ObjectMapper Bean:
在Spring Boot应用中,可以通过配置类来注册JavaTimeModule并设置反序列化特性。
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@Configuration
public class JsonConfig {
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return new Jackson2ObjectMapperBuilder();
}
@Bean
public ObjectMapper objectMapper() {
return jackson2ObjectMapperBuilder()
.build()
.registerModule(new JavaTimeModule()) // 注册Java 8日期时间模块
// 告知Jackson时间戳是毫秒而不是纳秒
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
}
}B. 更新数据类:
将creationDate字段的类型更改为Instant。如果JSON字段名与Java字段名不匹配,仍需使用@JsonProperty。
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
public class MyLocalApplicationClass {
private String name;
@JsonProperty("creation_date") // 如果JSON字段名不同
private Instant creationDate; // 更改为Instant类型
@JsonProperty("created_by")
private String createdBy;
// ... getters, setters, and other methods ...
public String getName() { return name; }
public Instant getCreationDate() { return creationDate; } // 返回Instant
public LocalDateTime getCreationLocalDateTime() { // 如果需要LocalDateTime,可以提供一个转换方法
return creationDate != null ? creationDate.atZone(ZoneOffset.UTC).toLocalDateTime() : null;
}
public String getCreatedBy() { return createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creati
onDate=" + creationDate + // Instant的toString()方法
", createdBy='" + createdBy + '\'' +
'}';
}
}C. 简化全局配置(Spring Boot推荐):
在Spring Boot中,JacksonAutoConfiguration会自动检测并注册所有已知的Jackson模块。因此,我们通常不需要手动声明ObjectMapper或Jackson2ObjectMapperBuilder Bean。只需在application.properties或application.yml中进行配置即可。
application.properties配置:
spring.jackson.deserialization.READ_DATE_TIMESTAMPS_AS_NANOSECONDS=false
application.yml配置:
spring:
jackson:
deserialization:
read-date-timestamps-as-nanoseconds: false使用这种方式,你只需确保jackson-datatype-jsr310依赖已添加到项目中,并且数据类中的日期时间字段类型为Instant。
优点:
缺点:
当上述方法不能满足特定需求(例如,你必须将Epoch毫秒直接反序列化为LocalDateTime,并且需要更精细的控制或特定的时区逻辑),可以实现一个自定义的Jackson反序列化器。
实现步骤:
示例代码:
A. 自定义反序列化器:
import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; public class DateTimeDeserializer extends StdDeserializer{ public DateTimeDeserializer() { super(LocalDateTime.class); } @Override public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { // 读取JSON节点 JsonNode node = p.getCodec().readTree(p); // 获取长整型的时间戳值 long timestamp = node.longValue(); // 将Epoch毫秒时间戳转换为UTC时区的LocalDateTime return Instant .ofEpochMilli(timestamp) .atZone(ZoneOffset.UTC) // 假设时间戳是UTC时间 .toLocalDateTime(); } }
B. 应用到数据类字段:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.time.LocalDateTime;
public class MyLocalApplicationClass {
private String name;
@JsonDeserialize(using = DateTimeDeserializer.class) // 应用自定义反序列化器
@JsonProperty("creation_date")
private LocalDateTime creationDate; // 字段类型保持为LocalDateTime
@JsonProperty("created_by")
private String createdBy;
// ... getters, setters, and other methods ...
public String getName() { return name; }
public LocalDateTime getCreationDate() { return creationDate; }
public String getCreatedBy() { return createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creationDate=" + creationDate +
", createdBy='" + createdBy + '\'' +
'}';
}
}优点:
缺点:
在处理Jackson反序列化Epoch毫秒时间戳到Java 8日期时间类型时,我们有多种策略可供选择:
选择哪种方法取决于你的项目需求、对代码复杂度的接受程度以及是否使用Spring Boot等框架。通常,对于Spring Boot项目,配置application.properties并使用Instant类型是最优雅和推荐的解决方案。