使用 ObjectMapper 設定即時格式
一、簡介
一致地格式化日期對於保持資料表示的清晰度和相容性至關重要,尤其是在使用 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. 使用自訂JsonSerializer進行完全控制
讓我們從最標準、通用的方法開始,在 Jackson 中使用特定格式序列化非標準字段,擴展JsonSerializer 。這個類別是通用的,我們用它來控制任何欄位的序列化。那麼,讓我們為Instant類型寫一個:
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 包含預期的格式化日期,並且反序列化時,即時字段與我們的原始日期匹配:
@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
}
設定timezone屬性也很重要。由於我們使用“ 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 Time Module 以使用我們的序列化器和反序列化器並測試它:
@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欄位的類別。
六,結論
在本文中,我們擴展了 Jackson 的內建序列化器和反序列化器,並對自訂序列化器有了清晰的了解。我們透過包含@JsonFormat註解和使用擴充模組來利用這些技術。最終,我們可以根據我們的規範一致地格式化Instant欄位。這增強了 JSON 資料的可讀性和相容性,並提供了對應用程式不同部分的日期和時間資訊表示的靈活性和控制。
與往常一樣,原始碼可以在 GitHub 上取得。