知識庫 / JSON / Jackson RSS 訂閱

Jersey 和 Jackson 自定義 ObjectMapper

Jackson
HongKong
6
09:40 PM · Dec 05 ,2025

1. 簡介

本教程將介紹如何為使用 Jackson 構建的 Jersey 應用程序創建和配置自定義 <em >ObjectMapper</em><em >ObjectMapper</em> 負責將 Java 對象轉換為 JSON,反之亦然。通過自定義它,我們可以集中控制諸如格式、日期處理、字段命名約定和序列化規則等方面。

2. 理解 ObjectMapper

ObjectMapper 是 Jackson 的核心功能,負責將 Java 對象轉換為 JSON 字符串,以及將 JSON 轉換回 Java 對象。 Jersey 自動集成 Jackson,因此我們可以輕鬆地從 REST 端點返回對象並獲取 JSON 響應。

雖然這種默認設置很方便,但它可能無法滿足所有實際應用的需求。例如,Jackson 默認將日期寫入時間戳,這不太易於人類閲讀,並且它不進行 JSON 美化輸出,這可能會使調試更困難。

3. JacksonJaxbJsonProvider 方法 (Jersey 2.x)

在 Jersey 2.x 中,自定義 Jackson 的一種常見方法是擴展 JacksonJaxbJsonProvider。該提供程序充當 Jersey 和 Jackson 之間的橋樑,允許我們注入自定義的 ObjectMapper

使用這種方法,我們可以全局配置 REST 端點中的 JSON 序列化和反序列化方式。

以下是一個簡單的自定義提供程序的示例:

@Provider
public class CustomObjectMapperProvider extends JacksonJaxbJsonProvider {
    public CustomObjectMapperProvider() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

        setMapper(mapper);
    }
}

在本提供程序中,ObjectMapper 已配置為通過啓用縮進,產生更易讀的 JSON。日期以 ISO-8601 字符串的形式寫入,而不是時間戳,通過禁用 WRITE_DATES_AS_TIMESTAMPS 實現。

此外,通過設置 setSerializationInclusion(JsonInclude.Include.NON_NULL),具有空值字段會自動被跳過。 我們還使用 PropertyNamingStrategies.SNAKE_CASE 將 Java 的 camelCase 屬性名稱轉換為 JSON 響應中的 snake_case

通過使用 @Provider 註解並擴展 JacksonJaxbJsonProvider,Jersey 自動檢測並註冊它。一旦該提供程序到位,應用程序中的每個 REST 端點都將使用這些 JSON 規則,而無需在單個資源類中進行額外的配置。

這種方法對於 Jersey 2.x 應用程序非常有效,但它是全局配置。

4. 使用 ContextResolver 和 ObjectMapper 方式 (Jersey 3.x)

在 Jersey 3.x 中,自定義 Jackson 的首選方式是通過實現 ContextResolver<ObjectMapper>。這種方法允許應用程序提供多個 ObjectMapper 配置,並根據模型類或註解條件地選擇它們。

例如,我們可能希望對於公共 API 響應使用更嚴格的規則,而內部模型則包含更多調試信息。 使用 ContextResolver 使這種條件序列化變得簡單、靈活且與 Jersey 3 和現代 Jakarta EE 應用程序完全兼容。

4.1. 項目設置

要開始在 Jersey 3.x 中使用 Jackson,我們需要在我們的 <em >pom.xml</em> 中包含 Jackson Jakarta RS 提供程序。該庫使 Jersey 端點能夠進行 JSON 序列化和反序列化:

<dependency>
    <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
    <artifactId>jackson-jakarta-rs-json-provider</artifactId>
    <version>2.19.1</version>
</dependency>

藉助此依賴項,Jersey 可以使用 Jackson 自動將 Java 對象轉換為 JSON,並將 JSON 解析回 Java 對象。

4.2. 基本 ContextResolver

當我們的應用程序需要不同的 ObjectMapper 設置時,我們使用 ContextResolver<ObjectMapper> 來切換到每個用例的正確配置。

以下是一個簡單的 ContextResolver 示例:

