1. 引言

在处理JSON数据时,统一日期格式对保持数据清晰性和兼容性至关重要。本教程将探讨使用Jackson的ObjectMapper在序列化时格式化Instant字段,并在反序列化时解析回原始对象的多种技术方案。我们还将讨论@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));

    // ...
}

测试基础方案包括验证两点:序列化后JSON字符串是否包含预期日期格式,以及反序列化后的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来控制非标准字段的序列化。该类是泛型的,可用于控制任意字段的序列化。我们为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()获取字段值,传给formatter解析:

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扩展

Jackson默认不支持Instant类型,需在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将日期字段序列化为数字时间戳。在JsonMapper.builder()中调用disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)可禁用此行为,但无法指定具体格式。 因为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"时区和开头定义的DATE_FORMAT。

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);
}

由于使用了@JsonFormat,无需禁用WRITE_DATES_AS_TIMESTAMPS。

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);
    }
}

使用接收单例和formatter的构造函数。传入false给useTimestamp和useNanoseconds,因为我们希望Instant字段使用特定格式。此时类中无需任何注解:

public class History implements TimeStampTracker {

    private Instant timeStamp;

    // standard getters and setters
}

5.1. 扩展InstantDeserializer实现自定义格式

反序列化时,扩展InstantDeserializer并用InstantDeserializer.INSTANT常量和我们的formatter构造:

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字段的类。

6. 总结

本文通过扩展Jackson内置序列化器/反序列化器,深入理解了自定义实现机制。我们结合@JsonFormat注解和扩展模块,实现了Instant字段的统一格式化。这不仅提升了JSON数据的可读性和兼容性,更在应用各层提供了日期时间表示的灵活控制力。

完整源码请见GitHub仓库


原始标题:Set Format for Instant Using ObjectMapper | Baeldung