来自Retrofit 源码的一个疑问
Retrofit 是如何传递泛型信息的?
上一段常见的网络接口请求代码:
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
使用jad 查看反编译 后的class 文件:
import retrofit2.Call;
public interface GitHubService
{
public abstract Call listRepos(String s);
}
可以看到 class文件中已经将泛型信息给擦除了,那么Retrofit 是如何拿到 Call<List> 的类型信息,并通过 Gson 进行反序列化 输出List 对象的呢?
实际上可以通过反射,获取方法的带泛型参数类型:
public static void main(String args[]) {
try {
Class clz = Class.forName("com.example.diva.leet.GitHubService");
Method method = clz.getMethod("listRepos", String.class);
System.out.println("getGenericReturnType " + method.getGenericReturnType());
System.out.println("getReturnType " + method.getReturnType());
} catch (Exception e) {
e.printStackTrace();
}
}
输出:
getGenericReturnType retrofit2.Call<java.util.List<com.example.diva.leet.Repo>>
getReturnType interface retrofit2.Call
关于反射获取的Type 类
java的泛型擦除与泛型信息保留
jdk 的 Class 、Method 、Field 类提供了一系列获取 泛型类型的相关方法。以Method 为例
getGenericReturnType 获取带泛型信息的返回类型 、 getGenericParameterTypes 获取带泛型信息的参数类型。
问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以 Signature 的形式 保留在Class文件的Constant pool中。
public Type getGenericReturnType() {
// 根据 Signature 信息 获取 泛型类型
if (getGenericSignature() != null) {
return getGenericInfo().getReturnType();
} else { return getReturnType();}
}
通过 javap 命令 可以看到 在Constant pool 中 #5 Signature 记录 了泛型的类型。
Constant pool:
#1 = Class #16 // com/example/diva/leet/GitHubService
#2 = Class #17 // java/lang/Object
#3 = Utf8 listRepos
#4 = Utf8 (Ljava/lang/String;)Lretrofit2/Call;
#5 = Utf8 Signature
#6 = Utf8 (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
#7 = Utf8 RuntimeVisibleAnnotations
#8 = Utf8 Lretrofit2/http/GET;
#9 = Utf8 value
#10 = Utf8 users/{user}/repos
#11 = Utf8 RuntimeVisibleParameterAnnotations
#12 = Utf8 Lretrofit2/http/Path;
#13 = Utf8 user
#14 = Utf8 SourceFile
#15 = Utf8 GitHubService.java
#16 = Utf8 com/example/diva/leet/GitHubService
#17 = Utf8 java/lang/Object
{
public abstract retrofit2.Call<java.util.List<com.example.diva.leet.Repo>> listRepos(java.lang.String);
flags: ACC_PUBLIC, ACC_ABSTRACT
Signature: #6 // (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
RuntimeVisibleAnnotations:
0: #8(#9=s#10)
RuntimeVisibleParameterAnnotations:
parameter 0:
0: #12(#9=s#13)
}
如果通过 Bytecode Viewer 工具查看纯正的 class 的 raw 文件,可以发现是泛型信息是记录在class文件中的。
那些泛型会被保留,哪些是真正的擦除了?
声明侧泛型会被记录在 Class 文件的 Constant pool 中 :
- 泛型类,或泛型接口的声明
- 带有泛型参数的方法
- 带有泛型参数的成员变量
使用侧泛型 :也就是方法的局部变量, 方法调用时传入的变量。
Gson 的TypeToken 原理
// Gson 常用的情况
public List<String> parse(String jsonStr){
List<String> topNews = new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
return topNews;
}
根据以上的总结,方法的局部变量的泛型是不会被保存的,Gson 是如何获取到
List< String > 的泛型信息 String 的呢?
由于通过Class.forName( ) 方法、ClassName.class 这种方式获取到 的是 Raw 类型,是不带泛型类型的。
但是,转折点来了:Class 类提供了一个方法 public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。 也就是说 java 的class 文件会保存继承的父类或者接口的泛型信息。
所以 Gson 使用了一个巧妙的方法:
- 创建一个泛型抽象类 TypeToken < T > ,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类。
- 创建一个 继承 自 TypeToken 的 匿名内部类, 并实例化 泛型参数 TypeToken < String >
- 通过class 类的 public Type getGenericSuperclass() 方法,获取带泛型信息的父类Type,也就是TypeToken < String >
一个最简单的TypeToken 代码 如下:
public abstract class TypeToken<T> {
private final Type type;
public TypeToken() {
Type genericSuperclass = getClass().getGenericSuperclass();
if(genericSuperclass instanceof Class){
throw new RuntimeException("Missing type parameter.");
}
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
Type[] typeArguments = parameterizedType.getActualTypeArguments();
type = typeArguments[0];
}
public Type getType() {
return type;
}
public static void main(String args[]){
TypeToken<List<String>> sToken = new TypeToken<List<String>>(){};
System.out.println(sToken.getType());
// 匿名内部类,也可以使用非匿名的方式。
class TypeToken$1 extends TypeToken<List<String>>{};
TypeToken$1 typeToken$1 = new TypeToken$1();
System.out.println(typeToken$1.getType());
}
}
输出结果:
java.util.List<java.lang.String>
java.util.List<java.lang.String>
总结:Gson 利用子类 会 保存父类 class 的 泛型参数信息的特点。 通过匿名内部类和反射 实现了泛型参数的传递。