Jackson 对于多态子类的自动反序列化

我正在开发一款 QQ 机器人应用程序。作为后台服务,它与前台客户端通信使用的是 OneBot 协议。

OneBot 协议客户端上报 QQ 事件的方式非常怪异,给我的开发造成很大困扰。

背景

事件 是 OneBot 对于上报内容的统称,有四种分类:

  • 消息事件

  • 通知事件

  • 请求事件

  • 元事件

不同的事件上报的路径完全相同,用于区分不同事件的方式是某个关键字段的值。

image.png

不同的事件仅共享部分字段,而根据字段值的不同,又在其后拥有自己独有的字段。事件下还有子事件,子事件的也采用这种区分方式。

问题

我使用的是 SpringBoot 作为开发应用的框架,它会对收到的请求自动进行反序列化,并注入 Controller 层的方法参数中。

但是由于 OneBot 不友好的协议设计,导致原生 SpringBoot 无法自动将上报事件映射为各子实体类,于是我强迫症犯了,必须要找到一个方法完美解决这个问题

解决

检索许久,在网上查找到这样一份博客 10 分钟轻松学会 Jackson 反序列化自动适配子类,看起来符合我的要求。

简单说就是用 JsonTypeInfo 标识基类以定义映射子类的原则;然后用一个自定义注解标识子类;应用启动时自动扫描标注了自定义注解的子类,将其与父类的映射关系注册到 ObjectMapper 中,Jackson 在反序列化时便可以自动 “找到” 子类

代码

我让 AI 根据这个思路写了一份代码,注释如下。关键方法分别为 performScan()applyTo() 。前者执行扫描工作,并将父、子类的映射关系保存到自身缓存中;后者提供一个外部接口,用于配置类提供 ObjectMapper 时将扫描结果注册进去。

java
@Component
@Slf4j
public class PolymorphicRegistrationProcessor {

    // 存储基类与其所有子类的映射关系
    private final Map<Class<?>, Set<Class<?>>> polymorphicMappings = new ConcurrentHashMap<>();
    private final Map<Class<?>, String> typeNameMap = new ConcurrentHashMap<>();

    private volatile boolean initialized = false;

    public PolymorphicRegistrationProcessor() {
        // 预注册基类
        registerPolymorphicBaseClass(OneBotEvent.class);
        registerPolymorphicBaseClass(MessageSegment.class);
    }

    /**
     * 注册基类
     * @param baseClass
     */
    public void registerPolymorphicBaseClass(Class<?> baseClass) {
        polymorphicMappings.putIfAbsent(baseClass, new HashSet<>());
    }

    /**
     * 注册子类到指定基类
     * @param baseClass 基类
     * @param subType 子类
     */
    public void registerSubType(Class<?> baseClass, Class<?> subType) {
        // 确保基类在映射表中存在,然后添加子类
        polymorphicMappings.computeIfAbsent(baseClass, k -> new HashSet<>())
                .add(subType);
    }

    /**
     * 初始化
     */
    public void initializeIfNeeded() {
        if (initialized) return;

        try {
            performScan();
            initialized = true;
            log.info("Polymorphic types registered.");
        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize polymorphic types", e);
        }
    }

    /**
     * 核心:执行包扫描并注册多态类型到缓存中
     * 1. 使用 Spring 扫描器查找所有带 @PolymorphicSubType 注解的类
     * 2. 解析注解中的类型名称(或自动推断)
     * 3. 确定子类的基类(通过父类是否包含 @JsonTypeInfo)
     * 4. 将子类注册到基类的映射表中
     */
    private void performScan() throws Exception {
        // 很难想象这个类的命名人的心理状态
        ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(false);

        // 规定扫描对象为携带了 @PolymorphicSubType 注解的类
        scanner.addIncludeFilter(new AnnotationTypeFilter(PolymorphicSubType.class));

        // 扫描路径
        String packageToScan = "com.yuier.yuni.core";
        // 执行扫描
        java.util.Set<org.springframework.beans.factory.config.BeanDefinition> candidates =
                scanner.findCandidateComponents(packageToScan);

        // 遍历所有候选类
        for (org.springframework.beans.factory.config.BeanDefinition candidate : candidates) {
            String className = candidate.getBeanClassName();

            try {
                Class<?> clazz = Class.forName(className);
                PolymorphicSubType annotation = clazz.getAnnotation(PolymorphicSubType.class);

                // 从注解获取子类的类型名称。在本应用案例中,这个名称会在父类的关键字段的值中体现
                String typeName = annotation.value();
                if (typeName == null || typeName.trim().isEmpty()) {
                    typeName = inferTypeName(clazz);  // 如果子类注解中没有提供本子类的名称,则使用自动推断
                }
                typeNameMap.put(clazz, typeName);

                // 查找子类打了 @JsonTypeInfo 注解的基类
                Class<?> baseClass = findPolymorphicBaseClass(clazz);
                if (baseClass != null) {
                    // 注册到缓存中
                    registerSubType(baseClass, clazz);
                }
            } catch (Exception e) {
                System.err.println("处理类失败: " + className);
                e.printStackTrace();
            }
        }
    }

