如何在 Java 中使用 Jackson Annotation 按原样反序列化时间戳?

Pra*_*ind 2 java datetime json utc jackson

我的 Java Bean 中有一个字段,如下所示,其中@JsonFormat包含 jackson 注释:

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm")
private Timestamp createdOn;
Run Code Online (Sandbox Code Playgroud)

当我返回 时Timestamp,它总是在我的 JSON 响应中转换为 UTC 时间。让我们说格式化我的时间戳,我得到2017-09-13 15:30但我的响应被返回为2017-09-13 10:00.

我知道这是因为 Jackson 注释默认采用 System TimeZone 并将时间戳转换为 UTC。(在我的情况下,服务器属于Asia/Calcutta时区,因此偏移量是+05:30,杰克逊映射器减去 5 小时 30 分钟以将时间戳转换为 UTC)

有没有办法按原样返回时间戳,即,2017-09-13 15:30而不是2017-09-13 10:00

小智 5

我在这里做了一个测试,将 JVM 默认时区更改为Asia/Calcutta并创建一个带有java.sql.Timestamp字段的类:

public class TestTimestamp {

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
    private Timestamp createdOn;

    // getter and setter
}
Run Code Online (Sandbox Code Playgroud)

在您的另一个问题中,您告诉我们正在使用 JDK 8,因此我在同一版本中进行了测试(如果您使用的是另一个版本(JDK <= 7),请检查底部的“非 Java 8”部分)。首先,我创建了一个Timestamp对应于2017 年9 月 13日的UTC 时间上午 10 点

TestTimestamp value = new TestTimestamp();
// timestamp corresponds to 2017-09-13T10:00:00 UTC
value.setCreatedOn(Timestamp.from(Instant.parse("2017-09-13T10:00:00Z")));
Run Code Online (Sandbox Code Playgroud)

然后我用杰克逊的序列化了它ObjectMapper

ObjectMapper om = new ObjectMapper();
String s = om.writeValueAsString(value);
Run Code Online (Sandbox Code Playgroud)

结果String是:

{"createdOn":"2017-09-13 10:00"}

请注意,在生成的 JSON 中,输出采用 UTC(上午 10 点)。如果我反序列化这个 JSON:

value = om.readValue(s, TestTimestamp.class);
System.out.println(value.getCreatedOn());
Run Code Online (Sandbox Code Playgroud)

这将打印:

2017-09-13 15:30:00.0

那是因为该Timestamp::toString()方法(在 中隐式调用System.out.println)在 JVM 默认时区中打印时间戳。在这种情况下,默认值为Asia/CalcuttaUTC中的10 AMCalcutta 中的 15:30相同,因此生成了上面的输出。

正如我在对您的其他问题的回答中所解释的那样,Timestamp对象没有任何时区信息。它只有自 Unix 纪元(1970-01-01T00:00Z“UTC 时间 1970 年 1 月 1 日午夜”以来的纳秒数)。

使用上面的示例,如果您看到 的值value.getCreatedOn().getTime(),您会看到它是1505296800000- 这是自纪元以来的毫秒数(要获得纳秒精度,有方法getNanos())。

此毫秒值对应于 UTC 的上午 10 点、圣保罗的上午 7 点、伦敦的上午 11 点、东京的晚上 7 点、加尔各答的 15:30 等。您不会Timestamp在区域之间进行转换,因为世界各地的毫秒值都是相同的。

但是,您可以更改的是此值的表示(特定时区中的相应日期/时间)。

在 Jackson 中,您可以创建自定义序列化器和反序列化器(通过扩展com.fasterxml.jackson.databind.JsonSerializercom.fasterxml.jackson.databind.JsonDeserializer),因此您可以更好地控制如何格式化和解析日期。

首先,我创建一个序列化程序,将 格式化Timestamp为 JVM 默认时区:

public class TimestampSerializer extends JsonSerializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        // get the timestmap in the default timezone
        ZonedDateTime z = value.toInstant().atZone(ZoneId.systemDefault());
        String str = fmt.format(z);

        gen.writeString(str);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我创建一个反序列化器,它读取 JVM 默认时区中的日期并创建Timestamp

public class TimestampDeserializer extends JsonDeserializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // parse to a LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt);
        // the date/time is in the default timezone
        return Timestamp.from(dt.atZone(ZoneId.systemDefault()).toInstant());
    }
}
Run Code Online (Sandbox Code Playgroud)

我还更改了字段以使用这些自定义类:

// remove JsonFormat annotation
@JsonSerialize(using = TimestampSerializer.class)
@JsonDeserialize(using = TimestampDeserializer.class)
private Timestamp createdOn;
Run Code Online (Sandbox Code Playgroud)

现在,当使用上述相同Timestamp(对应于2017-09-13T10:00:00Z)时。序列化它会产生:

{"createdOn":"2017-09-13 15:30"}

请注意,现在输出对应于 JVM 默认时区中的本地时间(15:30 in Asia/Calcutta)。

反序列化这个 JSON 时,我得到相同的结果Timestamp(对应于 UTC 时间的上午 10 点)。


这段代码使用 JVM 默认时区,但它可以在没有通知的情况下更改,即使在运行时也是如此,因此最好始终明确说明您正在使用哪个时区。

API使用IANA时区的名称(总是在格式Region/City,如Asia/CalcuttaEurope/Berlin),这样你就可以使用创建它们ZoneId.of("Asia/Calcutta")。避免使用 3 个字母的缩写(如ISTPST),因为它们含糊不清且不标准

您可以通过调用获取可用时区列表(并选择最适合您系统的时区)ZoneId.getAvailableZoneIds()

如果您想将输出更改为对应另一个时区,只需更改ZoneId.systemDefault()为您想要的区域即可。请记住,序列化和反序列化必须使用相同的区域,否则会得到错误的结果。


不是 Java 8?

如果您使用的是 Java <= 7,则可以使用ThreeTen Backport,这是 Java 8 新日期/时间类的一个很好的向后移植。

唯一的区别是包名(在 Java 8 中是 ,java.time而在 ThreeTen Backport 中是org.threeten.bp),但类和方法是相同的。

还有另一个区别:只有在 Java 8 中,Timestamp类才有方法toInstant()and from(),所以你需要使用org.threeten.bp.DateTimeUtilsclass 来进行转换:

public class TimestampSerializer extends JsonSerializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public void serialize(Timestamp value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
        // get the timestmap in the default timezone
        ZonedDateTime z = DateTimeUtils.toInstant(value).atZone(ZoneId.systemDefault());
        String str = fmt.format(z);

        gen.writeString(str);
    }
}

public class TimestampDeserializer extends JsonDeserializer<Timestamp> {

    private DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public Timestamp deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        // parse to a LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(jsonParser.getText(), fmt);
        // the date/time is in the default timezone
        return DateTimeUtils.toSqlTimestamp(dt.atZone(ZoneId.systemDefault()).toInstant());
    }
}
Run Code Online (Sandbox Code Playgroud)