Dubbo 泛化调用参数序列化:PojoUtils 泛化/还原的 3 个隐秘陷阱

场景:网关层泛化调用 $invoke 批量调用 Provider,时偶出现 ClassCastException / InstantiationException / StackOverflowError 路径:GenericFilter → PojoUtils.realize0 → newInstance / PojoInvocationHandler / CompatibleTypeUtils → 3 个陷阱各自的源码位置


上篇讲了 Dubbo 线程池满,根因在 Linux 用户线程数限制。这次 Dubbo 泛化调用又报了一个异常——ClassCastException: LinkedHashMap cannot be cast to...。第一反应是 Hessian2 序列化问题,检查了一圈序列化配置,全是正常的。不是序列化的问题。翻到 PojoUtils 的源码才发现——这个异常根本不在序列化阶段,在更前面的参数还原阶段。而且它不是唯一一个——同一条链上还有另外两个陷阱。

【遗迹】3 个异常,散在同一条链的不同节点

网上搜"泛化调用参数序列化异常",给的答案多半是"检查序列化接口""实现 Serializable"。这些答案没错,但它们没说到根上。泛化调用的参数路径经过了 Consumer generalize → Hessian2 序列化 → Provider realize 三个阶段,每个阶段卡住的异常都不一样,但它们全都汇聚在同一个类上——PojoUtils,一个在 dubbo-common 模块里待了十年的工具类。

泛化调用全流程,标注三个异常各在哪个阶段

三个异常,三段堆栈,三个不同的阶段:

异常 1:ClassCastException — LinkedHashMap 冒充了 POJO

Provider 端的日志:

java.lang.ClassCastException:
  java.util.LinkedHashMap cannot be cast to com.example.User$Address
  at com.example.UserService.getAddress(UserService.java)
  at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:187)

GenericFilter.invoke() 在第 107 行调用 PojoUtils.realize(args, params, method.getGenericParameterTypes())——注意第三个参数 method.getGenericParameterTypes() 已经携带了完整泛型信息。对于顶层参数(如 User user),realize0 走的是 具体类路径(行 475-522),通过 getSetterMethod 拿到 setter 的 getGenericParameterTypes()[0],泛型信息完整保留。

问题出在 接口类型字段 上。当 POJO 字段类型是接口(如 List<Address>、自定义 interface)时,realize0 行 471 走织入路径——Proxy.newProxyInstance(..., new PojoInvocationHandler(map))。代理的 PojoInvocationHandler 行 253-254 里调用 realize0(value, method.getReturnType(), null, ...) 时传了 null 做 genericType。嵌套在 List 里的 Address 元素的类型信息就在这个路径上丢失了——子元素被还原为 LinkedHashMap。

这不是 Hessian2 序列化的问题,是 Provider realize 阶段 接口类型字段的代理路径泛型丢失。

异常1 堆栈截图

异常 2:InstantiationException — 无默认构造函数的 POJO 还原失败

java.lang.RuntimeException: Failed to instantiate POJO: com.example.OrderDetail
  at org.apache.dubbo.common.utils.PojoUtils.newInstance(PojoUtils.java:597)
  at org.apache.dubbo.common.utils.PojoUtils.realize0(PojoUtils.java:476)
  at org.apache.dubbo.common.utils.PojoUtils.realize(PojoUtils.java:227)
  at org.apache.dubbo.rpc.filter.GenericFilter.invoke(GenericFilter.java:107)

Caused by: java.lang.InstantiationException: com.example.OrderDetail
  at java.lang.Class.newInstance(Class.java)
  at org.apache.dubbo.common.utils.PojoUtils.newInstance(PojoUtils.java:570)
  ... 3 more

PojoUtils.newInstance() 内部先用 cls.newInstance() 创建实例,失败后尝试通过有参构造器+默认值兜底(行 573-597),兜底也失败则抛出 RuntimeException,原始 InstantiationException 作为 cause。根因不变——类没有 public 无参构造器。Lombok 的 @AllArgsConstructor 不加 @NoArgsConstructor 是最常见的触发场景。

这个异常也发生在 Provider realize 阶段,在泛型擦除之前的反射构造环节。

异常2 堆栈 + POJO 类结构

异常 3:StackOverflowError — realize0 中的循环 Map 无人看守

java.lang.StackOverflowError
  at org.apache.dubbo.common.utils.PojoUtils.realize0(PojoUtils.java:478)
  at org.apache.dubbo.common.utils.PojoUtils.realize0(PojoUtils.java:478)
  ... (重复几百次)

