1. 引言
保持日期格式的一致性對於在數據表示中保持清晰度和兼容性至關重要,尤其是在使用 JSON 時。在本教程中,我們將探討在序列化和反序列化過程中格式化 Instant 字段的各種技術,並使用 Jackson 的 ObjectMapper 將其解析回原始格式。我們還將討論使用 @JsonFormat 註解以及擴展現有的序列化和反序列器以獲得完全控制的方法。
2. 場景與配置
為了説明這些技術,我們將設置一個基本場景,其中包含預定義的日期格式和 <em >DateTimeFormatter</em>:
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. 使用自定義 JsonSerializer 實現完全控制
讓我們從最標準、最通用的方式開始,使用自定義 JsonSerializer 來控制 Jackson 中非標準字段的序列化,特別是當需要指定特定的格式時。該類是通用的,可以用來控制任何字段的序列化。 讓我們創建一個用於序列化 Instant 類型的 JsonSerializer:
public class CustomInstantSerializer extends JsonSerializer<Instant> {
@Override
public void serialize(Instant instant, JsonGenerator json, SerializerProvider provider)
throws IOException {
// ...
}
}在覆蓋 serialize() 方法時,我們主要關注 JsonGenerator 參數,該參數用於使用我們的格式器將格式化的 instant 值寫入:
json.writeString(Instants.FORMATTER.format(instant));序列化已完成,接下來確保我們能夠使用這種格式反序列化對象。
3.1. 自定義 JsonDeserializer
對於反序列化,我們將遵循類似的流程,通過擴展 JsonDeserializer。
public class CustomInstantDeserializer extends JsonDeserializer<Instant> {
@Override
public Instant deserialize(JsonParser json, DeserializationContext context)
throws IOException {
// ...
}
}當重寫 deserialize() 時,我們會得到一個 JSON 解析器,而不是一個生成器。 讓我們稱 json.getText() 作為解析器,它持有字段值,並將其傳遞給我們的格式器進行解析:
return Instant.from(Instants.FORMATTER.parse(json.getText()));3.2. 使用自定義序列化器和反序列器
使用我們的自定義序列化器和反序列器需要使用@JsonSerialize和@JsonDeserialize註解。 讓我們將我們的實現傳遞給它們:
public class Event implements TimeStampTracker {
@JsonSerialize(using = CustomInstantSerializer.class)
@JsonDeserialize(using = CustomInstantDeserializer.class)
private Instant timeStamp;
// standard getters and setters
}讓我們測試它,驗證生成的 JSON 是否包含預期的格式化日期,並且在反序列化時,instant 字段與我們的原始日期匹配:
@Test
void givenDefaultMapper_whenUsingCustomSerializerDeserializer_thenExpectedInstantFormat()
throws JsonProcessingException {
Event object = new Event();
object.setTimeStamp(DATE);
ObjectMapper mapper = new ObjectMapper();
assertSerializedInstantMatchesWhenDeserialized(object, mapper);
}此方法在我們需要處理幾個具有Instant類型的字段(例如日期)的類,或者在某些類中使用特定的序列化/反序列化技術時非常有用。
4. 添加 JavaTimeModule 擴展
由於 Instant 不是 Jackson 支持的默認日期類型之一,因此我們需要將 JavaTimeModule 依賴添加到我們的 pom.xml 中:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>1.17.1</version>
</dependency>如果沒有它,如果我們嘗試序列化包含 Instant 字段的類,將會收到錯誤:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.Instant` not supported by default此依賴項包含 JavaTimeModule 類,稍後我們會使用它。
4.1. 選擇自定義格式,使用 @JsonFormat
默認情況下,ObjectMapper 會將日期字段序列化為數字時間戳。通過調用 disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS),可以在 JsonMapper.builder() 上關閉此行為,但無法設置特定的格式。這是因為調用 defaultDateFormat() 僅適用於 Date 和 long 值。因此,使用特定格式的一種方法是使用 @JsonFormat 註解:
public class Session implements TimeStampTracker {
@JsonFormat(pattern = Instants.DATE_FORMAT, timezone = "UTC")
private Instant timeStamp;
// standard getters and setters
}同樣重要的是設置 時區 屬性。由於我們使用的是 “UTC”,我們將在這裏重用它,以及我們在 pattern 字段中指定的時間格式。
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);
}禁用 WRITE_DATES_AS_TIMESTAMPS 將不會產生影響,因為我們使用 @JsonFormat,所以這裏不會進行停用。
5. 使用自定義格式擴展 InstantSerializer
Jackson 提供了用於大多數類型的序列化器,包括 InstantSerializer,它是一個單例,可與 JavaTimeModule 一起使用:
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(Instant.class, InstantSerializer.INSTANCE);不幸的是,這種替代方案也使我們無法使用不同的格式。 此外,由於 InstantSerializer 不包含公共構造函數,我們將對其進行擴展:
public class GlobalInstantSerializer extends InstantSerializer {
public GlobalInstantSerializer() {
super(InstantSerializer.INSTANCE, false, false, Instants.FORMATTER);
}
}我們正在使用接受單例作為基礎實現的構造函數,並結合格式化器。我們還將 false 傳遞給 useTimestamp 和 useNanoseconds,因為我們希望對我們的 Instant 字段使用特定的格式。 而且這一次,我們不需要在我們的類中添加任何註釋:
public class History implements TimeStampTracker {
private Instant timeStamp;
// standard getters and setters
}5.1. 使用自定義格式擴展 InstantDeserializer
相反,要使用自定義格式進行反序列化,我們需要擴展 InstantDeserializer 並使用 InstantDeserializer.INSTANT 常量和我們的格式器來構建它:
public class GlobalInstantDeserializer extends InstantDeserializer<Instant> {
public GlobalInstantDeserializer() {
super(InstantDeserializer.INSTANT, Instants.FORMATTER);
}
}值得注意的是,與序列化器不同,反序列器是通用的,可以接受任何 Temporal 類型作為反序列化的返回值。
5.2. 使用我們的 InstantSerializer / InstantDeserializer 實現
最後,讓我們配置 Java 時間模塊使用我們的序列化器和反序列器,並進行測試:
@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 數據的可讀性和兼容性,併為應用程序的不同部分提供了日期和時間信息表示的靈活性和控制。