2025年4月2日 星期三 乙巳(蛇)年 正月初三 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > Java

Gson的TypeToken与泛型擦除

时间:01-28来源:作者:点击数:33

问题#

在Java的json框架中,Gson是使用得比较广泛的一个,其Gson类提供了toJson()fromJson()方法,分别用来序列化与反序列化。

json序列化用得最多的场景是在调用外部服务接口时,大致如下:

  • @Data
  • @AllArgsConstructor
  • public class Response<T>{
  • int code;
  • String message;
  • T body;
  • }
  • @Data
  • @AllArgsConstructor
  • public class PersonInfo{
  • long id;
  • String name;
  • int age;
  • }
  • /**
  • * 服务端
  • */
  • 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);
  • }
  • }

由于大多数接口设计中,都会有统一的响应码结构,因此大多项目都会像上面一样,设计一个通用Response类来对应这种统一响应码结构,是很常见的情况。

但会发现,在反序列化过程中,传入目标类型时,使用了一段很奇怪的代码,即new TypeToken<Response<PersonInfo>>(){}.getType(),那它是什么?为啥要使用它?

TypeToken是什么#

为什么要使用TypeToken呢?我们直接使用Response<PersonInfo>.class行不行?如下:

image_2022-09-05_20220905181855

可以发现,java并不允许这么使用!

那传Response.class呢?如下:

image_2022-09-05_20220905182154

可以发现,代码能跑起来,但是Body变成了LinkedHashMap类型,这是因为传给gson的类型是Response.class,gson并不知道body属性是什么类型,那它只能使用LinkedHashMap这个默认的json对象类型了。

这就是TypeToken由来的原因,对于带泛型的类,使用TypeToken才能得到准确的类型信息,那TypeToken是怎么取到准确的类型的呢?

首先,new TypeToken<Response<PersonInfo>>(){}.getType()实际上是定义了一个匿名内部类的对象,然后调用了这个对象的getType()方法。

看看getType()的实现,如下:

image_2022-09-05_20220905182934

逻辑也比较简单,先通过getGenericSuperclass()获取了此对象的父类,即TypeToken<Response<PersonInfo>>,然后又通过getActualTypeArguments()[0]获取了实际类型参数,即Response<PersonInfo>

额,逻辑看起来说得通,但不是说Java泛型会擦除吗?这里不会擦除?

从所周知,java泛型擦除发生在编译期,ok,那我模拟上面的原理,写个空类继承TypeToken<Response<PersonInfo>>,然后编译这个类之后再反编译一下,看类型到底擦除没!

  • public class PersonResponseTypeToken extends TypeToken<Response<PersonInfo>> {
  • }

反编译结果如下:

image_2022-09-05_20220905184644

也就是说,被继承的父类上的泛型是不擦除的。

其它使用场景#

有时为了编程的方便,经常会有框架将远程调用接口化,类似下面这样:

  • 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();
  • //设置请求数据
  • 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());
  • }
  • //获取响应数据
  • if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
  • throw new RuntimeException("远程调用发生异常:url:" + conn.getURL() + ", requestBody:" + requestBody);
  • }
  • String responseStr = IOUtils.toString(conn.getInputStream(), StandardCharsets.UTF_8);
  • //响应结果反序列化为具体对象
  • 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());
  • }
  • }

这样做的好处是,开发人员不必再关心如何发远程请求了,只需要定义与调用接口即可。

但上面调用过程中会有一个问题,就是获取的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);
  • }

然后你就会发现,gson可以正确识别到body属性的类型了。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门