
1. 问题背景与常见错误
在与外部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默认行为可能导致以下错误:
- 对于LocalDateTime: raw timestamp (...) not allowed for java.time.LocalDateTime: need additional information such as an offset or time-zone。这表明LocalDateTime需要时区或偏移量信息才能从原始时间戳准确构建。
- 对于LocalDate: Invalid value for EpochDay (...)。即使添加了jackson-datatype-jsr310模块,直接将毫秒时间戳映射到LocalDate也会因为LocalDate只表示日期而无法处理时间部分和过大的Epoch值而报错。
为了解决这些问题,我们需要采取特定的策略来指导Jackson正确地进行类型转换。
2. 解决方案
以下是几种处理Jackson反序列化Epoch毫秒时间戳到Java 8日期时间类型的有效方法。
立即学习“Java免费学习笔记(深入)”;
2.1 通过构造函数手动解析时间戳
这种方法的核心思想是让Jackson将时间戳字段作为原始的long类型传递给数据类的构造函数,然后在构造函数内部手动将其转换为LocalDateTime。
实现步骤:
- 在目标数据类中定义一个全参数构造函数。
- 使用@JsonProperty注解将JSON字段名映射到构造函数参数。
- 将时间戳对应的参数类型定义为long。
- 在构造函数内部,使用Instant.ofEpochMilli()将long类型的时间戳转换为Instant,然后通过atZone(ZoneOffset.UTC).toLocalDateTime()转换为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配置。
缺点:
- 如果有很多日期时间字段需要处理,会引入大量重复代码。
- 构造函数可能变得复杂,尤其是当字段很多时。
2.2 全局配置:使用Instant类型与Jackson模块
对于更通用的场景,我们可以配置Jackson的ObjectMapper来自动处理Epoch毫秒时间戳。这种方法通常涉及将目标字段类型更改为Instant,并配置Jackson的行为。
核心思想:
- 注册JavaTimeModule: Jackson默认不支持Java 8日期时间类型,需要注册jackson-datatype-jsr310模块。
- 配置时间戳精度: 告知Jackson时间戳是毫秒而不是纳秒,通过设置DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS为false。
- 使用Instant类型: 在数据类中,将时间戳字段类型定义为java.time.Instant,因为Instant是表示时间线上的一个瞬时点,与Epoch毫秒直接对应。之后,可以根据需要从Instant转换为LocalDateTime。
实现步骤(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 + '\'' +
", creationDate=" + 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。
优点:
- 全局生效,一次配置,所有Instant字段都能正确反序列化。
- 代码简洁,无需在每个数据类中编写转换逻辑。
- 符合Java 8日期时间API的设计理念,Instant是处理时间戳的理想类型。
缺点:
- 要求数据类中的字段类型为Instant。如果你的业务逻辑强烈需要LocalDateTime,则需要在获取Instant后手动转换。
2.3 实现自定义反序列化器
当上述方法不能满足特定需求(例如,你必须将Epoch毫秒直接反序列化为LocalDateTime,并且需要更精细的控制或特定的时区逻辑),可以实现一个自定义的Jackson反序列化器。
实现步骤:
- 创建一个新的类,继承自com.fasterxml.jackson.databind.deser.std.StdDeserializer
,其中T是你希望反序列化的目标类型(例如LocalDateTime)。 - 重写deserialize()方法,在该方法中手动解析JsonParser获取时间戳,并将其转换为目标类型。
- 使用@JsonDeserialize(using = YourDeserializer.class)注解将自定义反序列化器应用到数据类字段上。
示例代码:
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 + '\'' +
'}';
}
}优点:
- 高度灵活,可以处理任何复杂的转换逻辑。
- 允许直接将时间戳反序列化为LocalDateTime或LocalDate,无需中间类型。
- 可重用,一旦定义,可以在多个字段上使用。
缺点:
- 增加了代码量和维护成本,需要为每种特定的转换场景编写自定义逻辑。
- 对于简单的Epoch毫秒转换,可能显得过于复杂。
3. 总结与选择建议
在处理Jackson反序列化Epoch毫秒时间戳到Java 8日期时间类型时,我们有多种策略可供选择:
- 构造函数解析: 适用于少量、特定字段的简单转换,且不希望引入额外Jackson配置的场景。
- 全局配置(配合Instant): 推荐在Spring Boot应用中使用,特别是当多数时间戳都是Epoch毫秒且可以接受Instant作为中间或最终类型时。它提供了简洁的配置和良好的可维护性。如果你最终需要LocalDateTime,可以在Instant字段的getter方法中进行转换。
- 自定义反序列化器: 当你需要将Epoch毫秒直接反序列化为LocalDateTime或LocalDate,并且需要非常精细的控制(例如,根据不同的JSON字段名应用不同的时区,或处理非标准的时间戳格式)时,这是最强大的选择。
选择哪种方法取决于你的项目需求、对代码复杂度的接受程度以及是否使用Spring Boot等框架。通常,对于Spring Boot项目,配置application.properties并使用Instant类型是最优雅和推荐的解决方案。










