使用 Jersey 和 Jackson 自訂 ObjectMapper
1. 簡介
在本教程中, ObjectMapper
將探索如何使用 Jackson 為Jersey應用程式建立和配置自訂ObjectMapper
負責將 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
。
使用這種方法,我們可以全域配置 JSON 在應用程式的所有 REST 端點上的序列化和反序列化方式。
以下是自訂提供者的一個簡單範例:
@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。透過停用WRITE_DATES_AS_TIMESTAMPS
,日期將以 ISO-8601 字串而不是時間戳記的形式寫入。
此外,透過設定setSerializationInclusion(JsonInclude.Include.NON_NULL)
,可以自動跳過具有空值的欄位。我們也使用PropertyNamingStrategies.SNAKE_CASE
將 Java camelCase
命名 (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,我們需要在pom.xml
中包含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-jsr31
0 中對 Java 8 日期/時間的支援)無需手動註冊即可自動啟用,從而使ObjectMapper
完全了解專案中的所有模組。
使用ContextResolver
非常靈活,因為我們可以根據類別的類型提供不同的ObjectMapper
實例。這對於具有多種 API 模型或不同序列化規則的應用程式來說非常理想。
4.3. 條件ObjectMapper
在實際應用中,我們通常需要針對不同類型的物件或 API 上下文制定不同的序列化規則。例如,我們可能需要對公共 API 回應制定更嚴格的規則,而內部模型則包含更多偵錯資訊。
可以擴充ContextResolver<ObjectMapper>
以支援基於類別類型或註解的條件ObjectMapper
配置。
讓我們建立一個更複雜的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 將對所有 REST 端點使用我們自訂的ObjectMapper
,確保整個 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
註解的模型,我們需要更詳細的序列化資訊。空值仍然會被排除,但為了方便調試,可以保留空集合:
@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
處理沒有特殊註解的模型。可選欄位和metadata
可以正常序列化:
@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 處理的靈活性、可維護性和一致性。
與往常一樣,原始碼可在 GitHub 上取得。