当前位置: 首页 > news >正文

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

今天遇到一个问题:一个典型的 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 或反射将对象转换为 MapdataMap = 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,使用时都可能导致结果不一致,最稳妥做法是*统一泛型类型(推荐)或封装类型转换逻辑(不推荐)。

推荐阅读文章

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 什么是 Cookie?简单介绍与使用方法

  • 什么是 Session?如何应用?

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • 如何理解应用 Java 多线程与并发编程?

  • 把握Java泛型的艺术:协变、逆变与不可变性一网打尽

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 如何理解线程安全这个概念?

  • 理解 Java 桥接方法

  • Spring 整合嵌入式 Tomcat 容器

  • Tomcat 如何加载 SpringMVC 组件

  • “在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”

  • “避免序列化灾难:掌握实现 Serializable 的真相!(二)”

  • 如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)

  • 解密 Redis:如何通过 IO 多路复用征服高并发挑战!

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • “打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”

  • Java 中消除 If-else 技巧总结

  • 线程池的核心参数配置(仅供参考)

  • 【人工智能】聊聊Transformer,深度学习的一股清流(13)

  • Java 枚举的几个常用技巧,你可以试着用用

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • 探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)

  • 为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)

相关文章:

  • 解决 html2canvas 把svg转成jpg,无法把svg里的image图片正常显示的情况
  • uni-app项目实战笔记5--使用grid进行定位布局
  • Spring boot 的 maven 打包过程
  • 人工智能 倒底是 智能 还是 智障?
  • FastAPI如何用角色权限让Web应用安全又灵活?
  • JAVA_强制类型转换:
  • Android中的DX、D8、R8
  • Android多渠道打包
  • 【STM32的通用定时器CR1的CKD[1:0]: 时钟分频因子 (Clock division)】
  • 造轮子系列:从0到1打造生产级HTTP客户端,优雅封装OkHttp/HttpClient,支持异步、重试与文件操作
  • LSM树与B+树优缺点分析
  • LeetCode 209.长度最小的子数组
  • 多线程中SimpleDateFormat为何不安全?如何解决?
  • 基于大模型预测过敏性紫癜的技术方案大纲
  • window 显示驱动开发-DirectX VA 2.0 的扩展支持
  • Python 爬虫入门 Day 2 - HTML解析入门(使用 BeautifulSoup)
  • 【工具教程】批量PDF识别提取区域的内容重命名,将PDF指定区域位置的内容提取出来改名的具体操作步骤
  • Logback-spring.xml 配置屏蔽特定路径的日志
  • 美化显示MSVC调试的数据结构
  • centos 8.3(阿里云服务器)mariadb由系统自带版本(10.3)升级到10.6
  • 律师网站建设建议/郑州seo培训
  • wordpress调用指定文章id/长沙优化网站推广
  • 网络品牌前十大排名/长春网站优化方案
  • 域名网站备案/郑州seo公司
  • 温州网站设计公司哪家好/排名函数
  • 网站官网建设企业/百家联盟推广部电话多少