@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {
        mapper = JsonMapper.builder()
          .findAndAddModules()
          .build();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

這段內容翻譯如下:

這個 ContextResolver 提供了一個預配置的 ObjectMapper,包含:

  • 格式化輸出的 JSON (INDENT_OUTPUT)
  • ISO-8601 日期格式 (WRITE_DATES_AS_TIMESTAMPS 已禁用)
  • 跳過空字段 (Include.NON_NULL)
  • 蛇形命名屬性名 (PropertyNamingStrategies.SNAKE_CASE)

我們使用 findAndAddModules() 自動掃描類路徑上的可用模塊並將其註冊到 ObjectMapper 中。 這確保了可選功能,例如從 jackson-datatype-jsr310 中 Java 8 日期/時間支持,在無需手動註冊的情況下自動啓用,使 ObjectMapper 能夠完全意識到項目中的所有模塊。

使用 ContextResolver 具有靈活性,因為我們可以根據類類型提供不同的 ObjectMapper 實例。 這使其成為具有多個 API 模型或變化的序列化規則的應用程序的理想選擇。

4.3. 條件化 ObjectMapper

在實際應用中,我們經常需要針對不同類型對象或 API 場景使用不同的序列化規則。例如,我們可能希望公共 API 響應具有更嚴格的規則,而內部模型則包含更多調試信息。

一個 ContextResolver<ObjectMapper> 可以擴展以支持基於類類型或註解的條件 ObjectMapper 配置。

讓我們創建一個更復雜的 ContextResolver,該 ContextResolver 根據類類型提供不同的 ObjectMapper 配置。

@Provider
public class ConditionalObjectMapperResolver implements ContextResolver<ObjectMapper> {
    private final ObjectMapper publicApiMapper;
    private final ObjectMapper internalApiMapper;
    private final ObjectMapper defaultMapper;

    public ConditionalObjectMapperResolver() {
        publicApiMapper = JsonMapper.builder()
          .findAndAddModules()
          .build();
        publicApiMapper.enable(SerializationFeature.INDENT_OUTPUT);
        publicApiMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        publicApiMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        publicApiMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        publicApiMapper.disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS);

        internalApiMapper = JsonMapper.builder()
          .findAndAddModules()
          .build();
        internalApiMapper.enable(SerializationFeature.INDENT_OUTPUT);
        internalApiMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        internalApiMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        internalApiMapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);

        defaultMapper = JsonMapper.builder()
          .findAndAddModules()
          .build();
        defaultMapper.enable(SerializationFeature.INDENT_OUTPUT);
        defaultMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        if (isPublicApiModel(type)) {
            return publicApiMapper;
        } else if (isInternalApiModel(type)) {
            return internalApiMapper;
        }
        return defaultMapper;
    }

    private boolean isPublicApiModel(Class<?> type) {
        return type.getPackage().getName().contains("public.api") ||
          type.isAnnotationPresent(PublicApi.class);
    }

    private boolean isInternalApiModel(Class<?> type) {
        return type.getPackage().getName().contains("internal.api") ||
          type.isAnnotationPresent(InternalApi.class);
    }
}

4.4. 標記註釋

