1. 引言
保持日期格式的一致性對於在數據表示中保持清晰度和兼容性至關重要,尤其是在使用 JSON 時。在本教程中,我們將探索在序列化和反序列化過程中格式化 Instant 字段的各種技術,使用 Jackson 的 ObjectMapper。我們還將討論使用 @JsonFormat 註解以及擴展現有的序列化器和反序列器以獲得完全控制的方法。
2. 場景和設置
為了説明這些技術,我們將設置一個基本場景,其中包含預定義的日期格式和 DateTimeFormatter:
public interface Instants {
ZoneOffset TIMEZONE = ZoneOffset.UTC;
String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT)
.withZone(ZoneOffset.UTC);
}
為了簡化,我們使用 UTC 作為時區。 我們旨在驗證當使用 ObjectMapper 時,我們是否可以將 Instant 字段序列化為該格式並將其反序列化為原始的 Instant。 讓我們也包含我們將用於測試的示例日期:
class InstantFormatUnitTest {
final String DATE_TEXT = "2024-05-27 12:34:56.789";
final Instant DATE = Instant.from(Instants.FORMATTER.parse(DATE_TEXT));
// ...
}
最後,我們的測試的基礎是檢查我們的映射器是否可以將 Instant 字段序列化為指定格式,然後將其反序列化為預期值。 我們將通過檢查 JSON String 是否包含我們的預期日期文本,然後如果反序列化字段 timeStamp 與我們的 DATE 對象匹配來完成此操作:
void assertSerializedInstantMatchesWhenDeserialized(TimeStampTracker object, ObjectMapper mapper)
throws JsonProcessingException {
String json = mapper.writeValueAsString(object);
assertTrue(json.contains(DATE_TEXT));
TimeStampTracker deserialized = mapper.readValue(json, object.getClass());
assertEquals(DATE, deserialized.getTimeStamp());
}
由於我們需要不同的對象來測試不同的方法,讓我們為它們定義一個簡單的接口:
public interface TimeStampTracker {
Instant getTimeStamp();
}
3. Full Control With a Custom JsonSerializer
讓我們從最標準、最通用的方式開始,使用特定格式序列化非標準字段,擴展 JsonSerializer。這個類是泛型的,我們使用它來控制任何字段的序列化。 讓我們為 Instant 類型編寫一個:
public class CustomInstantSerializer extends JsonSerializer<Instant> {
@Override
public void serialize(Instant instant, JsonGenerator json, SerializerProvider provider)
throws IOException {
// ...
}
}
json.writeString(Instants.FORMATTER.format(instant));
序列化已完成,現在確保我們可以使用這種特定格式反序列化對象。 對於反序列化,我們將遵循類似的路線,通過擴展 JsonDeserializer: 當覆蓋 deserialize() 時,我們將會得到一個 JSON 解析器,而不是生成器。return Instant.from(Instants.FORMATTER.parse(json.getText()));
Using our custom serializer and deserializer requires the @JsonSerialize and @JsonDeserialize annotations. Let’s pass our implementations to them: 讓我們測試它,斷言生成的 JSON 包含預期的格式化日期,並且在反序列化時,時間戳字段與我們的原始日期匹配: 這個方法對我們有幫助,如果我們在幾個類中具有 Instant 字段用於日期,或者想要在某些類中使用特定的序列化/反序列化技術。 由於 Instant 不是 Jackson 支持的默認日期類型之一,因此我們需要將 JavaTimeModule 依賴添加到我們的 pom.xml: 如果沒有它,如果我們嘗試序列化包含 Instant 字段的類,我們將收到錯誤: 此依賴項包含 JavaTimeModule 類,稍後我們會使用它。 默認情況下,ObjectMapper 將日期字段序列化為數字時間戳。 當調用 disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS), 我們可以在 JsonMapper.builder() 上關閉此行為,但它不會允許我們設置特定的格式。 這是因為調用 defaultDateFormat() 僅適用於 Date 和 long 值。 因此,使用特定格式的一種方法是使用 @JsonFormat 註解: 同樣重要的是設置 timezone 屬性。 由於我們使用 “UTC”,我們將在這裏重用它,以及我們在 pattern 字段中指定的日期格式。 讓我們把它們全部組合起來測試序列化和反序列化: 禁用 WRITE_DATES_AS_TIMESTAMPS 不會影響,因為我們使用 @JsonFormat,所以我們不會在這裏停用它。 Jackson 提供了大多數類型的序列化器,包括 InstantSerializer,它提供了一個單例,我們可以使用它與 JavaTimeModule 結合使用: 然而,這種替代方案也限制了我們使用不同格式的能力。 並且,由於 InstantSerializer 不包含公共構造函數,我們將對其進行擴展: 我們使用接受單例作為基本實現並且帶有格式器的構造函數。 我們還傳遞 false 到 useTimestamp 和 useNanoseconds,因為我們想要為我們的 Instant 字段使用特定格式。 而且這一次,我們不需要在我們的類中添加任何註解: 相反,為了在反序列化時使用特定格式,我們需要擴展 InstantDeserializer 並使用格式器構造它: 值得注意的是,與序列化器不同,反序列化器是泛型的,並且可以接受任何 Temporal 類型作為返回類型進行反序列化。3.1. Custom JsonDeserializer
public class CustomInstantDeserializer extends JsonDeserializer<Instant> {
@Override
public Instant deserialize(JsonParser json, DeserializationContext context)
throws IOException {
// ...
}
}3.2. Using the Custom Serializer and Deserializer
public class Event implements TimeStampTracker {
@JsonSerialize(using = CustomInstantSerializer.class)
@JsonDeserialize(using = CustomInstantDeserializer.class)
private Instant timeStamp;
// standard getters and setters
}@Test
void givenDefaultMapper_whenUsingCustomSerializerDeserializer_thenExpectedInstantFormat()
throws JsonProcessingException {
Event object = new Event();
object.setTimeStamp(DATE);
ObjectMapper mapper = new ObjectMapper();
assertSerializedInstantMatchesWhenDeserialized(object, mapper);
}4. 添加 JavaTimeModule 擴展
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>1.17.1</version>
</dependency>com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 日期/時間類型 `java.time.Instant` 不受默認支持4.1. 使用 @JsonFormat 選擇自定義格式
public class Session implements TimeStampTracker {
@JsonFormat(pattern = Instants.DATE_FORMAT, timezone = "UTC")
private Instant timeStamp;
// 標準的 getter 和 setter
}4.2. 測試我們的解決方案
@Test
void givenTimeModuleMapper_whenJsonFormat_thenExpectedInstantFormat()
throws JsonProcessingException {
Session object = new Session();
object.setTimeStamp(DATE);
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
assertSerializedInstantMatchesWhenDeserialized(object, mapper);
}5. 擴展 InstantSerializer 以自定義格式
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(Instant.class, InstantSerializer.INSTANCE);public class GlobalInstantSerializer extends InstantSerializer {
public GlobalInstantSerializer() {
super(InstantSerializer.INSTANCE, false, false, Instants.FORMATTER);
}
}public class History implements TimeStampTracker {
private Instant timeStamp;
// 標準的 getter 和 setter
}5.1. 擴展 InstantDeserializer 以自定義格式
public class GlobalInstantDeserializer extends InstantDeserializer<Instant> {
public GlobalInstantDeserializer() {
super(InstantDeserializer.INSTANT, Instants.FORMATTER);
}
}
5.2. 使用我們的 InstantSerializer/InstantDeserializer 實現
最後,讓我們配置 Java Time 模塊以使用我們的序列化器和反序列器並對其進行測試:
@Test
void givenTimeModuleMapper_whenSerializingAndDeserializing_thenExpectedInstantFormat()
throws JsonProcessingException {
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(Instant.class, new GlobalInstantSerializer());
module.addDeserializer(Instant.class, new GlobalInstantDeserializer());
History object = new History();
object.setTimeStamp(DATE);
ObjectMapper mapper = JsonMapper.builder()
.addModule(module)
.build();
assertSerializedInstantMatchesWhenDeserialized(object, mapper);
}
這個解決方案是最有效和最靈活的,因為我們不需要在我們的類中使用註解,並且它適用於具有 Instant 字段的任何類。
6. 結論
在本文中,我們擴展了 Jackson 內置的序列化器和反序列器,並對自定義序列化器有了清晰的理解。我們利用了這些技術,通過包含 @JsonFormat 註解和使用擴展模塊。最終,我們可以始終如一地格式化 Instant 字段,並按照我們的規範進行。這增強了 JSON 數據的可讀性和兼容性,並提供了在應用程序的不同部分控制日期和時間信息靈活性和控制。