    /**
     * 提供给外部的接口,供应用启动时注册 ObjectMapper 使用
     * @param mapper ObjectMapper
     */
    public void applyTo(ObjectMapper mapper) {
        initializeIfNeeded(); // 确保完成初始化

        // 遍历所有缓存中的映射
        for (Map.Entry<Class<?>, Set<Class<?>>> entry : polymorphicMappings.entrySet()) {
            Set<Class<?>> subTypes = entry.getValue();

            // 为每个子类生成 NamedType
            List<NamedType> namedTypes = subTypes.stream()
                    .map(clazz -> new NamedType(clazz, typeNameMap.getOrDefault(clazz, inferTypeName(clazz))))
                    .toList();

            if (!namedTypes.isEmpty()) {
                mapper.registerSubtypes(namedTypes.toArray(new NamedType[0]));
            }
        }
    }

    /**
     * 自动推断类型名称(用于JSON序列化标识)
     * 规则:
     * 1. 类名以 "Event" 结尾 → 去掉 "Event" 后转小写(如 MessageEvent → message)
     * 2. 类名以 "Segment" 结尾 → 去掉 "Segment" 后转小写(如 ImageSegment → image)
     * 3. 其他情况 → 直接转小写
     * @param clazz
     * @return 类型名称
     */
    private String inferTypeName(Class<?> clazz) {
        String simpleName = clazz.getSimpleName();
        if (simpleName.endsWith("Event")) {
            return lowerFirst(simpleName.substring(0, simpleName.length() - 5));
        } else if (simpleName.endsWith("Segment")) {
            return lowerFirst(simpleName.substring(0, simpleName.length() - 7));
        }
        return lowerFirst(simpleName);
    }

    private String lowerFirst(String str) {
        if (str == null || str.isEmpty()) return str;
        return Character.toLowerCase(str.charAt(0)) + str.substring(1);
    }

    /**
     * 递归查找子类的基类
     * 逻辑:
     * 1. 从子类开始向上遍历父类链
     * 2. 遇到第一个包含 @JsonTypeInfo 注解的父类即为基类
     * @param subType 子类
     * @return 基类
     */
    private Class<?> findPolymorphicBaseClass(Class<?> subType) {
        Class<?> current = subType.getSuperclass();
        while (current != null && current != Object.class) {
            if (current.isAnnotationPresent(com.fasterxml.jackson.annotation.JsonTypeInfo.class)) {
                return current;
            }
            current = current.getSuperclass();
        }
        return null;
    }

    public Set<Class<?>> getSubTypes(Class<?> baseClass) {
        return polymorphicMappings.getOrDefault(baseClass, Collections.emptySet());
    }
}

其他相关代码

java
// 配置类,用于提供 ObjectMapper
@Configuration
public class JacksonConfig {

    @Autowired
    private final PolymorphicRegistrationProcessor registrationProcessor;

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        // 对于实体类中未定义的字段不进行反序列化
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 反序列化时下划线风格转驼峰风格
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

        registrationProcessor.applyTo(mapper);
        return mapper;
    }
}
java
// 父类上添加的注解
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,  // 指定类型标识的生成方式为类的简短名称
        include = JsonTypeInfo.As.EXISTING_PROPERTY,  // 类型信息已经存在于 json 中,Jackson 无需额外添加
        property = "post_type",  // 存储类型信息的关键字段
        visible = true  // 反序列化后的结果显示关键字段,即此处的 postType 字段
)
public class OneBotEvent {

}
java
// 子类只需要添加一个自定义注解即可
@PolymorphicSubType
public class MessageEvent extends OneBotEvent {
    
}
Java 应用下简单插件系统的设计 - 插件代码开发与插件 jar 加载
RPC 架构和 Dubbo 框架