原创:按钮日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。问题在Java的json框架中,Gson被广泛使用。它的Gson类提供了toJson()和fromJson()方法,分别用于序列化和反序列化。json序列化最常用的场景是调用外部服务接口时,大致如下:@Data@AllArgsConstructorpublicclassResponse{intcode;字符串消息;Tbody;}@Data@AllArgsConstructorpublicclassPersonInfo{longid;字符串名称;intage;}/***server*/publicclassServer{publicstaticStringgetPersonById(Longid){PersonInfopersonInfo=newPersonInfo(1234L,"zhangesan",18);Responseresponse=newResponse<>(200,"success",personInfo);//序列化returnnewGson().toJson(response);}}/***Client*/publicclassClient{publicstaticvoidgetPerson(){StringresponseStr=Server.getPersonById(1234L);//反序列化Responseresponse=newGson().fromJson(responseStr,newTypeToken>(){}.getType());系统输出。打印(响应);}}由于大部分的界面设计都会有一个统一的响应码结构,所以像上面这样设计一个通用的Response类来对应这个统一的响应码结构是很常见的。但是你会发现在反序列化的过程中,在传入目标类型的时候,使用了一段很奇怪的代码,即newTypeToken>(){}.getType(),那到底是什么呢?为什么要使用它?什么是TypeToken?为什么要使用TypeToken?我们可以直接使用Response.class吗?如下:可以发现java是不允许这样使用的!传递Response.class怎么样?如下:可以发现代码可以运行,但是Body变成了LinkedHashMap类型。这是因为传递给gson的类型是Response.class,而gson不知道body属性是什么类型,所以只能使用默认的LinkedHashMapjson对象类型。这就是TypeToken的由来。对于具有泛型类型的类,可以使用TypeToken来获取准确的类型信息。TypeToken如何获取准确的类型?首先,newTypeToken>(){}.getType()实际上定义了一个匿名内部类的对象,然后调用这个对象的getType()方法。再看getType()的实现,如下:逻辑比较简单。首先通过getGenericSuperclass()得到对象的父类,即TypeToken>,再通过getActualTypeArguments()[0]得到实际类型参数,即Response。嗯,这个逻辑好像说得通,但这不就意味着Java泛型要被抹杀了吗?不会在这里擦除?众所周知,Java泛型擦除是在编译的时候发生的,好吧,那我模拟上面的原理,写一个空类继承TypeToken>,然后编译这个类,然后反编译看看类型。除了不!publicclassPersonResponseTypeTokenextendsTypeToken>{}反编译结果如下:也就是说,继承父类上的泛型没有被擦除。其他使用场景有时为了编程的方便,往往会有接口远程调用的框架,像下面这样:publicstaticTget(Classclazz){returnclazz.cast(REMOTE_CACHE.computeIfAbsent(clazz,RemoteUtil::getProxyInstance));}privatestaticObjectgetProxyInstance(Classclazz){returnProxy.newProxyInstance(ClassLoader.getSystemClassLoader(),newClass[]{clazz},(proxy,method,args)->{Gsongson=newGson();Stringpath=method.getAnnotation(RequestMapping.class).path()[0];HttpURLConnectionconn=null;try{conn=(HttpURLConnection)newURL("http://localhost:8080/"+path).openConnection();conn.setRequestMethod("POST");conn.setDoOutput(true);conn.setDoInput(true);conn.connect();//设置请求数据JsonObjectrequestBody=newJsonObject();try(Writerout=newOutputStreamWriter(conn.getOutputStream(),StandardCharsets.UTF_8)){inti=0;for(Parameterparameter:method.getParameters()){Stringname=parameter.getAnnotation(RequestParam.class).name();requestBody.add(name,gson.toJsonTree(args[i]));我++;}out.write(requestBody.toString());}//获取响应数据if(conn.getResponseCode()!=HttpURLConnection.HTTP_OK){thrownewRuntimeException("远程调用发生异常:url:"+conn.getURL()+",requestBody:"+requestBody);}StringresponseStr=IOUtils.toString(conn.getInputStream(),StandardCharsets.UTF_8);//响应结果反序列化为具体的对象返回gson.fromJson(responseStr,method.getReturnType());}finally{if(conn!=null){conn.disconnect();}}});}}publicinterfacePersonApi{@RequestMapping(path="/person")ResponsegetPersonById(@RequestParam(name="id")Longid);}publicclassClient{publicstaticvoidgetPerson(){Responseresponse=RemoteUtil.get(PersonApi.class).getPersonById(1234L);System.out.println(response.getBody());}}这样做的好处是,开发者不再需要关心如何发送远程请求,只需要定义和调用接口即可,但是会有一个问题,就是获取到的响应对象中的body属性是LinkedHashMap。原因是gson反序列化时,通过method.getReturnType()获取返回类型,返回类型中的泛型会被抹掉。解决这个问题也很简单,和上面的TypeToken一样,定义一个空类PersonResponse继承Response,然后定义返回类型为PersonResponse,如下:publicclassPersonResponseextendsResponse{}publicinterfacePersonApi{@RequestMapping(path="/person")PersonResponsegetPersonById(@RequestParam(name="id")Longid);}然后你会发现gson可以正确识别body属性的类型。上一篇密码学入门接口偶尔超时,其实是JVM卡顿的原因!花了几个月的时间,终于找到了JVM停顿十几秒的原因。mysqltimestamp会不会有时区问题?真正理解可重复读事务隔离级别字符编码解决混淆