这个异常不在 generalize() 阶段——Dubbo 2.7.23 的 generalize() 内部已有 IdentityHashMap 历史记录(行 115-117),对引用相等的循环对象做了防护。异常发生在 Provider realize 阶段

问题的根源在 realize0IdentityHashMap引用相等==)判断是否已访问。当 Hessian2 反序列化从 Consumer 发来的泛化 Map 数据时,如果 POJO 的父子双向引用在序列化时被 Hessian2 的 ref 机制还原为同一对象引用,history.get(pojo) 能正确识别循环。但如果是 JSON 序列化(Gson generic)、或跨协议传输导致引用链断裂,两个逻辑上相等的 Map 变成了两个不同实例——IdentityHashMap 认不出循环,realize0 在 Map→POJO→Map→POJO 中无限递归,最终栈溢出。

异常3 堆栈截图

【发掘】从 GenericFilter 到 PojoUtils 的源码追踪

泛化调用在 Provider 端由 GenericFilter 拦截 $invoke 方法,在 Consumer 端由 GenericImplFilter 拦截。不管是哪一端,参数转换都落到 PojoUtils 的两个方法上。

泛化调用的入口:GenericFilter.invoke()

Provider 端收到 $invoke 调用后的处理逻辑(Dubbo 2.7.23,位于 dubbo-rpc-api 模块 GenericFilter.java:74):

public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
    if (inv.getMethodName().equals($INVOKE) && inv.getArguments() != null
            && inv.getArguments().length == 3
            && !GenericService.class.isAssignableFrom(invoker.getInterface())) {
        String name = (String) inv.getArguments()[0];
        String[] types = (String[]) inv.getArguments()[1];
        Object[] args = (Object[]) inv.getArguments()[2];

        Method method = ReflectUtils.findMethodByMethodSignature(
                invoker.getInterface(), name, types);
        Class<?>[] params = method.getParameterTypes();

        // 关键:第三个参数 method.getGenericParameterTypes() 携带泛型
        args = PojoUtils.realize(args, params,
                method.getGenericParameterTypes());   // ← 行 107

        RpcInvocation rpcInvocation = new RpcInvocation(method,
                invoker.getInterface().getName(), ... , args, ...);
        return invoker.invoke(rpcInvocation);
    }
    return invoker.invoke(inv);
}

注意行 107——PojoUtils.realize(args, params, method.getGenericParameterTypes())。第三个参数是 带泛型信息的 Type[],不是 Raw Type。所以异常 1 的根因不在这一层。问题出在更深层的 realize0 处理接口类型字段时的代理路径。

GenericFilter.invoke() 源码

PojoUtils.realize0(Map → POJO):两种路径,两种行为

realize() 的核心入口是 realize0()(Dubbo 2.7.23,dubbo-common 模块):

public static Object realize(Object pojo, Class<?> type, Type genericType) {
    return realize0(pojo, type, genericType,
            new IdentityHashMap<>());           // ← 行 226-228
}

private static Object realize0(Object pojo, Class<?> type,
        Type genericType, Map<Object, Object> history) {

    if (pojo instanceof Map<?, ?> && type != null) {
        // ... 检查 map 中 "class" 键 ...

        if (type.isInterface()) {
            // ── 路径 A:接口 → JDK 动态代理(异常 1 的发源地)
            return Proxy.newProxyInstance(..., new PojoInvocationHandler(map));
        } else {
            // ── 路径 B:具体类 → newInstance + 反射 setter(泛型保留)
            Object dest = newInstance(type);  // ← 异常 2
            for (Map.Entry<Object, Object> entry : map.entrySet()) {
                if (key instanceof String) {
                    Method setter = getSetterMethod(..., value.getClass());
                    Type ptype = setter.getGenericParameterTypes()[0];
                    // ↑ 关键:setter 的泛型参数被保留
                    value = realize0(value, setter.getParameterTypes()[0],
                            ptype, history);
                    setter.invoke(dest, value);
                }
            }
            return dest;
        }
    }
    return pojo;
}

路径 B(具体类 + 反射 setter)getSetterMethod 找到 setter 后,getGenericParameterTypes()[0] 拿到完整的 ParameterizedType(如 List<Address>),嵌套泛型信息完整保留。异常 1 不在这里发生

路径 A(接口 → 动态代理)PojoInvocationHandler 被调用时,行 253-254:

if (value instanceof Map && !Map.class.isAssignableFrom(method.getReturnType())) {
    value = realize0(value, method.getReturnType(), null, history);
    //                                               ↑ null = 泛型丢失!
}

第三个参数传 null 而不是 ParameterizedType。当用户通过代理调用 getAddresses() 返回 List<Address> 时,method.getReturnType() 只能给出 List.class(Raw Type),嵌套的 Address 类型信息在递归 realize0 的 Collection 分支中丢失——子元素被还原为 LinkedHashMap。这就异常 1。

PojoUtils.realize() 源码高亮

PojoUtils.generalize(POJO → Map):引用相等的循环防护

Consumer 端的 GenericImplFilter 行 90 在发送请求前调用 PojoUtils.generalize(arguments)。Dubbo 2.7.23 的实现已经有 visited 历史保护:

// PojoUtils.generalize() — Dubbo 2.7.23
public static Object generalize(Object pojo) {
    return generalize(pojo, new IdentityHashMap<>());  // ← visited 集合
}

private static Object generalize(Object pojo, Map<Object, Object> history) {
    // ... 基本类型/枚举/日期直接返回 ...

    Object o = history.get(pojo);         // ← 检查是否已访问
    if (o != null) return o;              // ← 已访问 → 返回

    history.put(pojo, pojo);              // ← 先占位
    Map<String, Object> map = new HashMap<>();
    history.put(pojo, map);               // ← 覆盖为真实 Map

    for (Method method : pojo.getClass().getMethods()) {
        if (ReflectUtils.isBeanPropertyReadMethod(method)) {
            map.put(propertyName, generalize(method.invoke(pojo), history));
        }
    }
    return map;
}

generalize()IdentityHashMap(引用相等)追踪已访问对象——同一个 Parent 实例在 Parent→Child→Parent 环路中被第二次访问时,history.get(pojo) 返回已创建的 parent Map,递归终结。所以异常 3 不在 generalize 阶段。问题出现在后面的 realize 阶段,那里的 IdentityHashMap 在跨协议反序列化后"认不出"逻辑上相同但引用不同的 Map 实例。

PojoUtils.generalize() 源码高亮

【路径】3 个异常的 IDE 排查路径

PojoUtils 是三个异常的共同入口(Dubbo 2.7.x)

异常 入口类 关键方法 IDE 路径
ClassCastException PojoUtils (dubbo-common) realize() Ctrl+Shift+N → PojoUtils → Ctrl+F "realize" → 找到 Type 参数为 null 的分支
InstantiationException PojoUtils (dubbo-common) realize() Ctrl+Shift+N → PojoUtils → Ctrl+F "newInstance" → 检查 cls.newInstance() 调用处
StackOverflowError PojoUtils (dubbo-common) realize0() Ctrl+Shift+N → PojoUtils → Ctrl+F "realize0" → 找 history.get(pojo) + Proxy.newProxyInstance 分支

三个异常的排查路径都是同一个类——PojoUtils。区别只在看哪个方法和什么调用条件。

IDE 搜索路径 × 3

【解读】为什么这样设计

为什么 PojoUtils 用反射 + Map 中间表示而不是 JSON 序列化?

泛化调用需要跨序列化协议工作(Hessian2 / Java / JSON)。Map 是所有协议都能处理的中间表示——它是"最小公分母"。如果选 JSON 作为中间格式,Hessian2 序列化的场景就需要多一次 JSON 编解码,在网关这种高吞吐场景下性能不可接受。

为什么具体类+setter能保留泛型,接口+代理不能?

关键区别在路径 A 和 B 的源码实现上。

路径 B(具体类 + 反射 setter)GenericFilter.invoke() 行 107 调用 PojoUtils.realize(args, params, method.getGenericParameterTypes()) 时,第三个参数 method.getGenericParameterTypes() 是带泛型信息的 Type[]realize0 进入具体类路径后,getSetterMethod 找到 setter,setter.getGenericParameterTypes()[0] 拿到完整的 ParameterizedType(如 List<Address>),嵌套泛型信息完整保留。

路径 A(接口 → 动态代理)PojoInvocationHandler 行 253-254 调用 realize0(value, method.getReturnType(), null, history) 时第三个参数传死了 null——因为代理的 method.getReturnType() 只能返回 List.class 这样的 Raw Type,无法从接口方法签名拿到 ParameterizedType。嵌套的 Address 类型信息在递归 realize0 的 Collection 分支中丢失。

