当从REST服务接收到的JSON数据中包含Epoch毫秒时间戳(例如1666190973000),并尝试直接将其反序列化到Java 8的java.time.LocalDateTime或java.time.LocalDate类型时,Jackson库默认行为可能会导致错误。常见的错误信息提示raw timestamp ... not allowed for java.time.LocalDateTime: need additional information such as an offset or time-zone,或对于LocalDate,提示Invalid value for EpochDay。
这是因为LocalDateTime和LocalDate本身不包含时区信息,而Epoch时间戳是基于UTC的,需要一个时区或偏移量才能正确地转换为本地日期时间。此外,即使添加了jackson-datatype-jsr310模块,Jackson也需要明确的配置来知道如何处理数字形式的Epoch时间戳,因为它默认可能期望ISO 8601格式的字符串,或者在某些情况下,将其解析为纳秒精度。
为了解决这个问题,我们需要引导Jackson正确地将Epoch毫秒时间戳转换为java.time API中的日期时间类型。以下是几种可行的解决方案。
这种方法的核心思想是在目标Java对象的构造函数中,接收原始的long类型Epoch毫秒时间戳,然后手动将其转换为所需的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() {
}
// 全参数构造函数,用于反序列化
public MyLocalApplicationClass(@JsonProperty("name") String name,
@JsonProperty("creation_date") long creationDate,
@JsonProperty("created_by") String createdBy) {
this.name = name;
this.createdBy = createdBy;
// 将Epoch毫秒转换为LocalDateTime,这里假设时间戳是UTC时间
this.creationDate = Instant
.ofEpochMilli(creationDate)
.atZone(ZoneOffset.UTC) // 明确指定时区,通常UTC是安全的选择
.toLocalDateTime();
}
// Getter和Setter(省略)
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public LocalDateTime getCreationDate() { return creationDate; }
public void setCreationDate(LocalDateTime creationDate) { this.creati
onDate = creationDate; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creationDate=" + creationDate +
", createdBy='" + createdBy + '\'' +
'}';
}
}优点:
缺点:
此方案通过配置Jackson的ObjectMapper来全局处理Epoch毫秒时间戳。它要求将目标字段类型更改为java.time.Instant,因为Instant是时间线上的一个瞬时点,不带时区信息,与Epoch时间戳的概念更匹配。然后,我们可以通过配置告诉Jackson如何将Epoch毫秒反序列化为Instant。
核心配置:
在Spring Boot应用中,可以通过定义Jackson2ObjectMapperBuilder和ObjectMapper的Bean来定制Jackson的行为。
配置类示例:
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()) // 注册JavaTimeModule
.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); // 配置为毫秒精度
}
}目标类示例:
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
public class MyLocalApplicationClass {
private String name;
@JsonProperty("creation_date") // 如果JSON字段名与Java属性名不匹配,仍需此注解
private Instant creationDate; // 将类型改为Instant
@JsonProperty("created_by")
private String createdBy;
// 默认构造函数,Getter和Setter(省略)
public MyLocalApplicationClass() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Instant getCreationDate() { return creationDate; }
public void setCreationDate(Instant creationDate) { this.creationDate = creationDate; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creationDate=" + creationDate + // Instant默认输出ISO格式
", createdBy='" + createdBy + '\'' +
'}';
}
}接收到Instant后,如果需要LocalDateTime,可以在业务逻辑中通过instant.atZone(ZoneOffset.UTC).toLocalDateTime()进行转换。
Spring Boot的JacksonAutoConfiguration会自动检测并注册JavaTimeModule。因此,对于Spring Boot应用,最简便的方法是在application.properties或application.yml中直接配置Jackson属性,无需手动定义ObjectMapper Bean。
application.properties 配置:
spring.jackson.deserialization.read-date-timestamps-as-nanoseconds=false
目标类示例:
MyLocalApplicationClass的定义与2.1节中相同,creationDate字段类型仍为Instant。
优点:
缺点:
如果前两种方案不适用,或者需要对特定字段进行高度定制的日期时间反序列化逻辑,可以实现一个自定义的反序列化器。这种方法允许你完全控制如何将JSON值转换为Java对象。
实现步骤:
自定义反序列化器示例:
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 { JsonNode node = p.getCodec().readTree(p); long timestamp = node.longValue(); // 获取原始的long类型时间戳 // 将Epoch毫秒转换为LocalDateTime return Instant .ofEpochMilli(timestamp) .atZone(ZoneOffset.UTC) // 假设时间戳是UTC时间 .toLocalDateTime(); } }
目标类示例:
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;
// 默认构造函数,Getter和Setter(省略)
public MyLocalApplicationClass() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public LocalDateTime getCreationDate() { return creationDate; }
public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
@Override
public String toString() {
return "MyLocalApplicationClass{" +
"name='" + name + '\'' +
", creationDate=" + creationDate +
", createdBy='" + createdBy + '\'' +
'}';
}
}优点:
缺点:
理解Jackson处理日期时间的工作原理,并根据项目需求选择最合适的策略,能够有效避免反序列化错误,确保数据转换的准确性和代码的健壮性。