1概述

在这个教程中,我们将探讨如何利用jackson库中的推断式多态功能。

2. 基于名称的多态性

设想我们有一个如下面图像所示的类结构:


访问图片

首先,NamedCharacterImperialSpy 类实现了 Character 接口。其次,KingKnight 类实现了 NamedCharacter 类。最后,我们有一个 ControlledCharacter 类,它包含玩家控制的角色引用。

我们的目标是将JSON对象解析为Java对象,而无需修改接收到的JSON结构。

让我们看看类的定义。请注意,对于基接口,我们需要使用Jackson注解来声明我们将使用的推断,同时还需要添加@JsonSubTypes注解来声明我们想要推断的类。

@JsonTypeInfo(use = Id.NAME)
@JsonSubTypes({ @Type(ImperialSpy.class), @Type(King.class), @Type(Knight.class) })
public interface Character {
}

此外,我们还可以在Character接口和KingKnight类之间有一个中介类。因此,Jackson也知道在这种情况下如何推断多态:

public class NamedCharacter implements Character {
    private String name;

    // standard setters and getters
}

接下来,我们将实现Character接口的子类。我们在前面的代码示例中已经声明了这些子类作为子类型,所以实现中并不依赖于Jackson库:

public class ImperialSpy implements Character {
}
public class King extends NamedCharacter {
    private String land;

    // standard setters and getters
}
public class Knight extends NamedCharacter {
    private String weapon;

    // standard setters and getters
}

我们希望映射的一个JSON示例如下:

{
    "name": "Old King Allant",
    "land": "Boletaria",
}

首先,如果我们尝试读取上述JSON结构,Jackson会抛出一个运行时异常,消息为:“无法解析[简单类型,com.baeldung.jackson.deductionbasedpolymorphism.Character]的子类型:缺少'type'属性的类型标识。”

@Test
void givenAKingWithoutType_whenMapping_thenExpectAnError() {
    String kingJson = formatJson("{'name': 'Old King Allant', 'land':'Boletaria'}");
    assertThrows(InvalidTypeIdException.class, () -> objectMapper.readValue(kingJson, Character.class));
}

此外,formatJson工具方法通过将引号转换为双引号,帮助我们在测试代码中保持简洁,因为JSON需要这样:

public static String formatJson(String input) {
    return input.replaceAll("'", "\"");
}

因此,为了能够根据类型进行多态推断,我们必须修改JSON结构,并显式添加对象的类型。这就意味着我们将多态行为与JSON结构耦合在一起:

{
    "@type": "King"
    "name": "Old King Allant",
    "land": "Boletaria",
}
@Test
void givenAKing_whenMapping_thenExpectAKingType() throws Exception {
    String kingJson = formatJson("{'name': 'Old King Allant', 'land':'Boletaria', '@type':'King'}");

    Character character = objectMapper.readValue(kingJson, Character.class);
    assertTrue(character instanceof King);
    assertSame(character.getClass(), King.class);
    King king = (King) character;
    assertEquals("Boletaria", king.getLand());
}

3. 推断式多态性

要启用推断式多态性,我们唯一需要做的就是使用@JsonTypeInfo(use = Id.DEDUCTION)

@JsonTypeInfo(use = Id.DEDUCTION)
@JsonSubTypes({ @Type(ImperialSpy.class), @Type(King.class), @Type(Knight.class) })
public interface Character {
}

4. 简单推断

现在,让我们探索如何以多态方式读取JSON,使用简单的推断。我们要读取的对象如下:

{
    "name": "Ostrava, of Boletaria",
    "weapon": "Rune Sword",
}

首先,我们将值读入Character对象,然后测试Jackson是否正确推断了JSON的类型:

@Test
void givenAKnight_whenMapping_thenExpectAKnightType() throws Exception {
    String knightJson = formatJson("{'name':'Ostrava, of Boletaria', 'weapon':'Rune Sword'}");

    Character character = objectMapper.readValue(knightJson, Character.class);

    assertTrue(character instanceof Knight);
    assertSame(character.getClass(), Knight.class);
    Knight king = (Knight) character;
    assertEquals("Ostrava, of Boletaria", king.getName());
    assertEquals("Rune Sword", king.getWeapon());
}

此外,如果JSON是一个空对象,Jackson将把它解释为没有属性的ImperialSpy类:

@Test
void givenAnEmptyObject_whenMapping_thenExpectAnImperialSpy() throws Exception {
    String imperialSpyJson = "{}";

    Character character = objectMapper.readValue(imperialSpyJson, Character.class);

    assertTrue(character instanceof ImperialSpy);
}

而且,Jackson还会将空的JSON对象推断为null对象

@Test
void givenANullObject_whenMapping_thenExpectANullObject() throws Exception {
    Character character = objectMapper.readValue("null", Character.class);

    assertNull(character);
}

5. 不区分大小写的推断

Jackson也可以在属性名称大小写不匹配的情况下推断多态性。首先,我们需要创建一个启用ACCEPT_CASE_INSENSITIVE_PROPERTIES的ObjectMapper:

ObjectMapper objectMapper = JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();

然后,使用已实例化的objectMapper,我们可以测试多态性是否正确推断:

{
    "NaMe": "Ostrava, of Boletaria",
    "WeaPON": "Rune Sword",
}
@Test
void givenACaseInsensitiveKnight_whenMapping_thenExpectKnight() throws Exception {
    String knightJson = formatJson("{'NaMe':'Ostrava, of Boletaria', 'WeaPON':'Rune Sword'}");

    Character character = objectMapper.readValue(knightJson, Character.class);

    assertTrue(character instanceof Knight);
    assertSame(character.getClass(), Knight.class);
    Knight knight = (Knight) character;
    assertEquals("Ostrava, of Boletaria", knight.getName());
    assertEquals("Rune Sword", knight.getWeapon());
}

6. 包含的推断

我们还可以推断嵌套在其他对象中的对象的多态性。我们将使用ControlledCharacter类的定义来演示以下JSON的映射:

{
    "character": {
        "name": "Ostrava, of Boletaria",
        "weapon": "Rune Sword"
    }
}
@Test
void givenAKnightControlledCharacter_whenMapping_thenExpectAControlledCharacterWithKnight() throws Exception {
    String controlledCharacterJson = formatJson("{'character': {'name': 'Ostrava, of Boletaria', 'weapon': 'Rune Sword'}}");

    ControlledCharacter controlledCharacter = objectMapper.readValue(controlledCharacterJson, ControlledCharacter.class);
    Character character = controlledCharacter.getCharacter();

    assertTrue(character instanceof Knight);
    assertSame(character.getClass(), Knight.class);
    Knight knight = (Knight) character;
    assertEquals("Ostrava, of Boletaria", knight.getName());
    assertEquals("Rune Sword", knight.getWeapon());
}

7. 总结

在这个教程中,我们探讨了如何使用Jackson库实现推断式多态性。

本文的源代码可以在GitHub上找到