1. 概述
在本教程中,我們將使用 Jackson 對日期進行序列化。首先,我們將序列化一個簡單的 java.util.Date 對象,然後是 Joda-Time,最後是 Java 8 的 DateTime 對象。
2. 將 Date 序列化為時間戳
首先,讓我們看看如何使用 Jackson 序列化一個簡單的 java.util.Date。在下面的示例中,我們將序列化一個 “Event” 實例,該實例具有名為 “eventDate” 的 Date 字段:
@Test
public void whenSerializingDateWithJackson_thenSerializedToTimestamp()
throws JsonProcessingException, ParseException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = df.parse("01-01-1970 01:00");
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
mapper.writeValueAsString(event);
}
需要注意的是,Jackson 默認會將 Date 序列化為時間戳格式(自 1970 年 1 月 1 日 UTC 至今的毫秒數)。
“event” 序列化的實際輸出是:
{
"name":"party",
"eventDate":3600000
}
3. 將 Date 序列化為 ISO-8601 格式
將時間戳序列化為這種簡潔格式效率不高。相反,我們應該將 Date 序列化為 ISO-8601 格式:
@Test
public void whenSerializingDateToISO8601_thenSerializedToText()
throws JsonProcessingException, ParseException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
String toParse = "01-01-1970 02:30";
Date date = df.parse(toParse);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// StdDateFormat 是 ISO8601 格式,自 jackson 2.9 起
mapper.setDateFormat(new StdDateFormat().withColonInTimeZone(true));
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("1970-01-01T02:30:00.000+00:00"));
}
我們可以看到,日期的表示現在更易於閲讀。
4. 配置 ObjectMapper DateFormat
前述解決方案仍然缺乏選擇精確格式表示 java.util.Date 實例的完整靈活性。
相反,讓我們來看一個允許我們設置表示日期的格式的配置:
@Test
public void whenSettingObjectMapperDateFormat_thenCorrect()
throws JsonProcessingException, ParseException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm");
String toParse = "20-12-2014 02:30";
Date date = df.parse(toParse);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(df);
String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}
請注意,即使我們現在在日期格式方面更加靈活,我們仍然在整個 ObjectMapper 的級別上使用全局配置。
5. 使用 @JsonFormat 格式化 Date
接下來,讓我們看看 @JsonFormat 註解如何用於在單個類級別控制日期格式,而不是在整個應用程序級別進行全局控制:
public class Event {
public String name;
@JsonFormat
(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
public Date eventDate;
}
現在我們來測試一下:
@Test
public void whenUsingJsonFormatAnnotationToFormatDate_thenCorrect()
throws JsonProcessingException, ParseException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
String toParse = "20-12-2014 02:30:00";
Date date = df.parse(toParse);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}
6. 自定義 日期 序列化器
接下來,為了獲得對輸出的完全控制權,我們將使用自定義序列化器來處理日期:
public class CustomDateSerializer extends StdSerializer<Date> {
private SimpleDateFormat formatter
= new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
public CustomDateSerializer() {
this(null);
}
public CustomDateSerializer(Class t) {
super(t);
}
@Override
public void serialize (Date value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}
現在我們將它用作“eventDate”字段的序列化器:
public class Event {
public String name;
@JsonSerialize(using = CustomDateSerializer.class)
public Date eventDate;
}
最後,我們將對其進行測試:
@Test
public void whenUsingCustomDateSerializer_thenCorrect()
throws JsonProcessingException, ParseException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
String toParse = "20-12-2014 02:30:00";
Date date = df.parse(toParse);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString(toParse));
}
7. 使用 Jackson 序列化 Joda-Time
日期並非總是 java.util.Date 的實例。事實上,日期越來越多地由其他類表示,其中一個常見的實現是 Joda-Time 庫中的 DateTime 實現。
讓我們看看如何 使用 Jackson 序列化 DateTime
我們將使用 jackson-datatype-joda 模塊,以獲得開箱即用的 Joda-Time 支持:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.17.2</version>
</dependency>
然後,我們只需註冊 JodaModule,即可完成:
@Test
public void whenSerializingJodaTime_thenCorrect()
throws JsonProcessingException {
DateTime date = new DateTime(2014, 12, 20, 2, 30,
DateTimeZone.forID("Europe/London"));
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
String result = mapper.writeValueAsString(date);
assertThat(result, containsString("2014-12-20T02:30:00.000Z"));
}
8. 使用 Joda DateTime 對象與自定義序列化器進行序列化
如果我們不想引入額外的 Joda-Time Jackson 依賴,還可以利用自定義序列化器(類似於早期的示例),以獲得DateTime 實例的乾淨序列化:public class CustomDateTimeSerializer extends StdSerializer<DateTime> {
private static DateTimeFormatter formatter =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm");
public CustomDateTimeSerializer() {
this(null);
}
public CustomDateTimeSerializer(Class<DateTime> t) {
super(t);
}
@Override
public void serialize
(DateTime value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.print(value));
}
}
然後我們可以將其用作我們的屬性“eventDate”序列化器:
public class Event {
public String name;
@JsonSerialize(using = CustomDateTimeSerializer.class)
public DateTime eventDate;
}
最後,我們可以將所有內容組合在一起並進行測試:
@Test
public void whenSerializingJodaTimeWithJackson_thenCorrect()
throws JsonProcessingException {
DateTime date = new DateTime(2014, 12, 20, 2, 30);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("2014-12-20 02:30"));
}
9. 使用 Jackson 序列化 Java 8 Date
現在,讓我們看看如何使用 Jackson 序列化 Java 8 DateTime,在本例中,我們使用 LocalDateTime,並使用 jackson-datatype-jsr310 模塊:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.2</version>
</dependency>
然後,我們只需要註冊 JavaTimeModule (JSR310Module 已棄用),Jackson 將負責其餘工作:
@Test
public void whenSerializingJava8Date_thenCorrect()
throws JsonProcessingException {
LocalDateTime date = LocalDateTime.of(2014, 12, 20, 2, 30);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
String result = mapper.writeValueAsString(date);
assertThat(result, containsString("2014-12-20T02:30"));
}
10. 自定義 Java 8 Date 無需任何額外依賴地序列化
如果我們不想引入額外的依賴,我們始終可以使用 自定義序列化器將 Java 8 DateTime 寫入 JSON:
public class CustomLocalDateTimeSerializer
extends StdSerializer<LocalDateTime> {
private static DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
public CustomLocalDateTimeSerializer() {
this(null);
}
public CustomLocalDateTimeSerializer(Class<LocalDateTime> t) {
super(t);
}
@Override
public void serialize(
LocalDateTime value,
JsonGenerator gen,
SerializerProvider arg2)
throws IOException, JsonProcessingException {
gen.writeString(formatter.format(value));
}
}
然後我們將使用該序列化器為我們的 “eventDate” 字段:
public class Event {
public String name;
@JsonSerialize(using = CustomLocalDateTimeSerializer.class)
public LocalDateTime eventDate;
}
最後我們將進行測試:
@Test
public void whenSerializingJava8DateWithCustomSerializer_thenCorrect()
throws JsonProcessingException {
LocalDateTime date = LocalDateTime.of(2014, 12, 20, 2, 30);
Event event = new Event("party", date);
ObjectMapper mapper = new ObjectMapper();
String result = mapper.writeValueAsString(event);
assertThat(result, containsString("2014-12-20 02:30"));
}
11. 反序列化 Date
現在讓我們看看如何使用 Jackson 反序列化一個 Date。在下面的示例中,我們將反序列化一個包含日期的“Event”實例:
@Test
public void whenDeserializingDateWithJackson_thenCorrect()
throws JsonProcessingException, IOException {
String json = "{\"name\":\"party\",\"eventDate\":\"20-12-2014 02:30:00\"}";
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(df);
Event event = mapper.readerFor(Event.class).readValue(json);
assertEquals("20-12-2014 02:30:00", df.format(event.eventDate));
}
12. 反序列化 Joda ZonedDateTime 並保留時區
在默認配置下,Jackson 會將 Joda 的 ZonedDateTime 調整為本地上下文的時區。由於本地上下文的時區默認未設置,並且必須手動配置,因此 Jackson 將時區調整為 GMT:
@Test
public void whenDeserialisingZonedDateTimeWithDefaults_thenNotCorrect()
throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));
String converted = objectMapper.writeValueAsString(now);
ZonedDateTime restored = objectMapper.readValue(converted, ZonedDateTime.class);
System.out.println("serialized: " + now);
System.out.println("restored: " + restored);
assertThat(now, is(restored));
}
測試用例將產生以下輸出:
serialized: 2017-08-14T13:52:22.071+02:00[Europe/Berlin]
restored: 2017-08-14T11:52:22.071Z[UTC]
幸運的是,這種奇怪的默認行為有一個快速簡單的解決方案;我們只需要告訴 Jackson 不要調整時區。
可以通過在上述測試用例中添加以下代碼行來完成此操作:
objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
請注意,為了保留時區,我們還需要禁用將日期序列化為時間戳的默認行為。
13. 自定義 日期 序列化器
我們也可以使用 自定義 日期 序列化器。 我們將為屬性“eventDate”編寫自定義序列化器:
public class CustomDateDeserializer extends StdDeserializer<Date> {
private SimpleDateFormat formatter =
new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
public CustomDateDeserializer() {
this(null);
}
public CustomDateDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Date deserialize(JsonParser jsonparser, DeserializationContext context)
throws IOException, JsonProcessingException {
String date = jsonparser.getText();
try {
return formatter.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
接下來,我們將它用作“eventDate”序列化器:
public class Event {
public String name;
@JsonDeserialize(using = CustomDateDeserializer.class)
public Date eventDate;
}
最後,我們將對其進行測試:
@Test
public void whenDeserializingDateUsingCustomDeserializer_thenCorrect()
throws JsonProcessingException, IOException {
String json = "{"name":"party","eventDate":"20-12-2014 02:30:00"}";
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
ObjectMapper mapper = new ObjectMapper();
Event event = mapper.readerFor(Event.class).readValue(json);
assertEquals("20-12-2014 02:30:00", df.format(event.eventDate));
}
14. 修復 InvalidDefinitionException
創建 LocalDate 實例時,我們可能會遇到異常:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance
of `java.time.LocalDate`(no Creators, like default construct, exist): no String-argument
constructor/factory method to deserialize from String value ('2014-12-20') at [Source:
(String)"2014-12-20"; line: 1, column: 1]
這個問題發生的原因是 JSON 本身沒有日期格式,因此它將日期表示為 String。
日期的 String 表示形式與類型為 LocalDate 的內存對象不同,因此我們需要一個外部反序列化器來從 String 讀取該字段,以及一個序列化器來將日期格式化為 String 格式。
這些方法也適用於 LocalDateTime,唯一的區別是使用等效類來代替 LocalDateTime。
14.1. Jackson 依賴項
Jackson 允許我們以幾種方式解決這個問題。首先,我們需要確保 jsr310 依賴項 存在於我們的 pom.xml 中:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.2</version>
</dependency>
14.2. 將日期序列化為單個日期對象
為了能夠處理 LocalDate,我們需要將 JavaTimeModule 註冊到我們的 ObjectMapper 中。
我們還需要在 ObjectMapper 中禁用 WRITE_DATES_AS_TIMESTAMPS 功能,以防止 Jackson 將時間數字添加到 JSON 輸出中:
@Test
public void whenSerializingJava8DateAndReadingValue_thenCorrect() throws IOException {
String stringDate = "\"2014-12-20\"";
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
LocalDate result = mapper.readValue(stringDate, LocalDate.class);
assertThat(result.toString(), containsString("2014-12-20"));
}
Here we used Jackson’s native support for serializing and deserializing dates.
14.3. 在 POJO 中使用註解
另一種處理該問題的方法是使用 LocalDateDeserializer 和 JsonFormat 註解在實體級別:
public class EventWithLocalDate {
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy")
public LocalDate eventDate;
}
@JsonDeserialize 註解用於指定一個自定義反序列化器來將 JSON 對象反序列化。 類似地,@JsonSerialize 表示一個自定義序列化器,用於在實體級別序列化對象。
此外,@JsonFormat 註解允許我們指定日期序列化的格式。 因此,此 POJO 可以用於讀取和寫入 JSON。
@Test
public void whenSerializingJava8DateAndReadingFromEntity_thenCorrect() throws IOException {
String json = "{\"name\":\"party\",\"eventDate\":\"20-12-2014\"}";
ObjectMapper mapper = new ObjectMapper();
EventWithLocalDate result = mapper.readValue(json, EventWithLocalDate.class);
assertThat(result.getEventDate().toString(), containsString("2014-12-20"));
}
雖然這種方法比使用 JavaTimeModule 的默認設置需要更多工作,但它更具可定製性。
15. 結論
在本文中,我們探討了 Date 文章中,Jackson 如何幫助將日期轉換為 JSON,並使用我們可控的合理格式。