Gson中的TypeToken與泛型擦除詳情
問題
在Java的json框架中,Gson是使用得比較廣泛的一個,其Gson類提供了toJson()與fromJson()方法,分別用來序列化與反序列化。
json序列化用得最多的場景是在調(diào)用外部服務(wù)接口時,大致如下:
@Data
@AllArgsConstructor
public class Response<T>{
int code;
String message;
T body;
}
@Data
@AllArgsConstructor
public class PersonInfo{
long id;
String name;
int age;
}
/**
* 服務(wù)端
*/
public class Server {
public static String getPersonById(Long id){
PersonInfo personInfo = new PersonInfo(1234L, "zhangesan", 18);
Response<PersonInfo> response = new Response<>(200, "success", personInfo);
//序列化
return new Gson().toJson(response);
}
}
/**
* 客戶端
*/
public class Client {
public static void getPerson(){
String responseStr = Server.getPersonById(1234L);
//反序列化
Response<PersonInfo> response = new Gson().fromJson(responseStr, new TypeToken<Response<PersonInfo>>(){}.getType());
System.out.println(response);
}
}由于大多數(shù)接口設(shè)計中,都會有統(tǒng)一的響應(yīng)碼結(jié)構(gòu),因此大多項目都會像上面一樣,設(shè)計一個通用Response類來對應(yīng)這種統(tǒng)一響應(yīng)碼結(jié)構(gòu),是很常見的情況。
但會發(fā)現(xiàn),在反序列化過程中,傳入目標(biāo)類型時,使用了一段很奇怪的代碼,即new TypeToken<Response<PersonInfo>>(){}.getType(),那它是什么?為啥要使用它?
TypeToken是什么
為什么要使用TypeToken呢?我們直接使用Response<PersonInfo>.class行不行?如下:

可以發(fā)現(xiàn),java并不允許這么使用!
那傳Response.class呢?如下:

可以發(fā)現(xiàn),代碼能跑起來,但是Body變成了LinkedHashMap類型,這是因為傳給gson的類型是Response.class,gson并不知道body屬性是什么類型,那它只能使用LinkedHashMap這個默認(rèn)的json對象類型了。
這就是TypeToken由來的原因,對于帶泛型的類,使用TypeToken才能得到準(zhǔn)確的類型信息,那TypeToken是怎么取到準(zhǔn)確的類型的呢?
首先,new TypeToken<Response<PersonInfo>>(){}.getType()實際上是定義了一個匿名內(nèi)部類的對象,然后調(diào)用了這個對象的getType()方法。
看看getType()的實現(xiàn),如下:

邏輯也比較簡單,先通過getGenericSuperclass()獲取了此對象的父類,即TypeToken<Response<PersonInfo>>,然后又通過getActualTypeArguments()[0]獲取了實際類型參數(shù),即Response<PersonInfo>。
額,邏輯看起來說得通,但不是說Java泛型會擦除嗎?這里不會擦除?
從所周知,java泛型擦除發(fā)生在編譯期,ok,那我模擬上面的原理,寫個空類繼承TypeToken<Response<PersonInfo>>,然后編譯這個類之后再反編譯一下,看類型到底擦除沒!
public class PersonResponseTypeToken extends TypeToken<Response<PersonInfo>> {
}反編譯結(jié)果如下:

也就是說,被繼承的父類上的泛型是不擦除的。
其它使用場景
有時為了編程的方便,經(jīng)常會有框架將遠(yuǎn)程調(diào)用接口化,類似下面這樣:
public class RemoteUtil {
private static final ConcurrentMap<Class, Object> REMOTE_CACHE = new ConcurrentHashMap<>();
public static <T> T get(Class<T> clazz) {
return clazz.cast(REMOTE_CACHE.computeIfAbsent(clazz, RemoteUtil::getProxyInstance));
}
private static Object getProxyInstance(Class clazz) {
return Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{clazz}, (proxy, method, args) -> {
Gson gson = new Gson();
String path = method.getAnnotation(RequestMapping.class).path()[0];
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("http://localhost:8080/" + path).openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.connect();
//設(shè)置請求數(shù)據(jù)
JsonObject requestBody = new JsonObject();
try (Writer out = new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8)) {
int i = 0;
for (Parameter parameter : method.getParameters()) {
String name = parameter.getAnnotation(RequestParam.class).name();
requestBody.add(name, gson.toJsonTree(args[i]));
i++;
}
out.write(requestBody.toString());
}
//獲取響應(yīng)數(shù)據(jù)
if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new RuntimeException("遠(yuǎn)程調(diào)用發(fā)生異常:url:" + conn.getURL() + ", requestBody:" + requestBody);
}
String responseStr = IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8);
//響應(yīng)結(jié)果反序列化為具體對象
return gson.fromJson(responseStr, method.getReturnType());
} finally {
if (conn != null) {
conn.disconnect();
}
}
});
}
}
public interface PersonApi {
@RequestMapping(path = "/person")
Response<PersonInfo> getPersonById(@RequestParam(name = "id") Long id);
}
public class Client {
public static void getPerson() {
Response<PersonInfo> response = RemoteUtil.get(PersonApi.class).getPersonById(1234L);
System.out.println(response.getBody());
}
}這樣做的好處是,開發(fā)人員不必再關(guān)心如何發(fā)遠(yuǎn)程請求了,只需要定義與調(diào)用接口即可。
但上面調(diào)用過程中會有一個問題,就是獲取的response對象中body屬性是LinkedHashMap,原因是gson反序列化時是通過method.getReturnType()來獲取返回類型的,而返回類型中的泛型會被擦除掉。
要解決這個問題也很簡單,和上面TypeToken一樣的道理,定義一個空類PersonResponse來繼承Response<PersonInfo>,然后將返回類型定義為PersonResponse
如下:
public class PersonResponse extends Response<PersonInfo> {
}
public interface PersonApi {
@RequestMapping(path = "/person")
PersonResponse getPersonById(@RequestParam(name = "id") Long id);
}然后你就會發(fā)現(xiàn),gson可以正確識別到body屬性的類型了。
到此這篇關(guān)于Gson中的TypeToken與泛型擦除詳情的文章就介紹到這了,更多相關(guān)Gson TypeToken內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實現(xiàn)訂單超時未支付自動取消的8種方法總結(jié)
這篇文章主要為大家介紹了Java實現(xiàn)訂單超時未支付自動取消功能的8種不同方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-08-08
Spring中的@PostConstruct注解使用方法解析
這篇文章主要介紹了Spring中的@PostConstruct注解使用方法解析,@PostConstruct注解是用來處理在@Autowired注入屬性后init()方法之前,對一些零散的屬性進(jìn)行賦值的注解,需要的朋友可以參考下2023-11-11
SpringBoot v2.2以上重復(fù)讀取Request Body內(nèi)容的解決方案
這篇文章主要介紹了SpringBoot v2.2以上重復(fù)讀取Request Body內(nèi)容的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10

