The background,
The Android project wanted to encapsulate retrofit to provide public methods like GET and POST, for example:
Uniform data format returned:
{
"errCode":0."errMsg":"ok"."data":...
}
Copy the code
The following may be the case in data:
- Basic data type data, such as:
true
,1
,string
Etc. - Json object, such as
{"key1":"value"}
- Json array, as in
[{"key1":"value1"},{"key2":"value2"}]
In this case, the type of data is difficult to determine, and in traditional Retrofit, it can be used in the following ways:
interface ApiService {
@GET
Observable<ResponseEntity<String>> getxxx1(...)
@GET
Observable<ResponseEntity<JsonClass>> getxxx2(...)
@GET
Observable<ResponseEntity<List<JsonClass>> getxxx3(...)
}
class JsonClass {}
class ResponseEntity<T>{
public int errCode;
public String errMsg;
public T data;
}
Copy the code
By passing in a generic class in a concrete ResponseEntity, Retrofit can do type conversions via GsonConverterFactory, but this has the following problems: ⚠️
-
Cannot uniformly handle error codes for returned ResponseEntity
For example, some fixed error codes are defined in our service. ErrCode =0 indicates that data is returned normally, errCode=1001 indicates that the token has expired and you need to jump to the login page for re-login. If errCode> XXX, you need to pop up a toast prompt. These are not handled uniformly and need to be evaluated in the onSuccess method of each calling interface.
-
The entire returned data is exposed to the business, but the business only cares about the data represented by T
For the framework, it should be OK to provide the business with the data it needs, and some low-level judgments should not be exposed to the business
-
There are multiple ApiService for componentized projects
In the interview process of these years, I found that some interviewees had an ApiService in each component of the componentized project, and each ApiService assumed the business interface data request of different business modules. Since Java does not support category classification (that is, the same class can be scattered into different files, IOS is a Category, similar to the extension function in Kotlin), which results in multiple Retrofit instances in your project
For these reasons, I wanted to further encapsulate Retrofit in our project to avoid these problems
First I’ll try this:
interface ApiService {
@GET
<T> Observable<ResponseEntity<T> get(Map<String,String> params)
@POST
<T> Observable<ResponseEntity<T>> post(Map<String,String> params)
}
Copy the code
Complains ❌ com. Google. Gson. Internal. LinkedTreeMap always be cast to XXX, investigate its main reason is that exist in the Java generics erase, This results in a GsonConverterFactory conversion where the true type of T is not found, and the ResponseEntity
can be thought of as ResponseEntity
Gson’s method of deserializing generic data
How do I convert A JSON string into a generic data structure
class ResponseData<T>{
private int errCode;
private String errMsg;
private T data;
/ /... Omit the get the set
}
class Result{
private String name;
/ /... Omit the get the set
}
public static void main(String[] args) {
Gson gson = new Gson();
String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";
ResponseData<Result> responseData = gson.fromJson(r1Str, ResponseData.class);
System.out.println(responseData.getData().getName());
}
Copy the code
An error ❌ : Com. Google. Gson. Internal. LinkedTreeMap always be cast to the Result, it is because there are generic erasure problem, can’t write in the code ResponseData < Result >. The class, So we don’t know what the real type of T is during the conversion. Another way:
public static void main(String[] args) {
Gson gson = new Gson();
String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";
ResponseData<Result> responseData = gson.fromJson(r1Str,new TypeToken<ResponseData<Result>>(){}.getType());
System.out.println(responseData.getData().getName());
}
Copy the code
Use TypeToken to obtain the true type of T, so how to implement TypeToken, let’s look at the source code
protected TypeToken(a) {
this.type = getSuperclassTypeParameter(this.getClass());
this.rawType = Types.getRawType(this.type);
this.hashCode = this.type.hashCode();
}
static Type getSuperclassTypeParameter(Class
subclass) {
// getGenericSuperclass returns the Type of the immediate superclass of the entity (Class, interface, primitive Type, or void) represented by this Class.
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
} else {
// ParameterizedType is used to get generic signature types in metadata.
ParameterizedType parameterized = (ParameterizedType)superclass;
// getActualTypeArguments gets the true type arguments
return Types.canonicalize(parameterized.getActualTypeArguments()[0]); }}public final Type getType(a) {
return this.type;
}
Copy the code
The core uses getGenericSuperclass and ParameterizedType. In the next section, we’ll take a closer look at these API classes in reflection technology and see how they can be used to retrieve the true type of a generic.
There is one more detail to note:
🏁 new TypeToken
(){}
The TypeToken constructor is protected, indicating that the package and subclasses are available. Adding a {} extends the class, which is equivalent to:
class TypeToken$0 extends TypeToken<ResponseData<Result>>{}
TypeToken<ResponseData<Result>> sToken = new TypeToken$0(a);Copy the code
How do I get the real type of a generic by reflection?
When we reflect a generic class, we need the real data types in the generic to perform operations such as JSON deserialization. This needs to be done through the Type system. The Type interface contains an implementation Class and four implementation interfaces:
-
TypeVariable
Generic type variables. The upper and lower limits of generics can be obtained.
-
ParameterizedType
Generic signature type (generic real type) in metadata
-
GenericArrayType
When you need to describe an array of generic classes, such as List[],Map[], this interface is implemented as Type.
-
WildcardType
Wildcard generics to obtain upper and lower limits of information;
3.1, TypeVariable
public class TestType <K extends Comparable & Serializable.V> {
K key;
V value;
public static void main(String[] args) throws Exception {
// Get the field type
Field fk = TestType.class.getDeclaredField("key");
Field fv = TestType.class.getDeclaredField("value");
// getGenericType
TypeVariable keyType = (TypeVariable)fk.getGenericType();
TypeVariable valueType = (TypeVariable)fv.getGenericType();
/ / getName method
System.out.println(keyType.getName());
System.out.println(valueType.getName());
/ / getGenericDeclaration method
System.out.println(keyType.getGenericDeclaration());
System.out.println(valueType.getGenericDeclaration());
/ / getBounds method
System.out.println("Upper bound on K :");
for (Type type : keyType.getBounds()) {
System.out.println(type);
}
System.out.println("Upper bound of V :");
for(Type type : valueType.getBounds()) { System.out.println(type); }}}Copy the code
3.2, ParameterizedType
public class TestType {
Map<String, String> map;
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("map");
System.out.println(f.getGenericType()); // java.util.Map<java.lang.String,java.lang.String>
ParameterizedType pType = (ParameterizedType) f.getGenericType();
System.out.println(pType.getRawType()); // interface java.util.Map
for (Type type : pType.getActualTypeArguments()) {
System.out.println(type); // Print it twice: class java.lang.string}}}Copy the code
3.3, GenericArrayType
public class TestType<T> {
List<String>[] lists;
public static void main(String[] args) throws Exception {
Field f = TestType.class.getDeclaredField("lists"); GenericArrayType genericType = (GenericArrayType) f.getGenericType(); System.out.println(genericType.getGenericComponentType()); }}Copy the code
3.4, WildcardType
public class TestType {
private List<? extends Number> a; / / ceiling
private List<? super String> b; / / lower limit
public static void main(String[] args) throws Exception {
Field fieldA = TestType.class.getDeclaredField("a");
Field fieldB = TestType.class.getDeclaredField("b");
// Get the template type first
ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType();
ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType();
// Get the wildcard type from the template
WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0];
WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0];
// Method test
System.out.println(wTypeA.getUpperBounds()[0]); // class java.lang.Number
System.out.println(wTypeB.getLowerBounds()[0]); // class java.lang.String
// What is the wildcard type? extends java.lang.NumberSystem.out.println(wTypeA); }}Copy the code
As you can see from the above four classes, to get the actual type of a generic type, ParameterizedType is required, which is obtained from getActualTypeArguments.
Now that we know about these apis, how do we implement a method that gets generic types ourselves, as we’ll see in the next section
4. Customize TypeToken
public class MyTypeToken<T> {
final Type type;
protected MyTypeToken(a) {
this.type = getSuperclassTypeParameter(this.getClass());
}
static Type getSuperclassTypeParameter(Class
subclass) {
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class) {
throw new RuntimeException("Missing type parameter.");
} else {
ParameterizedType parameterized = (ParameterizedType)superclass;
return parameterized.getActualTypeArguments()[0]; }}public Type getType(a) {
returntype; }}Copy the code
Usage:
String r1Str = "{\"errCode\":0,\"errMsg\":\"ok\",\"data\":{\"name\":\"result1\"}}";
System.out.println(r1Str);
ResponseData<Result> responseData = gson.fromJson(r1Str,new MyTypeToken<ResponseData<Result>>(){}.getType());
System.out.println(responseData.getData().getName());
Copy the code
⚠️new MyTypeToken
(){} *{}**.