个典型的 Java 泛型在反序列化场景下“类型擦除 + 无法推断具体类型”导致的隐性 Bug

发布于:2025-06-14 ⋅ 阅读:(13) ⋅ 点赞:(0)

今天遇到一个问题:一个典型的 Java 泛型在反序列化场景下“类型擦除 + 无法推断具体类型”导致的隐性 Bug,尤其是在 RPC(如 DubboFeign 等)和 本地 JVM 内直连调用共存时,这种问题会显现得非常明显。

A 服务暴露了一个 RPC 接口规范,如下:

public class WeaResult<T> implements Serializable {
    private static final long serialVersionUID = 15869325700230991L;
    @ApiModelProperty("状态码")
    private int code;
    @ApiModelProperty("提示信息")
    private String msg;
    @ApiModelProperty("状态")
    private boolean status;
    @ApiModelProperty("数据")
    private T data;
}

定义的 RPC 接口如下:

WeaResult selectDetail(RuleTypeSettingDto ruleTypeSettingDto);

API 中的返回值没有声明泛型 <T> 的具体类型。然后被 B 服务调用了,远程调用代码:

private Integer isMultiMode(AllocationRuleDto request) {
        return Optional.ofNullable(ruleTypeSettingService.selectDetail(RuleTypeSettingDto.builder()
                        .moduleName(AllocationComponent.CUSTOMER_SERVICE)
                        .typeId(request.getTypeId())
                        .tenantKey(request.getTenantKey())
                        .typeName("cs").build()))
                .map(WeaResult::getData)
                .map(data ->(Map<?,?>)data)
                .map(dataMap -> dataMap.get("sceneType"))
                .map(Object::toString).map(Integer::valueOf)
                .orElse(0);
    }

接受到结果,只能硬着头皮强转,获取对应值。

这里解释下,为什么要强转?

当是 RPC 场景(如 JSON 序列化传输)时,框架通常会把 data 转换为 Map<String, Object>(比如 JSON 默认映射到 HashMap),所以我这里直接强转成 Map 类型:

map(data -> (Map<?,?>) data)

这样是能够能运行的,没啥问题。

但是,重点来了,当是A 和 B 服务合并单体时部署时(在同一个 JVM 中,或者说是本地部署),就会直接返回原始的具体类型对象(比如是 RuleTypeSettingVo),此时 (Map<?, ?>) data 就会抛 ClassCastException —— 因为根本不是 Map!所以这个就是一个巨坑!这就是没有合理定义 API 接口导致的,并且泛型一定一定要注明清楚。否则调用方永远只是一个盲区。

提示:这里的合并指的是将服务提供者和消费者都合并成一个单体服务部署。可能是节省客户资源。


那么怎么去正确改进呢?
方法一:指定泛型类型,让接口明确返回结构
WeaResult<RuleTypeSettingVo> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);

这样无论是远程调用还是本地调用,返回值类型一致,调用方可以安全地 (Map),但是不推荐用 RuleTypeSettingVo 还是,大部分都是按照实体返回。所以,定义 API 规范时,一定要明确所有出入参,以及涉及到的泛型。

另外,定义了这种 WeaResultcode + status 返回的,一定要优先判断 code + status。否则,你一定会吃大亏,code + status 可以让我们在调用远程接口时减少很多不必要的麻烦

方法二:在调用方显式判断类型(不推荐)

如果你不能修改接口,但调用方需要容错处理,可以使用:

Object data = ruleTypeSettingService.selectDetail(...).getData();
Map<?, ?> dataMap;
if (data instanceof Map) {
    dataMap = (Map<?, ?>) data;
} else {
    // 使用 BeanUtils 或反射将对象转换为 Map
    dataMap = convertBeanToMap(data);
}

或者

data -> JSONObject.parseObject(JSON.toJSONString(data), Map.class))

你可以封装一个 convertBeanToMap(Object obj) 工具类,比如用 Apache Commons BeanUtils、Spring 的 BeanWrapperImpl 或自定义反射实现。

但是这种方法不推荐这样做,对调用方太不友好,而且写这样的代码很不好维护。这只是一个临时解决方案!

建议:为 RPC 接口统一泛型类型!!!

应该避免接口返回 WeaResult 没有明确泛型,否则不同的调用方(远程 vs 本地)会得到结构不一致的对象,严重时导致生产级兼容问题

建议的统一写法:

WeaResult<Map<String, Object>> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);

或者如果你能保证返回值是某个固定 VO 类:

WeaResult<RuleTypeSettingVo> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);

然后在调用方处理:

RuleTypeSettingVo vo = result.getData();
vo.getSceneType(); // 等价于 map.get("sceneType")
最后推荐大家:

RPC 接口的返回值类型一旦模糊(如未指定泛型),不管是微服务架构体系,还是合并单体公用同一个 JVM,使用时都可能导致结果不一致,最稳妥做法是*统一泛型类型(推荐)或封装类型转换逻辑(不推荐)。

推荐阅读文章


网站公告

今日签到

点亮在社区的每一天
去签到