目录
- 问题
- TypeToken是什么
- 其它使用场景
问题
在Java的json框架中,Gson是使用得比较广泛的一个,其Gson
类提供了toJson()
与fromJson()
方法,分别用来序列化与反序列化。
json序列化用得最多的场景是在调用外部服务接口时,大致如下:
public class Response<T>{ | |
int code; | |
String message; | |
T body; | |
} | |
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 { | |
Response<PersonInfo> getPersonById(; 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 { | |
PersonResponse getPersonById(Long id); | |
} |
然后你就会发现,gson可以正确识别到body属性的类型了。