網(wǎng)站后端都需要什么意思網(wǎng)站交換鏈接的常見(jiàn)形式
今天遇到一個(gè)問(wèn)題:一個(gè)典型的 Java 泛型在反序列化場(chǎng)景下“類(lèi)型擦除 + 無(wú)法推斷具體類(lèi)型”導(dǎo)致的隱性 Bug,尤其是在 RPC
(如 Dubbo
、Feign
等)和 本地 JVM 內(nèi)直連調(diào)用共存時(shí),這種問(wèn)題會(huì)顯現(xiàn)得非常明顯。
A 服務(wù)暴露了一個(gè) RPC
接口規(guī)范,如下:
public class WeaResult<T> implements Serializable {private static final long serialVersionUID = 15869325700230991L;@ApiModelProperty("狀態(tài)碼")private int code;@ApiModelProperty("提示信息")private String msg;@ApiModelProperty("狀態(tài)")private boolean status;@ApiModelProperty("數(shù)據(jù)")private T data;
}
定義的 RPC 接口如下:
WeaResult selectDetail(RuleTypeSettingDto ruleTypeSettingDto);
API 中的返回值沒(méi)有聲明泛型 <T>
的具體類(lèi)型。然后被 B 服務(wù)調(diào)用了,遠(yuǎn)程調(diào)用代碼:
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);}
接受到結(jié)果,只能硬著頭皮強(qiáng)轉(zhuǎn),獲取對(duì)應(yīng)值。
這里解釋下,為什么要強(qiáng)轉(zhuǎn)?
當(dāng)是 RPC 場(chǎng)景(如 JSON 序列化傳輸)時(shí),框架通常會(huì)把 data
轉(zhuǎn)換為 Map<String, Object>
(比如 JSON 默認(rèn)映射到 HashMap
),所以我這里直接強(qiáng)轉(zhuǎn)成 Map 類(lèi)型:
map(data -> (Map<?,?>) data)
這樣是能夠能運(yùn)行的,沒(méi)啥問(wèn)題。
但是,重點(diǎn)來(lái)了,當(dāng)是A 和 B 服務(wù)合并單體時(shí)部署時(shí)(在同一個(gè) JVM 中,或者說(shuō)是本地部署),就會(huì)直接返回原始的具體類(lèi)型對(duì)象(比如是 RuleTypeSettingVo
),此時(shí) (Map<?, ?>) data
就會(huì)拋 ClassCastException
—— 因?yàn)楦静皇?Map
!所以這個(gè)就是一個(gè)巨坑!這就是沒(méi)有合理定義 API 接口導(dǎo)致的,并且泛型一定一定要注明清楚。否則調(diào)用方永遠(yuǎn)只是一個(gè)盲區(qū)。
提示:這里的合并指的是將服務(wù)提供者和消費(fèi)者都合并成一個(gè)單體服務(wù)部署??赡苁枪?jié)省客戶(hù)資源。
那么怎么去正確改進(jìn)呢?
方法一:指定泛型類(lèi)型,讓接口明確返回結(jié)構(gòu)
WeaResult<RuleTypeSettingVo> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);
這樣無(wú)論是遠(yuǎn)程調(diào)用還是本地調(diào)用,返回值類(lèi)型一致,調(diào)用方可以安全地 (Map)
,但是不推薦用 RuleTypeSettingVo
還是,大部分都是按照實(shí)體返回。所以,定義 API 規(guī)范時(shí),一定要明確所有出入?yún)?#xff0c;以及涉及到的泛型。
另外,定義了這種 WeaResult
有 code
+ status
返回的,一定要優(yōu)先判斷 code
+ status
。否則,你一定會(huì)吃大虧,code
+ status
可以讓我們?cè)谡{(diào)用遠(yuǎn)程接口時(shí)減少很多不必要的麻煩
方法二:在調(diào)用方顯式判斷類(lèi)型(不推薦)
如果你不能修改接口,但調(diào)用方需要容錯(cuò)處理,可以使用:
Object data = ruleTypeSettingService.selectDetail(...).getData();
Map<?, ?> dataMap;
if (data instanceof Map) {dataMap = (Map<?, ?>) data;
} else {// 使用 BeanUtils 或反射將對(duì)象轉(zhuǎn)換為 MapdataMap = convertBeanToMap(data);
}
或者
data -> JSONObject.parseObject(JSON.toJSONString(data), Map.class))
你可以封裝一個(gè) convertBeanToMap(Object obj)
工具類(lèi),比如用 Apache Commons BeanUtils、Spring 的 BeanWrapperImpl
或自定義反射實(shí)現(xiàn)。
但是這種方法不推薦這樣做,對(duì)調(diào)用方太不友好,而且寫(xiě)這樣的代碼很不好維護(hù)。這只是一個(gè)臨時(shí)解決方案!
建議:為 RPC 接口統(tǒng)一泛型類(lèi)型!!!
應(yīng)該避免接口返回 WeaResult
沒(méi)有明確泛型,否則不同的調(diào)用方(遠(yuǎn)程 vs 本地)會(huì)得到結(jié)構(gòu)不一致的對(duì)象,嚴(yán)重時(shí)導(dǎo)致生產(chǎn)級(jí)兼容問(wèn)題。
建議的統(tǒng)一寫(xiě)法:
WeaResult<Map<String, Object>> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);
或者如果你能保證返回值是某個(gè)固定 VO 類(lèi):
WeaResult<RuleTypeSettingVo> selectDetail(RuleTypeSettingDto ruleTypeSettingDto);
然后在調(diào)用方處理:
RuleTypeSettingVo vo = result.getData();
vo.getSceneType(); // 等價(jià)于 map.get("sceneType")
最后推薦大家:
RPC 接口的返回值類(lèi)型一旦模糊(如未指定泛型),不管是微服務(wù)架構(gòu)體系,還是合并單體公用同一個(gè) JVM,使用時(shí)都可能導(dǎo)致結(jié)果不一致,最穩(wěn)妥做法是*統(tǒng)一泛型類(lèi)型(推薦)或封裝類(lèi)型轉(zhuǎn)換邏輯(不推薦)。
推薦閱讀文章
-
由 Spring 靜態(tài)注入引發(fā)的一個(gè)線(xiàn)上T0級(jí)別事故(真的以后得避坑)
-
如何理解 HTTP 是無(wú)狀態(tài)的,以及它與 Cookie 和 Session 之間的聯(lián)系
-
HTTP、HTTPS、Cookie 和 Session 之間的關(guān)系
-
什么是 Cookie?簡(jiǎn)單介紹與使用方法
-
什么是 Session?如何應(yīng)用?
-
使用 Spring 框架構(gòu)建 MVC 應(yīng)用程序:初學(xué)者教程
-
有缺陷的 Java 代碼:Java 開(kāi)發(fā)人員最常犯的 10 大錯(cuò)誤
-
如何理解應(yīng)用 Java 多線(xiàn)程與并發(fā)編程?
-
把握J(rèn)ava泛型的藝術(shù):協(xié)變、逆變與不可變性一網(wǎng)打盡
-
Java Spring 中常用的 @PostConstruct 注解使用總結(jié)
-
如何理解線(xiàn)程安全這個(gè)概念?
-
理解 Java 橋接方法
-
Spring 整合嵌入式 Tomcat 容器
-
Tomcat 如何加載 SpringMVC 組件
-
“在什么情況下類(lèi)需要實(shí)現(xiàn) Serializable,什么情況下又不需要(一)?”
-
“避免序列化災(zāi)難:掌握實(shí)現(xiàn) Serializable 的真相!(二)”
-
如何自定義一個(gè)自己的 Spring Boot Starter 組件(從入門(mén)到實(shí)踐)
-
解密 Redis:如何通過(guò) IO 多路復(fù)用征服高并發(fā)挑戰(zhàn)!
-
線(xiàn)程 vs 虛擬線(xiàn)程:深入理解及區(qū)別
-
深度解讀 JDK 8、JDK 11、JDK 17 和 JDK 21 的區(qū)別
-
10大程序員提升代碼優(yōu)雅度的必殺技,瞬間讓你成為團(tuán)隊(duì)寵兒!
-
“打破重復(fù)代碼的魔咒:使用 Function 接口在 Java 8 中實(shí)現(xiàn)優(yōu)雅重構(gòu)!”
-
Java 中消除 If-else 技巧總結(jié)
-
線(xiàn)程池的核心參數(shù)配置(僅供參考)
-
【人工智能】聊聊Transformer,深度學(xué)習(xí)的一股清流(13)
-
Java 枚舉的幾個(gè)常用技巧,你可以試著用用
-
由 Spring 靜態(tài)注入引發(fā)的一個(gè)線(xiàn)上T0級(jí)別事故(真的以后得避坑)
-
如何理解 HTTP 是無(wú)狀態(tài)的,以及它與 Cookie 和 Session 之間的聯(lián)系
-
HTTP、HTTPS、Cookie 和 Session 之間的關(guān)系
-
使用 Spring 框架構(gòu)建 MVC 應(yīng)用程序:初學(xué)者教程
-
有缺陷的 Java 代碼:Java 開(kāi)發(fā)人員最常犯的 10 大錯(cuò)誤
-
Java Spring 中常用的 @PostConstruct 注解使用總結(jié)
-
線(xiàn)程 vs 虛擬線(xiàn)程:深入理解及區(qū)別
-
深度解讀 JDK 8、JDK 11、JDK 17 和 JDK 21 的區(qū)別
-
10大程序員提升代碼優(yōu)雅度的必殺技,瞬間讓你成為團(tuán)隊(duì)寵兒!
-
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
-
為什么用了 @Builder 反而報(bào)錯(cuò)?深入理解 Lombok 的“暗坑”與解決方案(二)