原创:扣钉日记(微信公众号 ID:codelogs),欢送分享,转载请保留出处。
问题
在 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
行不行?如下:
能够发现,java 并不容许这么应用!
那传 Response.class
呢?如下:
能够发现,代码能跑起来,然而 Body 变成了 LinkedHashMap
类型,这是因为传给 gson 的类型是 Response.class
,gson 并不知道 body 属性是什么类型,那它只能应用LinkedHashMap
这个默认的 json 对象类型了。
这就是 TypeToken 由来的起因,对于带泛型的类,应用 TypeToken 能力失去精确的类型信息,那 TypeToken 是怎么取到精确的类型的呢?
首先,new TypeToken<Response<PersonInfo>>(){}.getType()
实际上是定义了一个匿名外部类的对象,而后调用了这个对象的 getType()
办法。
看看 getType()
的实现,如下:
逻辑也比较简单,先通过 getGenericSuperclass()
获取了此对象的父类,即 TypeToken<Response<PersonInfo>>
,而后又通过getActualTypeArguments()[0]
获取了理论类型参数,即Response<PersonInfo>
。
额,逻辑看起来说得通,但不是说 Java 泛型会擦除吗?这里不会擦除?
从所周知,java 泛型擦除产生在编译期,ok,那我模仿下面的原理,写个空类继承TypeToken<Response<PersonInfo>>
,而后编译这个类之后再反编译一下,看类型到底擦除没!
public class PersonResponseTypeToken extends TypeToken<Response<PersonInfo>> {}
反编译后果如下:
也就是说,被继承的父类上的泛型是不擦除的。
其它应用场景
有时为了编程的不便,常常会有框架将近程调用接口化,相似上面这样:
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 属性的类型了。
往期内容
密码学入门
接口偶然超时,竟又是 JVM 进展的锅!
耗时几个月,终于找到了 JVM 进展十几秒的起因
mysql 的 timestamp 会存在时区问题?
真正了解可反复读事务隔离级别
字符编码解惑