今天遇到一个问题:一个典型的 Java 泛型在反序列化场景下“类型擦除 + 无法推断具体类型”导致的隐性 Bug,尤其是在 RPC
(如 Dubbo
、Feign
等)和 本地 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 规范时,一定要明确所有出入参,以及涉及到的泛型。
另外,定义了这种 WeaResult
有 code
+ 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 的“暗坑”与解决方案(二)