这不是泛化调用协议的缺陷——是 JDK 动态代理 InvocationHandler 的固有局限:代理方法拿不到声明站点(接口)的 ParameterizedType

为什么 realize0 的 IdentityHashMap 挡不住跨协议循环?

generalize()realize0() 都用 IdentityHashMap 做 visited 保护,但它们的保护范围不同——只在同一 JVM 内的同一引用链上生效。当数据经过网络传输反序列化后,原来逻辑上"同一个"对象变成了两个不同的 Java 实例。realize0 处理 Provider 端从 Hessian2/JSON 重建的 Map 数据时,A→parent→child→parent 路径上的两个 "parent" Map 是不同对象——IdentityHashMap== 判断失效。

网上答案错在哪

搜"ClassCastException in Dubbo"——90% 的答案说"检查 Serializable 接口"。但异常 1 和 Serializable 无关,是 realize 阶段的泛型擦除。

搜"Dubbo InstantiationException"——回答多是"检查类是否 public"。但更深层的原因是 Lombok 时代开发者习惯只用 @AllArgsConstructor

搜"StackOverflowError in Dubbo"——如果你用的是 Dubbo 2.7.23+,StackOverflow 大概率不在 generalize 阶段(它已有循环保护)。看看堆栈顶端是不是 realize0——如果是,问题出在跨协议反序列化后的 Map 循环引用。

泛化调用数据流全图 —— 标注异常位置 + 网上答案误区

【收获】排查锚点 + 预防指南

排查锚点

你花 10 分钟跟读完 PojoUtils 这个类,以后遇到上述 3 个异常不用搜。

  • ClassCastException → LinkedHashMap:POJO 用了接口类型(interface)而非具体类。Dubbo 2.7.23 具体类+setter路径能保留嵌套泛型,但接口类型走动态代理路径 method.getReturnType() 拿不到 ParameterizedType。排查路径:PojoUtils.realize0()type.isInterface() 分支 → PojoInvocationHandler 行 253-254。
  • InstantiationException + realize 堆栈:目标 POJO 缺少无参构造器。Lombok 用户特别容易中招。排查路径:找到堆栈中 cls.newInstance()cls 是哪个类,加 @NoArgsConstructor
  • StackOverflowError + realize0 堆栈:POJO 循环引用在跨协议传输后 IdentityHashMap 防护失效。排查路径:先确认序列化协议——JSON/Gson 序列化几乎必触发。切回 Hessian2 或检查实体双向关联,用 transient 打断。

一个异常堆栈等于一个类的某个方法出了问题。这三个异常都指向 PojoUtils——泛化调用的序列化问题,90% 可以直接翻这个类。

预防三原则

  1. 泛化调用的 POJO 用具体类,不用接口:具体类+setter路径的 getGenericParameterTypes() 能保留 ParameterizedType;接口类型走动态代理会丢失泛型信息
  2. 无参构造器是必须的:Lombok 用 @Data(自带 @NoArgsConstructor + @AllArgsConstructor)或显式加 @NoArgsConstructor
  3. 优先用 Hessian2 序列化:Hessian2 的 ref 机制能保留对象引用关系,IdentityHashMap== 判断才能生效。用 JSON 序列化做泛化调用时,循环引用几乎必触发 StackOverflow
  4. 或显式切断循环链:双向引用的一方加 transient@JSONField(serialize = false)
// 预防三原则代码示例
// ✅ POJO 用具体类(不要用接口)
@Data
public class UserRequest {
    private Long id;
    private String name;
    private Address address;        // 具体类→setter路径泛型保留,安全
    private List<Address> addrs;    // 具体类+List也安全(setter有ParameterizedType)
}

// ✅ 显式切断循环链(用 transient 关键字,非 @Transient 注解)
public class Report {
    private Long id;
    private transient ReportSummary summary;  // transient 打断双向引用
}

预防三原则代码截图

框架的作者不是随机写这行代码的——PojoUtils.realize()cls.newInstance() 这行代码在 2014 年的 Dubbo 2.5 版本就已经在了。十年后的今天,Lombok 流行让无参构造器不再是默认项,同样的代码就变成了生产陷阱。

下次遇到泛化调用的参数序列化异常,先打开 PojoUtils 的 realize()generalize() 方法——3 个陷阱的源码都在那里。30 行关键代码,让你绕过 3 个坑。


🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:Ai拆代码的曹操

下篇我们聊 Dubbo 异步调用回调未执行——从 DefaultFutureRequest 的完整追踪。