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 處理。