今天遇到一個問題:一個典型的 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 的“暗坑”與解決方案(二)