我們可以創建簡單的註釋來標記模型屬於哪個API類型:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PublicApi {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InternalApi {
}

現在,創建模型類時,帶有 @PublicApi 註解的模型將使用更嚴格的規則,例如跳過空 JSON 數組。

另一方面,帶有 @InternalApi 註解的模型將包含更多用於調試目的的詳細信息,例如在 JSON 輸出中保留空集合。

4.5. 註冊自定義 ObjectMapper

在創建 ContextResolver 後,我們需要告訴 Jersey 使用它。 這可以通過在擴展 ResourceConfig 的類中註冊來實現。

在這裏,我們還可以指定 Jersey 應該掃描哪些包以查找資源類:

public class MyApplication extends ResourceConfig {
    public MyApplication() {
        packages("com.baeldung.model");
        register(ObjectMapperContextResolver.class);
    }
}

通過這種配置,Jersey 將使用我們自定義的 ObjectMapper 處理所有 REST 端點,從而確保 API 跨端點的一致的 JSON 格式。

5. 測試自定義的 ObjectMapper

為了驗證我們的條件型 ObjectMapper 的行為是否符合預期,我們可以編寫單元測試。這些測試表明,不同的模型類型會根據其註解和配置規則進行序列化。

首先,讓我們創建測試模型類:

@PublicApi
public static class PublicApiMessage {
    public String text;
    public LocalDate date;
    public String sensitiveField; 

    public PublicApiMessage(String text, LocalDate date, String sensitiveField) {
        this.text = text;
        this.date = date;
        this.sensitiveField = sensitiveField;
    }
}

@InternalApi
public static class InternalApiMessage {
    public String text;
    public LocalDate date;
    public String debugInfo;
    public List<String> metadata; 

    public InternalApiMessage(String text, LocalDate date, String debugInfo, List<String> metadata) {
        this.text = text;
        this.date = date;
        this.debugInfo = debugInfo;
        this.metadata = metadata;
    }
}

接下來,我們可以設置測試解析器:

@BeforeEach
void setUp() {
    ConditionalObjectMapperResolver resolver = new ConditionalObjectMapperResolver();
    this.publicApiMapper = resolver.getContext(PublicApiMessage.class);
    this.internalApiMapper = resolver.getContext(InternalApiMessage.class);
}

5.1. 公共 API 模型

對於帶有 @PublicApi 標記的模型,我們希望採用更嚴格的序列化規則。例如,敏感字段應被跳過,空值應被排除,並且 JSON 格式應為 snake_case

@Test
void givenPublicApiMessage_whenSerialized_thenOmitsSensitiveFieldAndNulls() throws Exception {
    PublicApiMessage message = new PublicApiMessage("Public Hello!", LocalDate.of(2025, 8, 23), null);

    String json = publicApiMapper.writeValueAsString(message);

    assertTrue(json.contains("text"));
    assertTrue(json.contains("date"));
    assertFalse(json.contains("sensitiveField"));
    assertFalse(json.contains("null"));
}

此測試確認公共 API 映射器對面向公共的數據執行更嚴格的規則。

5.2. 內部 API 模型

對於帶有 @InternalApi 標記的模型,我們希望進行更詳細的序列化。 仍然排除值為 null 的值,但空集合可以保留用於調試目的:

@Test
void givenInternalApiMessageWithEmptyMetadata_whenSerialized_thenIncludesEmptyArraysButNoNulls() throws Exception {
    InternalApiMessage message = new InternalApiMessage("Internal Hello!", LocalDate.of(2025, 8, 23),
      "debug-123", new ArrayList<>());

    String json = internalApiMapper.writeValueAsString(message);

    assertTrue(json.contains("debugInfo"));
    assertFalse(json.contains("null"));
    assertFalse(json.contains("metadata"));
}

如果元數據列表包含值,則應進行序列化。

@Test
void givenInternalApiMessageWithNonEmptyMetadata_whenSerialized_thenMetadataIsIncluded() throws Exception {
    InternalApiMessage message = new InternalApiMessage("Internal Hello!", LocalDate.of(2025, 8, 23),
      "debug-123", Arrays.asList("meta1"));

    String json = internalApiMapper.writeValueAsString(message);

    assertTrue(json.contains("metadata"));
}

5.3. 默認 ObjectMapper

最後,默認的 ObjectMapper 處理沒有特殊註解的模型。可選字段和 <em>metadata</em> 按照正常方式進行序列化:

@Test
void givenDefaultMessage_whenSerialized_thenIncludesOptionalFieldAndMetadata() throws Exception {
    Message message = new Message("Default Hello!", LocalDate.of(2025, 8, 23), "optional");
    message.metadata = new ArrayList<>();

    String json = defaultMapper.writeValueAsString(message);

    assertTrue(json.contains("metadata"));
    assertTrue(json.contains("optionalField") || json.contains("optional"));
}

我們還檢查日期是否以 ISO-8601 格式序列化:

@Test
void givenMessageWithDate_whenSerialized_thenDateIsInIso8601Format() throws Exception {
    Message message = new Message("Date Test", LocalDate.of(2025, 9, 2), "optional");

    String json = defaultMapper.writeValueAsString(message);

    assertTrue(json.contains("2025-09-02"));
}

6. 結論

在本教程中,我們探討了為 Jersey 應用程序自定義 Jackson 的 ObjectMapper 的方法。對於 Jersey 2.x 版本,通過擴展 JacksonJaxbJsonProvider 允許我們全局配置 JSON 序列化,從而確保所有端點都使用一致的格式。這種方法簡單易行,但會應用相同的規則於所有模型。

對於 Jersey 3.x 版本,實現一個 `<ContextResolver<ObjectMapper> 提供了更大的靈活性。我們可以定義多個 ObjectMapper 配置,並根據註解或模型類型進行條件選擇。這種方法確保了現代 Jersey 應用程序中靈活、可維護且一致的 JSON 處理。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.